Переглянути джерело

fix bugs, add check clone and fakegps and release for 5.0.56

alqindiirsyam 1 місяць тому
батько
коміт
2095189af0

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

@@ -568,7 +568,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.55;
+				MARKETING_VERSION = 5.0.56;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -604,7 +604,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.55;
+				MARKETING_VERSION = 5.0.56;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -640,7 +640,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.52;
+				MARKETING_VERSION = 5.0.56;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -679,7 +679,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.52;
+				MARKETING_VERSION = 5.0.56;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 2
AppBuilder/AppBuilder/AppDelegate.swift

@@ -104,8 +104,7 @@ extension AppDelegate: ConnectDelegate, UNUserNotificationCenterDelegate {
     }
     
     func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
-        APIS.showNotificationNexilis(userInfo)
-        completionHandler(.newData)
+        APIS.showNotificationNexilis(userInfo, completionHandler)
     }
     func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
         completionHandler([.banner, .sound, .badge])

BIN
NexilisLite/.DS_Store


BIN
NexilisLite/NexilisLite/.DS_Store


+ 105 - 130
NexilisLite/NexilisLite/Source/APIS.swift

@@ -1558,104 +1558,108 @@ public class APIS: NSObject {
     
     public static var uuidCall: UUID?
     public static var fpinCall: String?
-    public static func showNotificationNexilis(_ userInfo: [AnyHashable : Any]) {
-//        print("MASUK SHOW NOTIFICATION NEXILIS: \(userInfo)")
-        if checkAppStateisBackground() {
-//            Nexilis.sendStateToServer(s: "MASUK SHOW NOTIFICATION NEXILIS")
-//            print("MASUK SHOW NOTIFICATION NEXILIS: \(userInfo)")
-            DispatchQueue.main.async {
-                if let payload = userInfo["payload"] as? [String: Any] {
-                    if let messagePayload = payload["message"] as? [String: Any] {
-                        if let data = messagePayload["data"] as? [String: Any] {
-                            let code = data["nx_code"] as? String ?? ""
-                            if code == "CL01" {
-                                if let message = data["bodies"] as? [String: String] {
-                                    let idAck = data["message_id"] as? String ?? ""
-                                    let messageToSave = TMessage()
-                                    messageToSave.mBodies = message
-                                    do {
-                                        var messageExist = false
-                                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                                            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select message_id from MESSAGE where message_id = '\(message[CoreMessage_TMessageKey.MESSAGE_ID] ?? "")'"), cursor.next() {
-                                                messageExist = true
-                                                cursor.close()
+    public static func showNotificationNexilis(_ userInfo: [AnyHashable : Any], _ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
+        DispatchQueue.main.async {
+            if checkAppStateisBackground() {
+                print("SHOW NOTIF ON BACKGROUND")
+                DispatchQueue.global().async {
+                    if let payload = userInfo["payload"] as? [String: Any] {
+                        if let messagePayload = payload["message"] as? [String: Any] {
+                            if let data = messagePayload["data"] as? [String: Any] {
+                                let code = data["nx_code"] as? String ?? ""
+                                if code == "CL01" {
+                                    if let message = data["bodies"] as? [String: String] {
+                                        let idAck = data["message_id"] as? String ?? ""
+                                        let messageToSave = TMessage()
+                                        messageToSave.mBodies = message
+                                        do {
+                                            var messageExist = false
+                                            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select message_id from MESSAGE where message_id = '\(message[CoreMessage_TMessageKey.MESSAGE_ID] ?? "")'"), cursor.next() {
+                                                    messageExist = true
+                                                    cursor.close()
+                                                }
+                                            })
+                                            if messageExist {
+                                                completionHandler(.newData)
+                                                ackAPN(id: idAck)
+                                                return
                                             }
-                                        })
-                                        if messageExist {
-                                            ackAPN(id: idAck)
-                                            return
+                                        } catch {
+                                            print("error saving message: \(error)")
+                                        }
+                                        completionHandler(.newData)
+                                        APIS.addNotificationNexilis(messageToSave)
+                                        ackAPN(id: idAck)
+                                        Nexilis.saveMessage(message: messageToSave, withStatus: false, fromAPNS: true)
+                                    }
+                                } else if code == "CL03" {
+                                    let callFromName = data["call-from-name"] as? String ?? ""
+                                    let callFrom = data["call-from"] as? String ?? ""
+                                    let callType = data["call-type"] as? String ?? ""
+        //                                uuidCall = UUID()
+                                    fpinCall = callFrom
+                                    Nexilis.callAPNActivated = true
+                                    let center = UNUserNotificationCenter.current()
+                                    let content = UNMutableNotificationContent()
+                                    content.title = callFromName
+                                    if callType == "1" {
+                                        content.body = "Incoming Audio Call".localized()
+                                    } else {
+                                        content.body = "Incoming Video Call".localized()
+                                    }
+                                    content.userInfo = ["id" : callFrom, "type" : code, "callType": callType]
+                                    content.sound = nil
+                                    let request = UNNotificationRequest(identifier: callFrom, content: content, trigger: nil)
+                                    center.add(request) { error in
+                                        if let error = error {
+                                            print("Error scheduling notification: \(error.localizedDescription)")
                                         }
+                                    }
+                                    let session = AVAudioSession.sharedInstance()
+                                    do {
+                                        try session.setCategory(.playback, options: [.duckOthers])
+                                        try session.setActive(true)
                                     } catch {
-                                        print("error saving message: \(error)")
+                                        print("Audio session error: \(error)")
                                     }
-                                    APIS.addNotificationNexilis(messageToSave)
-                                    ackAPN(id: idAck)
-                                    Nexilis.saveMessage(message: messageToSave, withStatus: false, fromAPNS: true)
-                                }
-                            } else if code == "CL03" {
-                                let callFromName = data["call-from-name"] as? String ?? ""
-                                let callFrom = data["call-from"] as? String ?? ""
-                                let callType = data["call-type"] as? String ?? ""
-//                                uuidCall = UUID()
-                                fpinCall = callFrom
-                                Nexilis.callAPNActivated = true
-                                let center = UNUserNotificationCenter.current()
-                                let content = UNMutableNotificationContent()
-                                content.title = callFromName
-                                if callType == "1" {
-                                    content.body = "Incoming Audio Call".localized()
-                                } else {
-                                    content.body = "Incoming Video Call".localized()
-                                }
-                                content.userInfo = ["id" : callFrom, "type" : code, "callType": callType]
-                                content.sound = nil
-                                let request = UNNotificationRequest(identifier: callFrom, content: content, trigger: nil)
-                                center.add(request) { error in
-                                    if let error = error {
-                                        print("Error scheduling notification: \(error.localizedDescription)")
+                                    Nexilis.playRingtoneCall()
+                                    completionHandler(.newData)
+                                } else if code == "CL02" {
+                                    print("data \(data)")
+                                    let callFromName = data["call-cancel-name"] as? String ?? ""
+                                    let callFrom = data["call-cancel"] as? String ?? ""
+                                    let callType = data["call-type"] as? String ?? ""
+        //                                if let uuidCall = uuidCall {
+                                    Nexilis.stopRingtoneCall()
+                                    Nexilis.callAPNActivated = false
+                                    let center = UNUserNotificationCenter.current()
+                                    center.removeDeliveredNotifications(withIdentifiers: [callFrom])
+                                    var textCall = ""
+                                    if callType == "1" {
+                                        textCall = "audio"
+                                    } else {
+                                        textCall = "video"
                                     }
-                                }
-                                let session = AVAudioSession.sharedInstance()
-                                do {
-                                    try session.setCategory(.playback, options: [.duckOthers])
-                                    try session.setActive(true)
-                                } catch {
-                                    print("Audio session error: \(error)")
-                                }
-                                Nexilis.playRingtoneCall()
-                            } else if code == "CL02" {
-                                print("data \(data)")
-                                let callFromName = data["call-cancel-name"] as? String ?? ""
-                                let callFrom = data["call-cancel"] as? String ?? ""
-                                let callType = data["call-type"] as? String ?? ""
-//                                if let uuidCall = uuidCall {
-                                Nexilis.stopRingtoneCall()
-                                Nexilis.callAPNActivated = false
-                                let center = UNUserNotificationCenter.current()
-                                center.removeDeliveredNotifications(withIdentifiers: [callFrom])
-                                var textCall = ""
-                                if callType == "1" {
-                                    textCall = "audio"
-                                } else {
-                                    textCall = "video"
-                                }
-                                let content = UNMutableNotificationContent()
-                                content.title = callFromName
-                                content.body = "☎️ Missed \(textCall) call".localized()
-                                content.userInfo = ["id" : callFrom, "type" : code, "callType": callType]
-                                content.sound = nil
-                                let request = UNNotificationRequest(identifier: callFrom, content: content, trigger: nil)
-                                center.add(request) { error in
-                                    if let error = error {
-                                        print("Error scheduling notification: \(error.localizedDescription)")
+                                    let content = UNMutableNotificationContent()
+                                    content.title = callFromName
+                                    content.body = "☎️ Missed \(textCall) call".localized()
+                                    content.userInfo = ["id" : callFrom, "type" : code, "callType": callType]
+                                    content.sound = nil
+                                    let request = UNNotificationRequest(identifier: callFrom, content: content, trigger: nil)
+                                    center.add(request) { error in
+                                        if let error = error {
+                                            print("Error scheduling notification: \(error.localizedDescription)")
+                                        }
                                     }
+                                    Nexilis.saveMessageCall(idCall: (User.getMyPin() ?? "") + CoreMessage_TMessageUtil.getTID(), textMessage: "Missed \(textCall) call".localized() + " at 0", fPin: callFrom, lPin: (User.getMyPin() ?? ""), timeCall: String(Date().currentTimeMillis()), attachment_type: MessageScope.MISSED_CALL)
+                                    completionHandler(.newData)
                                 }
-                                Nexilis.saveMessageCall(idCall: (User.getMyPin() ?? "") + CoreMessage_TMessageUtil.getTID(), textMessage: "Missed \(textCall) call".localized() + " at 0", fPin: callFrom, lPin: (User.getMyPin() ?? ""), timeCall: String(Date().currentTimeMillis()), attachment_type: MessageScope.MISSED_CALL)
                             }
                         }
+                    } else if let message_id = userInfo["message_id"] as? String {
+                        getMessageById(id: message_id, completionHandler)
                     }
-                } else if let message_id = userInfo["message_id"] as? String {
-                    getMessageById(id: message_id)
                 }
             }
         }
@@ -1723,15 +1727,16 @@ public class APIS: NSObject {
         }
     }
     
-    private static func getMessageById(id: String) {
+    private static func getMessageById(id: String, _ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
         DispatchQueue.global().async {
             let parameter: [String : Any] = [
                 "pin": User.getMyPin() ?? "",
                 "message_id": id
             ]
-//            HttpBackgroundManager().startUpload(parameter: parameter, to: URL(string: Utils.getDomainOpr() + "pull_notification")!, identifier: "pull_notification")
             Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getDomainOpr() + "pull_notification")!, parameter: parameter, isFormData: true) { data, response, error in
-                if let data = data {
+                if error != nil {
+                    completionHandler(.failed)
+                } else if let data = data {
                     do {
                         if let dataString = String(data: data, encoding: .utf8) {
                             if let jsonObj = try JSONSerialization.jsonObject(with: dataString.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [String: Any] {
@@ -1758,49 +1763,19 @@ public class APIS: NSObject {
                                 DispatchQueue.main.async {
                                     UIApplication.shared.applicationIconBadgeNumber = Int(APIS.getTotalCounter())
                                 }
+                                completionHandler(.newData)
                             }
                         }
                     } catch {
                         
                     }
+                } else {
+                    completionHandler(.failed)
+                    DispatchQueue.main.async {
+                        UIApplication.shared.applicationIconBadgeNumber = Int(APIS.getTotalCounter())
+                    }
                 }
             }
-            DispatchQueue.main.async {
-                UIApplication.shared.applicationIconBadgeNumber = Int(APIS.getTotalCounter())
-            }
-//            Nexilis.sendStateToServer(s: "send ack from apn")
-//            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 {
-//                        print("nGetCLXConnState: 0")
-//                        Thread.sleep(forTimeInterval: 1)
-//                    }
-//                    print("nGetCLXConnState: lewat")
-//                    getMessage()
-//                } else {
-//                    getMessage()
-//                }
-//                func getMessage() {
-//                    if let result = Nexilis.writeSync(message: CoreMessage_TMessageBank.getMessageById(messageId: id), timeout: 30 * 1000) {
-//                        print("result: \(result.toLogString())")
-//                        if result.isOk() {
-//                            let respData = result.getBody(key: CoreMessage_TMessageKey.DATA)
-//                            if let data = Data(base64Encoded: respData, options: .ignoreUnknownCharacters),
-//                               let decodedString = String(data: data, encoding: .utf8) {
-//                                let message = TMessage(data: decodedString)
-//                                print("message: \(message.toLogString())")
-//                            }
-//                        } else {
-//
-//                        }
-//                    } else {
-//                    }
-//                }
-//            } catch {
-//
-//            }
         }
     }
     
@@ -2163,9 +2138,9 @@ public class APIS: NSObject {
         }
     }
 
-    public static func checkClone(window: inout UIWindow?) {
-        CloneCheck.enforceAllChecks(window: &window)
-    }
+//    public static func checkClone(window: inout UIWindow?) {
+//        CloneCheck.enforceAllChecks(window: &window)
+//    }
     
     public static func checkAppStateisBackground() -> Bool {
         let state = UIApplication.shared.applicationState

+ 0 - 0
NexilisLite/CloneCheck.swift → NexilisLite/NexilisLite/Source/CloneCheck.swift


+ 10 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift

@@ -2880,4 +2880,14 @@ public class CoreMessage_TMessageBank {
         return tMessage
     }
     
+    public static func pullFormSyc(formId: String) -> TMessage {
+        let tMessage = NexilisLite.TMessage()
+        let me = User.getMyPin() ?? ""
+        tMessage.mPIN = me
+        tMessage.mCode = CoreMessage_TMessageCode.PULL_FORM_SYNC
+        tMessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tMessage.mBodies[CoreMessage_TMessageKey.FORM_ID] = formId
+        return tMessage
+    }
+    
 }

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

@@ -270,14 +270,16 @@ public class CoreMessage_TMessageCode {
     public static let IMAGE_DOWNLOAD = "A109";
     
     public static let REQUEST_FORM_LIST = "A112";
+    public static let PULL_FORM_SYNC = "A112A";
     public static let FORM_PUSH = "A113";
     public static let FORM_PUSH_UPDATE = "A113A";
     public static let FORM_PIC_SUBMIT = "A114";
-    public static let SUBMIT_FORM    = "A115";
-    public static let APPROVE_FORM   = "A115A";
+    public static let SUBMIT_FORM = "A115";
+    public static let APPROVE_FORM = "A115A";
     public static let FOLLOW_FORM = "A115B";
     public static let SUB_ACTIVITY_UPDATE = "A115C";
-    public static let APPROVE_SUBMIT_STATUS   = "A115D";
+    public static let APPROVE_SUBMIT_STATUS = "A115D";
+    public static let PULL_INSTANT_MESSAGING_ATTACHMENT = "PIA";
     
     public static let FOLLOW_PERSON         = "B01";
     public static let UNFOLLOW_PERSON       = "B02";

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

@@ -423,6 +423,10 @@ public class CoreMessage_TMessageKey {
     public static let SUB_TOTAL = "A150"
     public static let IS_CALL_CENTER = "icc"
     public static let CALL_CENTER_ID = "ccid"
+    public static let IS_BROADCAST_MESSAGE = "ibm";
+    public static let EX_BOOK = "exbook";
+    public static let EX_BOOK1 = "exbook1";
+    public static let LN = "LN";
     
     public static let MERCHANT_ID = "MERID"
     public static let MERCHANT_IMAGE = "MERIMG"
@@ -499,6 +503,12 @@ public class CoreMessage_TMessageKey {
     public static let SCALE = "SCL";
     public static let STYLE = "STL";
     public static let GIF_ID = "GF";
+    public static let FLOATING_MODE = "FLM";
+    public static let BACKGROUND = "BCG";
+    public static let COLOR = "CLR";
+    public static let ICON_TITLE = "ICT";
+    public static let ICON_SUFFIX = "ICSF";
+    public static let FOOTER = "FT";
     
     public static let IS_SECRET = "sct";
     public static let IS_DELETED_RETENTION = "idl";

+ 113 - 61
NexilisLite/NexilisLite/Source/Database.swift

@@ -98,10 +98,28 @@ public class Database {
         database?.inTransaction({(fmdb, rollback) in
             do {
                 try createDatabase(fmdb: fmdb)
+                //MESSAGE_SUMMARY
                 addColumnIfNeeded(database: fmdb, tableName: "MESSAGE_SUMMARY", columnName: "pinned", columnType: "INTEGER", defaultValue: "0")
                 addColumnIfNeeded(database: fmdb, tableName: "MESSAGE_SUMMARY", columnName: "archived", columnType: "INTEGER", defaultValue: "0")
-                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "is_pinned", columnType: "TEXT", defaultValue: "0")
+                
+                //MESSAGE
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "notif_broadcast", columnType: "INTEGER", defaultValue: "0")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "is_broadcast", columnType: "INTEGER", defaultValue: "0")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "ex_book", columnType: "TEXT", defaultValue: "")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "ex_book1", columnType: "TEXT", defaultValue: "")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "deleted_by_admin", columnType: "INTEGER", defaultValue: "0")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "is_work_mode", columnType: "INTEGER", defaultValue: "0")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "is_hidden", columnType: "INTEGER", defaultValue: "0")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "attachment_speciality", columnType: "TEXT", defaultValue: "")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "gif_id", columnType: "TEXT", defaultValue: "")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "style", columnType: "INTEGER", defaultValue: "0")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "story_url", columnType: "TEXT", defaultValue: "")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "story_text", columnType: "TEXT", defaultValue: "")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "story_thumb", columnType: "TEXT", defaultValue: "")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "story_pin", columnType: "TEXT", defaultValue: "")
                 addColumnIfNeeded(database: fmdb, tableName: "MESSAGE", columnName: "attachment_speciality", columnType: "TEXT", defaultValue: "")
+                
+                //COMMUNITY
                 changeNameColumn(database: fmdb, tableName: "COMMUNITY", oldColumnName: "group_type", newColumnName: "community_type")
                 result = 1
 //                    print("Create Done")
@@ -276,66 +294,78 @@ public class Database {
                                 "'is_education' INTEGER DEFAULT 0)", values: nil)
         
         try fmdb.executeUpdate("CREATE TABLE IF NOT EXISTS 'MESSAGE' (" +
-                                "'message_id' TEXT NOT NULL UNIQUE," +
-                                "'f_pin' TEXT," +
-                                "'l_pin' TEXT," +
-                                "'message_scope_id' TEXT," +
-                                "'server_date' INTEGER," +
-                                "'status' TEXT," +
-                                "'message_text' TEXT," +
-                                "'audio_id' TEXT," +
-                                "'video_id' TEXT," +
-                                "'image_id' TEXT," +
-                                "'thumb_id' TEXT," +
-                                "'opposite_pin' TEXT," +
-                                "'lock' TEXT," +
-                                "'format' TEXT," +
-                                "'broadcast_flag' INTEGER DEFAULT 0," +
-                                "'blog_id' TEXT," +
-                                "'f_user_id' TEXT," +
-                                "'l_user_id' TEXT," +
-                                "'read_receipts' INTEGER DEFAULT 0," +
-                                "'chat_id' TEXT," +
-                                "'file_id' TEXT," +
-                                "'delivery_receipts' INTEGER DEFAULT 0," +
-                                "'account_type' TEXT," +
-                                "'contact' TEXT," +
-                                "'credential' TEXT," +
-                                "'attachment_flag' INTEGER DEFAULT 0," +
-                                "'is_stared' INTEGER DEFAULT 0," +
-                                "'f_display_name' TEXT," +
-                                "'reff_id' TEXT," +
-                                "'sent_qty' INTEGER DEFAULT 0," +
-                                "'delivered_qty' INTEGER DEFAULT 0," +
-                                "'read_qty' INTEGER DEFAULT 0," +
-                                "'ack_qty' INTEGER DEFAULT 0," +
-                                "'read_local_qty' INTEGER DEFAULT 0," +
-                                "'delivered_pin' TEXT," +
-                                "'read_pin' TEXT," +
-                                "'ack_pin' TEXT," +
-                                "'read_local_pin' TEXT," +
-                                "'expired_qty' TEXT," +
-                                "'message_large_text' TEXT," +
-                                "'tag_forum' TEXT," +
-                                "'tag_activity' TEXT," +
-                                "'unk_numbers' INTEGER DEFAULT 0," +
-                                "'conn_state' INTEGER DEFAULT 1," +
-                                "'tag_client' TEXT," +
-                                "'gif_id' TEXT," +
-                                "'tag_subactivity' TEXT," +
-                                "'messagenumber' INTEGER DEFAULT 0," +
-                                "'mail_account' TEXT," +
-                                "'message_text_plain' TEXT," +
-                                "'local_timestamp' TEXT," +
-                                "'is_consult' INTEGER DEFAULT 0," +
-                                "'is_call_center' INTEGER DEFAULT 0," +
-                                "'call_center_id' TEXT," +
-                                "'last_edited' INTEGER DEFAULT 0," +
-                                "'is_secret' INTEGER DEFAULT 0," +
-                                "'is_deleted_retention' INTEGER DEFAULT 0," +
-                                "'is_forwarded_message' INTEGER DEFAULT 0," +
-                                "'is_pinned' INTEGER DEFAULT 0," +
-                                "'attachment_speciality' TEXT" +
+                               "'message_id' TEXT NOT NULL UNIQUE," +
+                               "'f_pin' TEXT," +
+                               "'l_pin' TEXT," +
+                               "'message_scope_id' TEXT," +
+                               "'server_date' INTEGER," +
+                               "'status' TEXT," +
+                               "'message_text' TEXT," +
+                               "'audio_id' TEXT," +
+                               "'video_id' TEXT," +
+                               "'image_id' TEXT," +
+                               "'thumb_id' TEXT," +
+                               "'opposite_pin' TEXT," +
+                               "'lock' TEXT," +
+                               "'format' TEXT," +
+                               "'broadcast_flag' INTEGER DEFAULT 0," +
+                               "'blog_id' TEXT," +
+                               "'f_user_id' TEXT," +
+                               "'l_user_id' TEXT," +
+                               "'read_receipts' INTEGER DEFAULT 0," +
+                               "'chat_id' TEXT," +
+                               "'file_id' TEXT," +
+                               "'delivery_receipts' INTEGER DEFAULT 0," +
+                               "'account_type' TEXT," +
+                               "'contact' TEXT," +
+                               "'credential' TEXT," +
+                               "'attachment_flag' INTEGER DEFAULT 0," +
+                               "'is_stared' INTEGER DEFAULT 0," +
+                               "'f_display_name' TEXT," +
+                               "'reff_id' TEXT," +
+                               "'sent_qty' INTEGER DEFAULT 0," +
+                               "'delivered_qty' INTEGER DEFAULT 0," +
+                               "'read_qty' INTEGER DEFAULT 0," +
+                               "'ack_qty' INTEGER DEFAULT 0," +
+                               "'read_local_qty' INTEGER DEFAULT 0," +
+                               "'delivered_pin' TEXT," +
+                               "'read_pin' TEXT," +
+                               "'ack_pin' TEXT," +
+                               "'read_local_pin' TEXT," +
+                               "'expired_qty' TEXT," +
+                               "'message_large_text' TEXT," +
+                               "'tag_forum' TEXT," +
+                               "'tag_activity' TEXT," +
+                               "'unk_numbers' INTEGER DEFAULT 0," +
+                               "'conn_state' INTEGER DEFAULT 1," +
+                               "'tag_client' TEXT," +
+                               "'tag_subactivity' TEXT," +
+                               "'messagenumber' INTEGER DEFAULT 0," +
+                               "'mail_account' TEXT," +
+                               "'message_text_plain' TEXT," +
+                               "'local_timestamp' TEXT," +
+                               "'is_consult' INTEGER DEFAULT 0," +
+                               "'is_call_center' INTEGER DEFAULT 0," +
+                               "'call_center_id' INTEGER DEFAULT 0," +
+                               "'notif_broadcast' INTEGER DEFAULT 0," +
+                               "'is_broadcast' INTEGER DEFAULT 0," +
+                               "'ex_book' TEXT," +
+                               "'ex_book1' TEXT," +
+                               "'deleted_by_admin' INTEGER DEFAULT 0," +
+                               "'is_work_mode' INTEGER DEFAULT 0," +
+                               "'is_hidden' INTEGER DEFAULT 0," +
+                               "'gif_id' TEXT," +
+                               "'style' INTEGER DEFAULT 0," +
+                               "'last_edited' INTEGER DEFAULT 0," +
+                               "'is_secret' INTEGER DEFAULT 0," +
+                               "'is_deleted_retention' INTEGER DEFAULT 0," +
+                               "'is_forwarded_message' INTEGER DEFAULT 0," +
+                               "'story_url' TEXT," +
+                               "'story_text' TEXT," +
+                               "'story_thumb' TEXT," +
+                               "'story_pin' TEXT," +
+                               "'is_pinned' INTEGER DEFAULT 0," +
+                               "'attachment_speciality' TEXT" +
                                 ")", values: nil)
         
         try fmdb.executeUpdate("CREATE INDEX IF NOT EXISTS index_m_opposite on MESSAGE (opposite_pin, chat_id)", values: nil)
@@ -633,6 +663,28 @@ public class Database {
                                "'thumb_id' TEXT NOT NULL," +
                                "'created_date' TEXT DEFAULT (0)," +
                                "PRIMARY KEY ('community_id', 'f_pin'))", values: nil)
+        
+        try fmdb.executeUpdate("CREATE TABLE IF NOT EXISTS 'FORM' (" +
+                               "'_id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
+                               "'form_id' TEXT NOT NULL UNIQUE," +
+                               "'name' TEXT," +
+                               "'created_date' TEXT," +
+                               "'created_by' TEXT," +
+                               "'sq_no' INTEGER," +
+                               "'icon_title' TEXT," +
+                               "'icon_suffix' TEXT," +
+                               "'footer' TEXT)", values: nil)
+        
+        try fmdb.executeUpdate("CREATE TABLE IF NOT EXISTS 'FORM_ITEM' (" +
+                               "'_id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
+                               "'form_id' TEXT," +
+                               "'key' TEXT," +
+                               "'label' TEXT," +
+                               "'value' TEXT," +
+                               "'type' TEXT," +
+                               "'sq_no' INTEGER," +
+                               "'background' TEXT," +
+                               "'color' TEXT)", values: nil)
     }
     
     public func executes(fmdb: FMDatabase, queries: [String]) {

+ 35 - 6
NexilisLite/NexilisLite/Source/Extension.swift

@@ -455,7 +455,7 @@ extension NSObject {
     
     private static var urlStore = [String:String]()
 
-    public func getImage(name url: String, placeholderImage: UIImage? = nil, isCircle: Bool = false, tableView: UITableView? = nil, indexPath: IndexPath? = nil, completion: @escaping (Bool, Bool, UIImage?)->()) {
+    public func getImage(name url: String, placeholderImage: UIImage? = nil, isCircle: Bool = false, tableView: UITableView? = nil, indexPath: IndexPath? = nil, isResized: Bool = true, completion: @escaping (Bool, Bool, UIImage?)->()) {
         let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
         type(of: self).urlStore[tmpAddress] = url
         if url.isEmpty {
@@ -466,8 +466,12 @@ extension NSObject {
             let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
             let file = documentDir.appendingPathComponent(url)
             if FileManager().fileExists(atPath: file.path) {
-                let image = UIImage(contentsOfFile: file.path)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
-                completion(true, false, isCircle ? image?.circleMasked : image)
+                var image = UIImage(contentsOfFile: file.path)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
+                if isResized {
+                    completion(true, false, isCircle ? image?.circleMasked : image)
+                } else {
+                    completion(true, false, isCircle ? UIImage(contentsOfFile: file.path)?.circleMasked : UIImage(contentsOfFile: file.path))
+                }
             } else if var tempData = try FileEncryption.shared.readSecure(filename: url) {
                 let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: tempData)
                 if dataDecrypt != nil {
@@ -475,7 +479,11 @@ extension NSObject {
                 }
                 let image = UIImage(data: tempData)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
 //                FileEncryption.shared.wipeData(&tempData)
-                completion(true, true, isCircle ? image?.circleMasked : image)
+                if isResized {
+                    completion(true, false, isCircle ? image?.circleMasked : image)
+                } else {
+                    completion(true, false, isCircle ? UIImage(data: tempData)?.circleMasked : UIImage(data: tempData))
+                }
             } else {
 //                completion(false, false, placeholderImage)
                 Download().startHTTP(forKey: url) { (name, progress) in
@@ -487,7 +495,11 @@ extension NSObject {
                         if type(of: self).urlStore[tmpAddress] == name && tableView == nil {
                             if FileManager().fileExists(atPath: file.path) {
                                 let image = UIImage(contentsOfFile: file.path)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
-                                completion(true, true, isCircle ? image?.circleMasked : image)
+                                if isResized {
+                                    completion(true, false, isCircle ? image?.circleMasked : image)
+                                } else {
+                                    completion(true, false, isCircle ? UIImage(contentsOfFile: file.path)?.circleMasked : UIImage(contentsOfFile: file.path))
+                                }
                             } else if FileEncryption.shared.isSecureExists(filename: url) {
                                 do {
                                     if var imageData = try FileEncryption.shared.readSecure(filename: url) {
@@ -496,7 +508,11 @@ extension NSObject {
                                             imageData = dataDecrypt!
                                         }
                                         let image = UIImage(data: imageData)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
-                                        completion(true, true, isCircle ? image?.circleMasked : image)
+                                        if isResized {
+                                            completion(true, false, isCircle ? image?.circleMasked : image)
+                                        } else {
+                                            completion(true, false, isCircle ? UIImage(data: imageData)?.circleMasked : UIImage(data: imageData))
+                                        }
                                     }
                                 } catch {
                                     
@@ -1194,6 +1210,7 @@ extension Bundle {
 //
 //}
 
+private var actionKey: UInt8 = 0
 extension UIButton {
     private func actionHandleBlock(action:(() -> Void)? = nil) {
         struct __ {
@@ -1220,6 +1237,18 @@ extension UIButton {
         self.titleEdgeInsets = UIEdgeInsets(top: 0, left: -((image?.size.width ?? 0) + 5), bottom: 0, right: (image?.size.width ?? 0))
         self.imageEdgeInsets = UIEdgeInsets(top: 10, left: (self.titleLabel?.frame.size.width ?? 0) + 90, bottom: 10, right: -((self.titleLabel?.frame.size.width ?? 0) + 5))
     }
+    
+    func addAction(for controlEvents: UIControl.Event = .touchUpInside,
+                   _ closure: @escaping (UIButton) -> Void) {
+        objc_setAssociatedObject(self, &actionKey, closure, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        addTarget(self, action: #selector(handleAction), for: controlEvents)
+    }
+    
+    @objc private func handleAction() {
+        if let closure = objc_getAssociatedObject(self, &actionKey) as? (UIButton) -> Void {
+            closure(self)
+        }
+    }
 }
 
 extension UINavigationController {

+ 1 - 1
NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -1333,7 +1333,7 @@ class IncomingThread {
     }
     
     private func receiveMessage(message: TMessage) -> Void {
-        print("recive message \(message.toLogString())")
+//        print("receive message \(message.toLogString())")
         if Utils.getSecureFolderOffline() == "0" {
             if API.nGetCLXConnState() == 0 {
                 do {

+ 37 - 0
NexilisLite/NexilisLite/Source/Model/Form.swift

@@ -0,0 +1,37 @@
+//
+//  Form.swift
+//  Pods
+//
+//  Created by Qindi on 27/08/25.
+//
+
+import Foundation
+
+public class FormM: Model {
+    public var formId: String
+    public var title: String
+    public var createdDate: String
+    public var createdBy: String
+    public var sqNo: Int64
+    public var iconTitle: String
+    public var iconSuffix: String
+    public var footer: String
+    public var description: String
+    
+    public init(formId: String, title: String, createdDate: String, createdBy: String, sqNo: Int64, iconTitle: String, iconSuffix: String, footer: String) {
+        self.formId = formId
+        self.title = title
+        self.createdDate = createdDate
+        self.createdBy = createdBy
+        self.sqNo = sqNo
+        self.iconTitle = iconTitle
+        self.iconSuffix = iconSuffix
+        self.footer = footer
+        self.description = ""
+    }
+    
+    public static func == (lhs: FormM, rhs: FormM) -> Bool {
+        return lhs.formId == rhs.formId
+    }
+    
+}

+ 49 - 0
NexilisLite/NexilisLite/Source/Model/FormItem.swift

@@ -0,0 +1,49 @@
+//
+//  FormItem.swift
+//  Pods
+//
+//  Created by Qindi on 27/08/25.
+//
+
+import Foundation
+
+public class FormItemM: Model {
+    public var formId: String
+    public var label: String
+    public var value: String
+    public var key: String
+    public var sqNo: Int64
+    public var type: String
+    public var background: String
+    public var color: String
+    public var description: String
+    
+    public init(formId: String, label: String, value: String, key: String, sqNo: Int64, type: String, background: String, color: String) {
+        self.formId = formId
+        self.label = label
+        self.value = value
+        self.key = key
+        self.sqNo = sqNo
+        self.type = type
+        self.background = background
+        self.color = color
+        self.description = ""
+    }
+    
+    public init(formId: String) {
+        self.formId = formId
+        self.label = ""
+        self.value = ""
+        self.key = ""
+        self.sqNo = 0
+        self.type = ""
+        self.background = ""
+        self.color = ""
+        self.description = ""
+    }
+    
+    public static func == (lhs: FormItemM, rhs: FormItemM) -> Bool {
+        return lhs.formId == rhs.formId
+    }
+    
+}

+ 108 - 1
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.55"
+    public static var cpaasVersion = "5.0.56"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""
@@ -1663,6 +1663,8 @@ public class Nexilis: NSObject {
                         "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,
@@ -3051,6 +3053,111 @@ extension Nexilis: MessageDelegate {
     func showBroadcastMessage(m: [String: String]) {
         let fileType = m[CoreMessage_TMessageKey.CATEGORY_FLAG]!
         let gifId = m[CoreMessage_TMessageKey.GIF_ID] ?? ""
+        let scopeBrodcast = m[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID]
+        if scopeBrodcast == "18" {
+            DispatchQueue.global().async {
+                while (API.nGetCLXConnState() == 0) {
+                    Thread.sleep(forTimeInterval: 0.5)
+                }
+                if let stringForm = m[CoreMessage_TMessageKey.MESSAGE_TEXT] {
+                    if let data = stringForm.data(using: .utf8) {
+                        do {
+                            if let jsonForm = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
+                                let formId = jsonForm["form_id"] as! String
+                                if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.pullFormSyc(formId: formId)) {
+                                    if response.isOk() {
+                                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                            do {
+                                                let formId = response.getBody(key: CoreMessage_TMessageKey.FORM_ID)
+                                                let title = response.getBody(key: CoreMessage_TMessageKey.TITLE)
+                                                let createdDate = response.getBody(key: CoreMessage_TMessageKey.CREATED_DATE)
+                                                let createdBy = response.getBody(key: CoreMessage_TMessageKey.CREATED_BY)
+                                                let seq = response.getBodyAsLong(key: CoreMessage_TMessageKey.SEQUENCE, default_value: 0)
+                                                let iconTitle = response.getBody(key: CoreMessage_TMessageKey.ICON_TITLE)
+                                                let iconSuffix = response.getBody(key: CoreMessage_TMessageKey.ICON_SUFFIX)
+                                                let footer = response.getBody(key: CoreMessage_TMessageKey.FOOTER)
+                                                let form = FormM(formId: formId, title: title, createdDate: createdDate, createdBy: createdBy, sqNo: Int64(seq), iconTitle: iconTitle, iconSuffix: iconSuffix, footer: footer)
+                                                
+                                                _ = try Database.shared.insertRecord(fmdb: fmdb, table: "FORM", cvalues: [
+                                                    "form_id" : formId,
+                                                    "name" : title,
+                                                    "created_date" : createdDate,
+                                                    "created_by" : createdBy,
+                                                    "sq_no" : seq,
+                                                    "icon_title" : iconTitle,
+                                                    "icon_suffix" : iconSuffix,
+                                                    "footer" : footer
+                                                ], replace: true)
+                                                var isButton = false
+                                                let data = response.getBody(key: CoreMessage_TMessageKey.DATA)
+                                                var firstFormItem: FormItemM = FormItemM(formId: "")
+                                                if !data.isEmpty {
+                                                    if let jsonArray = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {
+                                                        for json in jsonArray {
+                                                            let key = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.KEY)
+                                                            let label = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.LABEL)
+                                                            let value = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.VALUE)
+                                                            let type = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.TYPE)
+                                                            let background = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.BACKGROUND)
+                                                            let color = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.COLOR)
+                                                            let formItem = FormItemM(formId: formId, label: label, value: value, key: key, sqNo: Int64(seq), type: type, background: background, color: color)
+                                                            firstFormItem = formItem
+                                                            if type == "26" {
+                                                                isButton = true
+                                                            }
+                                                            
+                                                            _ = try Database.shared.insertRecord(fmdb: fmdb, table: "FORM_ITEM", cvalues: [
+                                                                "form_id" : formId,
+                                                                "key" : key,
+                                                                "label" : label,
+                                                                "value" : value,
+                                                                "type" : type,
+                                                                "sq_no" : seq,
+                                                                "background" : background,
+                                                                "color" : color
+                                                            ], replace: true)
+                                                        }
+                                                    }
+                                                }
+                                                if isButton {
+                                                    DispatchQueue.main.async {
+                                                        let dialog = DialogBroadcastInApp()
+                                                        dialog.modalTransitionStyle = .crossDissolve
+                                                        dialog.modalPresentationStyle = .overCurrentContext
+                                                        dialog.form = form
+                                                        dialog.formItem = firstFormItem
+                                                        dialog.listTitleButton = (m["exbook1"]!).components(separatedBy: ",")
+                                                        let brdTitle = jsonForm["brd_title"] as? String
+                                                        let formL = jsonForm["form_label"] as? String
+                                                        if brdTitle != nil && formL != nil{
+                                                            dialog.labelForm = formL ?? ""
+                                                        }
+                                                        dialog.message = m
+                                                        UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                                    }
+                                                } else {
+                                                    print("show broadcast no button")
+                                                }
+                                            } catch {
+                                                rollback.pointee = true
+                                                print("Access database error: \(error.localizedDescription)")
+                                            }
+                                        })
+                                    } else {
+                                        self.showBroadcastMessage(m: m)
+                                    }
+                                } else {
+                                    self.showBroadcastMessage(m: m)
+                                }
+                            }
+                        } catch {
+                            print("Error converting string to JSON:", error)
+                        }
+                    }
+                }
+            }
+            return
+        }
         let broadcastVC = UIViewController()
         if let viewBroadcast = broadcastVC.view {
             broadcastVC.modalPresentationStyle = .custom

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

@@ -787,6 +787,8 @@ public final class Utils {
         let urlConfig = URLSessionConfiguration.default
         urlConfig.timeoutIntervalForRequest = 30.0
         urlConfig.timeoutIntervalForResource = 60.0
+        urlConfig.isDiscretionary = false
+        urlConfig.sessionSendsLaunchEvents = true
         let sessionDelegate = SelfSignedURLSessionDelegate()
         let session = URLSession(configuration: urlConfig, delegate: sessionDelegate, delegateQueue: nil)
         let task = session.dataTask(with: request, completionHandler: completion)
@@ -2997,6 +2999,177 @@ public class DialogErrorMFA: UIViewController {
     }
 }
 
+public class DialogBroadcastInApp: UIViewController {
+    
+    public var form: FormM!
+    public var formItem: FormItemM!
+    public var labelForm = ""
+    public var listTitleButton: [String] = []
+    public var message: [String: Any] = [:]
+    
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+        self.view.backgroundColor = .black.withAlphaComponent(0.5)
+        
+        let container = UIView()
+        self.view.addSubview(container)
+        container.anchor(left: self.view.leftAnchor, right: self.view.rightAnchor, paddingLeft: 20, paddingRight: 20, centerY: self.view.centerYAnchor)
+        container.layer.cornerRadius = 20.0
+        container.clipsToBounds = true
+        container.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .white
+        
+        let title = UILabel()
+        title.text = form.title
+        title.font = .boldSystemFont(ofSize: 14)
+        title.numberOfLines = 0
+        title.textAlignment = .center
+        title.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
+        container.addSubview(title)
+        title.anchor(top: container.topAnchor, paddingTop: 15, centerX: container.centerXAnchor, maxWidth: UIScreen.main.bounds.width / 2)
+        
+        let imageWarning = UIImageView(image: UIImage(named: "pb_security_warning_green", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
+        if !form.iconTitle.isEmpty {
+            getImage(name: form.iconTitle, completion: { result, isDownloaded, image in
+                DispatchQueue.main.async {
+                    if let img = image {
+                        DispatchQueue.main.async {
+                            imageWarning.image = image
+                        }
+                    }
+                }
+            })
+        }
+        container.addSubview(imageWarning)
+        imageWarning.anchor(top: container.topAnchor, right: title.leftAnchor, paddingTop: 10, paddingRight: -5, width: 30, height: 30)
+        
+        let imageLogo = UIImageView(image: UIImage(named: "bjb-blue-flat", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
+        container.addSubview(imageLogo)
+        imageLogo.anchor(top: container.topAnchor, left: container.leftAnchor, paddingTop: 10, paddingLeft: 10, width: 40, height: 40)
+        
+        let imageChat = UIImageView(image: UIImage(named: "pb_startup_iconsuffix", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
+        if !form.iconSuffix.isEmpty {
+            getImage(name: form.iconSuffix, completion: { result, isDownloaded, image in
+                if let img = image {
+                    DispatchQueue.main.async {
+                        imageChat.image = image
+                    }
+                }
+            })
+        }
+        container.addSubview(imageChat)
+        imageChat.anchor(top: container.topAnchor, right: container.rightAnchor, paddingTop: 10, paddingRight: 10, width: 30, height: 30)
+        
+        let content = labelForm
+        var contentAtt = NSAttributedString(string: "")
+        let contentS = UITextView()
+        contentS.tintColor = .label
+        if HtmlUtils.hasHtmlTag(content) {
+            contentAtt = HtmlUtils.toHTMLPreview(content)
+            contentS.attributedText = contentAtt
+        } else {
+            contentS.attributedText = content.richText()
+        }
+        contentS.isEditable = false
+        contentS.isScrollEnabled = false
+        contentS.dataDetectorTypes = [.link]
+        container.addSubview(contentS)
+        contentS.anchor(top: title.bottomAnchor, left: container.leftAnchor, right: container.rightAnchor, paddingTop: 20, paddingLeft: 15, paddingRight: 10)
+        
+        let spacing: CGFloat = 5
+        let buttonHeight: CGFloat = 35
+        let maxPerRow = 3
+        let parentWidth = UIScreen.main.bounds.width - 40
+        
+        let containerButton = UIView()
+        container.addSubview(containerButton)
+        containerButton.anchor(top: contentS.bottomAnchor, left: container.leftAnchor, right: container.rightAnchor, width: parentWidth)
+        
+        
+        let buttonWidth = (parentWidth - (CGFloat(maxPerRow + 1) * spacing)) / CGFloat(maxPerRow)
+        var finalRow = 1
+        for (index, title) in listTitleButton.enumerated() {
+            let row = index / maxPerRow
+            let col = index % maxPerRow
+            
+            let x = spacing + CGFloat(col) * (buttonWidth + spacing)
+            let y = spacing + CGFloat(row) * (buttonHeight + spacing)
+            
+            var finalTitleButton = title
+            if title.starts(with: "call_") {
+                finalTitleButton = "Call " + title.components(separatedBy: "_")[1]
+            } else if title == "cc" {
+                finalTitleButton = "Contact Center"
+            }
+            
+            let button = UIButton(type: .system)
+            button.frame = CGRect(x: x, y: y, width: buttonWidth, height: buttonHeight)
+            button.layer.cornerRadius = 17.5
+            button.clipsToBounds = true
+            button.titleLabel?.font = .boldSystemFont(ofSize: 14)
+            button.setTitleColor(.white, for: .normal)
+            button.addAction{ btn in
+                if title == "cc" {
+                    if self.form.formId == "212953" || self.form.formId == "112903"{
+                        APIS.openContactCenterWithContext(context: self.formItem.label + "~Transaction~Credit Card~Fraud")
+                    } else {
+                        APIS.openContactCenterWithContext(context: self.formItem.label)
+                    }
+                } else if title.starts(with: "call_") {
+                    var phone = Utils.getCallCenter()
+                    if phone.substring(from: 0, to: 0) == "0" {
+                        phone = "+62" + phone.substring(from: 1, to: phone.count)
+                    }
+                    if let url = URL(string: "tel://\(phone)") {
+                        UIApplication.shared.open(url)
+                    }
+                } else {
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
+                            "ex_book" : self.message[CoreMessage_TMessageKey.MESSAGE_TEXT] ?? ""
+                        ], _where: "message_id = '\(self.message[CoreMessage_TMessageKey.MESSAGE_ID] ?? "")'")
+                    })
+                    var messageTextSend = ""
+                    let message = CoreMessage_TMessageBank.sendMessage(l_pin: self.form.formId, message_scope_id: MessageScope.FORM, status: "1", message_text: messageTextSend, credential: "0", attachment_flag: "", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: "", audio_id: "", video_id: "", file_id: self.form.formId, thumb_id: "", reff_id: "", read_receipts: "4", chat_id: "", is_call_center: "0", call_center_id: "", opposite_pin: "", specFile: "")
+                    OutgoingThread.default.addQueue(message: message)
+                    self.dismiss(animated: true)
+                }
+            }
+
+            if formItem.background.isEmpty {
+                button.setTitle(finalTitleButton, for: .normal)
+                button.backgroundColor = .systemBlue
+            } else {
+                let backgrounds = formItem.background.components(separatedBy: ",")
+                if index < backgrounds.count {
+                    let background = backgrounds[index]
+                    button.setTitle("", for: .normal)
+                    getImage(name: background, isResized: false, completion: { result, isDownloaded, image in
+                        if let img = image {
+                            DispatchQueue.main.async {
+                                button.setBackgroundImage(img.resizableImage(withCapInsets: .zero, resizingMode: .stretch), for: .normal)
+                            }
+                        }
+                    })
+                }
+            }
+            
+            containerButton.addSubview(button)
+            finalRow = row + 1
+        }
+        
+        containerButton.heightAnchor.constraint(equalToConstant: CGFloat(35 * finalRow)).isActive = true
+        
+        let footer = UILabel()
+        footer.text = form.footer
+        footer.font = .systemFont(ofSize: 12)
+        footer.textColor = .gray
+        footer.numberOfLines = 0
+        container.addSubview(footer)
+        footer.anchor(top: containerButton.bottomAnchor, bottom: container.bottomAnchor, right: container.rightAnchor, paddingTop: 10, paddingBottom: 5, paddingRight: 10)
+        
+    }
+}
+
 class LocationManager: NSObject, CLLocationManagerDelegate {
     private var locationManager = CLLocationManager()
 
@@ -3448,3 +3621,117 @@ public class CallBannerView: UIView {
         fatalError("init(coder:) has not been implemented")
     }
 }
+
+class HtmlUtils {
+    private static func unescapeHTMLEntities(_ text: String) -> String {
+        var result = text
+
+        // quick named entity replacements
+        let named: [String: String] = [
+            "&lt;": "<",
+            "&gt;": ">",
+            "&amp;": "&",
+            "&quot;": "\"",
+            "&apos;": "'",
+            "&#039;": "'" // common single-quote entity in some HTML sources
+        ]
+        for (k, v) in named {
+            result = result.replacingOccurrences(of: k, with: v)
+        }
+
+        // decode decimal numeric entities like &#39;
+        let decimalPattern = "&#(\\d+);"
+        if let decRegex = try? NSRegularExpression(pattern: decimalPattern, options: []) {
+            let matches = decRegex.matches(in: result, options: [], range: NSRange(location: 0, length: result.utf16.count))
+            for match in matches.reversed() { // reverse so ranges remain valid while replacing
+                guard match.numberOfRanges >= 2,
+                      let numRange = Range(match.range(at: 1), in: result) else { continue }
+                let numStr = String(result[numRange])
+                if let code = Int(numStr), let scalar = UnicodeScalar(code) {
+                    let char = String(scalar)
+                    if let fullRange = Range(match.range(at: 0), in: result) {
+                        result.replaceSubrange(fullRange, with: char)
+                    }
+                }
+            }
+        }
+
+        // decode hex numeric entities like &#x27;
+        let hexPattern = "&#x([0-9a-fA-F]+);"
+        if let hexRegex = try? NSRegularExpression(pattern: hexPattern, options: []) {
+            let matches = hexRegex.matches(in: result, options: [], range: NSRange(location: 0, length: result.utf16.count))
+            for match in matches.reversed() {
+                guard match.numberOfRanges >= 2,
+                      let hexRange = Range(match.range(at: 1), in: result) else { continue }
+                let hexStr = String(result[hexRange])
+                if let code = Int(hexStr, radix: 16), let scalar = UnicodeScalar(code) {
+                    let char = String(scalar)
+                    if let fullRange = Range(match.range(at: 0), in: result) {
+                        result.replaceSubrange(fullRange, with: char)
+                    }
+                }
+            }
+        }
+
+        return result
+    }
+
+    static func toHTMLPreview(_ pText: String, fontSize: CGFloat = 12) -> NSAttributedString {
+        let unescaped = unescapeHTMLEntities(pText).replacingOccurrences(of: "\n", with: "<br>")
+
+        let parsed: NSAttributedString = {
+            guard let data = unescaped.data(using: .utf8) else { return NSAttributedString(string: unescaped) }
+            do {
+                return try NSAttributedString(
+                    data: data,
+                    options: [
+                        .documentType: NSAttributedString.DocumentType.html,
+                        .characterEncoding: String.Encoding.utf8.rawValue
+                    ],
+                    documentAttributes: nil
+                )
+            } catch {
+                return NSAttributedString(string: unescaped)
+            }
+        }()
+
+        // 3) Apply your custom fonts while preserving link attributes
+        let mutable = NSMutableAttributedString(attributedString: parsed)
+        let normalFont = UIFont.systemFont(ofSize: fontSize)
+        let boldFont = UIFont.boldSystemFont(ofSize: fontSize)
+        let italicFont = UIFont.italicSystemFont(ofSize: fontSize)
+        let boldItalicFont = UIFont.systemFont(ofSize: fontSize, weight: .semibold)
+
+        mutable.enumerateAttribute(.font, in: NSRange(location: 0, length: mutable.length)) { value, range, _ in
+            guard let oldFont = value as? UIFont else { return }
+            let traits = oldFont.fontDescriptor.symbolicTraits
+            let newFont: UIFont
+            if traits.contains([.traitBold, .traitItalic]) {
+                newFont = boldItalicFont
+            } else if traits.contains(.traitBold) {
+                newFont = boldFont
+            } else if traits.contains(.traitItalic) {
+                newFont = italicFont
+            } else {
+                newFont = normalFont
+            }
+            // replace font but DO NOT remove link attribute or other attrs
+            mutable.addAttribute(.font, value: newFont, range: range)
+        }
+
+        return mutable
+    }
+    
+    static func hasHtmlTag(_ pText: String) -> Bool {
+        // unescape entities first
+        let unescaped = unescapeHTMLEntities(pText)
+        
+        let pattern = ".*\\<[^>]+>.*"
+        if let regex = try? NSRegularExpression(pattern: pattern,
+                                                options: [.dotMatchesLineSeparators]) {
+            let range = NSRange(location: 0, length: (unescaped as NSString).length)
+            return regex.firstMatch(in: unescaped, options: [], range: range) != nil
+        }
+        return false
+    }
+}

+ 201 - 44
StreamShield/StreamShield/Source/SecurityShield.swift

@@ -292,11 +292,17 @@ private class Process: NSObject, CLLocationManagerDelegate {
                         if jsonData["minimum_ios_version"]! != nil {
                             Preference.setMinimumOsVersion(value: jsonData["minimum_ios_version"]! as! String)
                         }
-                        if jsonData["check_sum"]! != nil {
-                            Preference.setCheckTempering(value: jsonData["check_sum"]! as! String == "1")
-                            Preference.setCheckTemperingAction(value: jsonData["action"]! as! String)
-                            Preference.setCheckTemperingAlertTitle(value: jsonData["alert_title"]! as! String)
-                            Preference.setCheckTemperingAlertMessage(value: jsonData["alert_message"]! as! String)
+//                        if jsonData["check_sum"]! != nil {
+//                            Preference.setCheckTempering(value: jsonData["check_sum"]! as! String == "1")
+//                            Preference.setCheckTemperingAction(value: jsonData["action"]! as! String)
+//                            Preference.setCheckTemperingAlertTitle(value: jsonData["alert_title"]! as! String)
+//                            Preference.setCheckTemperingAlertMessage(value: jsonData["alert_message"]! as! String)
+//                        }
+                        if jsonData["check_cloned_app"]! != nil {
+                            Preference.setCheckCloned(value: jsonData["check_cloned_app"]! as! String == "1")
+                            Preference.setCheckClonedAction(value: jsonData["action"]! as! String)
+                            Preference.setCheckClonedAlertTitle(value: jsonData["alert_title"]! as! String)
+                            Preference.setCheckClonedAlertMessage(value: jsonData["alert_message"]! as! String)
                         }
                         if jsonData["check_hook"]! != nil {
                             Preference.setCheckHooked(value: jsonData["check_hook"]! as! String == "1")
@@ -471,9 +477,8 @@ private class Process: NSObject, CLLocationManagerDelegate {
             }
             subCheck(4)
         } else if typeSecurity == 4 {
-            if checkTempering() {
+            if checkCloned() {
 //                print("ERROR 4")
-                sendShieldErrorLog(code: 14)
                 return
             }
             subCheck(5)
@@ -531,14 +536,12 @@ private class Process: NSObject, CLLocationManagerDelegate {
         } else if typeSecurity == 12 {
             if checkGeovelocity() {
 //                print("ERROR 12")
-                sendShieldErrorLog(code: 21)
                 return
             }
             subCheck(13)
         } else if typeSecurity == 13 {
             if checkBehaviourAnalysis() {
 //                print("ERROR 13")
-                sendShieldErrorLog(code: 17)
                 return
             }
         }
@@ -699,6 +702,14 @@ private class Process: NSObject, CLLocationManagerDelegate {
         return false
     }
     
+    static func checkCloned() -> Bool {
+        if Preference.getCheckCloned() {
+            isCloned()
+            return true
+        }
+        return false
+    }
+    
     static func checkDebugging() -> Bool {
         if Preference.getCheckDebugging() && isDebugging() {
             DispatchQueue.main.async(execute: {
@@ -874,29 +885,8 @@ private class Process: NSObject, CLLocationManagerDelegate {
     }
     
     static func checkGeovelocity() -> Bool {
-        if Preference.getCheckGeoVelocity() && isGeovelocityDetected() {
-            DispatchQueue.main.async(execute: {
-                let alert = SSLibAlertController(title: Preference.getCheckGeoVelocityAlertTitle(), message: Preference.getCheckGeoVelocityAlertMessage(), preferredStyle: .alert)
-                if Preference.getCheckGeoVelocityAction() == PreferencesKey.SECURITY_SHIELD_ALERT_CONTINUE {
-                    alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {_ in
-                        subCheck(13)
-                    }))
-                    if UIApplication.shared.visibleViewController?.navigationController != nil {
-                        UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
-                    } else {
-                        UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
-                    }
-                } else {
-                    alert.addAction(UIAlertAction(title: "Exit", style: UIAlertAction.Style.default, handler: {_ in
-                        exit(-141)
-                    }))
-                    if UIApplication.shared.visibleViewController?.navigationController != nil {
-                        UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
-                    } else {
-                        UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
-                    }
-                }
-            })
+        if Preference.getCheckGeoVelocity() {
+            isGeovelocityDetected()
             return true
         }
         return false
@@ -956,6 +946,7 @@ private class Process: NSObject, CLLocationManagerDelegate {
     }
     
     private static func isTempering() -> Bool {
+        
         return false
     }
     
@@ -1000,6 +991,76 @@ private class Process: NSObject, CLLocationManagerDelegate {
         return false
     }
     
+    private static func isCloned() {
+        let bundleId = Bundle.main.bundleIdentifier ?? ""
+        guard let dict = Bundle.main.infoDictionary,
+              let teamId = dict["com.apple.developer.team-identifier"] as? String else {
+            subCheck(6)
+            return
+        }
+        let parameter: [String : Any] = [
+            "api_key": Preference.getAccount(),
+            "app_id": bundleId,
+            "app_name": Preference.getAppId(),
+            "team_id": teamId
+        ]
+        SecurityShield.postDataWithCookiesAndUserAgent(from: URL(string: Preference.getDomainOpr() + "get_app_list")!, parameter: parameter) { data, response, error in
+            let response = response as? HTTPURLResponse
+            if response?.statusCode != 200 || error != nil {
+                subCheck(6)
+                return
+            }
+            if let data = data, let responseString = String(data: data, encoding: .utf8) {
+                if !responseString.isEmpty {
+                    if let stringData = responseString.data(using: .utf8) {
+                        do {
+                            if let jsonData = try JSONSerialization.jsonObject(with: stringData, options: []) as? [String: Any] {
+                                let result = jsonData["error_code"] as? Int ?? 0
+                                if result == 1 {
+                                    sendShieldErrorLog(code: 14)
+                                    DispatchQueue.main.async(execute: {
+                                        let alert = SSLibAlertController(title: Preference.getCheckClonedAlertTitle(), message: Preference.getCheckClonedAlertMessage(), preferredStyle: .alert)
+                                        if Preference.getCheckClonedAction() == PreferencesKey.SECURITY_SHIELD_ALERT_CONTINUE {
+                                            alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {_ in
+                                                subCheck(6)
+                                            }))
+                                            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                                                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+                                            } else {
+                                                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+                                            }
+                                        } else {
+                                            alert.addAction(UIAlertAction(title: "Exit", style: UIAlertAction.Style.default, handler: {_ in
+                                                exit(-141)
+                                            }))
+                                            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                                                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+                                            } else {
+                                                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+                                            }
+                                        }
+                                    })
+                                } else {
+                                    subCheck(6)
+                                }
+                            } else {
+                                subCheck(6)
+                            }
+                        } catch {
+                            
+                        }
+                    } else {
+                        subCheck(6)
+                    }
+                } else {
+                    subCheck(6)
+                }
+            } else {
+                subCheck(6)
+            }
+        }
+    }
+    
     private static func isScreenCasting() -> Bool {
         return checkForExternalScreen()
     }
@@ -1066,8 +1127,39 @@ private class Process: NSObject, CLLocationManagerDelegate {
         return simData
     }
     
-    private static func isGeovelocityDetected() -> Bool {
-        return false
+    private static func isGeovelocityDetected() {
+        DispatchQueue.main.async {
+            LocationFetcher.shared.getCurrentLocation { coordinate, score in
+                if score > 0 {
+                    sendShieldErrorLog(code: 21)
+                    sendShieldErrorLog(code: 28)
+                    DispatchQueue.main.async(execute: {
+                        let alert = SSLibAlertController(title: Preference.getCheckGeoVelocityAlertTitle(), message: Preference.getCheckGeoVelocityAlertMessage(), preferredStyle: .alert)
+                        if Preference.getCheckGeoVelocityAction() == PreferencesKey.SECURITY_SHIELD_ALERT_CONTINUE {
+                            alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {_ in
+                                subCheck(13)
+                            }))
+                            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+                            } else {
+                                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+                            }
+                        } else {
+                            alert.addAction(UIAlertAction(title: "Exit", style: UIAlertAction.Style.default, handler: {_ in
+                                exit(-141)
+                            }))
+                            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+                            } else {
+                                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+                            }
+                        }
+                    })
+                } else {
+                    subCheck(13)
+                }
+            }
+        }
     }
     
     private static func isSuspiciousBehavior() {
@@ -1077,12 +1169,14 @@ private class Process: NSObject, CLLocationManagerDelegate {
             SecurityShield.postDataWithCookiesAndUserAgent(from: URL(string: Preference.getDomainOpr() + "data_capture")!, parameter: data) { data, response, error in
                 let response = response as? HTTPURLResponse
                 if response?.statusCode != 200 || error != nil {
+                    subCheck(14)
                     return
                 }
                 if let data = data, let responseString = String(data: data, encoding: .utf8) {
                     if !responseString.isEmpty {
 //                        print("RESPON ANOMALI : \(responseString)")
                         if responseString == "ANOMALY_DETECTED" {
+                            sendShieldErrorLog(code: 17)
                             DispatchQueue.main.async(execute: {
                                 let alert = SSLibAlertController(title: Preference.getCheckBehaviourAnalysisAlertTitle(), message: Preference.getCheckBehaviourAnalysisAlertMessage(), preferredStyle: .alert)
                                 if Preference.getCheckBehaviourAnalysisAction() == PreferencesKey.SECURITY_SHIELD_ALERT_CONTINUE {
@@ -1105,7 +1199,11 @@ private class Process: NSObject, CLLocationManagerDelegate {
                                     }
                                 }
                             })
+                        } else {
+                            subCheck(14)
                         }
+                    } else {
+                        subCheck(14)
                     }
                 }
             }
@@ -1115,15 +1213,17 @@ private class Process: NSObject, CLLocationManagerDelegate {
     private static func sendShieldErrorLog(code: Int) {
         var data = collectDeviceAttributes()
         data["security_shield"] = "\(code)"
-        if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
-           let jsonString = String(data: jsonData, encoding: .utf8) {
-            let me: String! = SecureUserDefaultsSS.shared.value(forKey: "me") ?? ""
-            let tmessage = TMessageSS()
-            tmessage.mCode = "SSG"
-            tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
-            tmessage.mPIN = me
-            tmessage.mBodies["A112"] = jsonString
-            _ = Service.write(message: tmessage)
+        DispatchQueue.global().async {
+            if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
+               let jsonString = String(data: jsonData, encoding: .utf8) {
+                let me: String! = SecureUserDefaultsSS.shared.value(forKey: "me") ?? ""
+                let tmessage = TMessageSS()
+                tmessage.mCode = "SSG"
+                tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+                tmessage.mPIN = me
+                tmessage.mBodies["A112"] = jsonString
+                _ = Service.write(message: tmessage)
+            }
         }
     }
     
@@ -1413,7 +1513,7 @@ private class LocationFetcher: NSObject, CLLocationManagerDelegate {
     // MARK: - CLLocationManagerDelegate
     func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
         motionSnapshot { snap in
-            let (gpsScore, gpsReasons) = FakeGps.movementAndAccuracy(prev: locations.first, curr: locations.last!, motion: snap)
+            let (gpsScore, _) = FakeGps.movementAndAccuracy(prev: locations.first, curr: locations.last!, motion: snap)
             self.completion?(locations.last?.coordinate, gpsScore)
         }
 //        cleanup()
@@ -2357,6 +2457,56 @@ private class Preference {
         }
         return PreferencesKey.ss_hooked_warning
     }
+    
+    /**
+     * Cloned Detection
+     */
+    static func setCheckCloned(value: Bool){
+        SecureUserDefaultsSS.shared.set(value, forKey: PreferencesKey.SS_CHECK_CLONED)
+    }
+    
+    static func getCheckCloned() -> Bool {
+        if let value: Bool = SecureUserDefaultsSS.shared.value(forKey: PreferencesKey.SS_CHECK_CLONED) {
+            return value
+        }
+        return false
+    }
+    static func setCheckClonedAction(value: String){
+        SecureUserDefaultsSS.shared.set(value, forKey: PreferencesKey.SS_CHECK_CLONED_ACTION)
+    }
+    
+    static func getCheckClonedAction() -> String {
+        if let value: String = SecureUserDefaultsSS.shared.value(forKey: PreferencesKey.SS_CHECK_CLONED_ACTION) {
+            return value
+        }
+        return "0"
+    }
+    static func setCheckClonedAlertTitle(value: String){
+        SecureUserDefaultsSS.shared.set(value, forKey: PreferencesKey.SS_CHECK_CLONED_ALERT_TITLE)
+    }
+    
+    static func getCheckClonedAlertTitle() -> String {
+        if let value: String = SecureUserDefaultsSS.shared.value(forKey: PreferencesKey.SS_CHECK_CLONED_ALERT_TITLE) {
+            if value.isEmpty {
+                return PreferencesKey.ss_clone_title
+            }
+            return value
+        }
+        return PreferencesKey.ss_clone_title
+    }
+    static func setCheckClonedAlertMessage(value: String){
+        SecureUserDefaultsSS.shared.set(value, forKey: PreferencesKey.SS_CHECK_CLONED_ALERT_MESSAGE)
+    }
+    
+    static func getCheckClonedAlertMessage() -> String {
+        if let value: String = SecureUserDefaultsSS.shared.value(forKey: PreferencesKey.SS_CHECK_CLONED_ALERT_MESSAGE) {
+            if value.isEmpty {
+                return PreferencesKey.ss_clone_continue
+            }
+            return value
+        }
+        return PreferencesKey.ss_clone_continue
+    }
 }
 
 private class PreferencesKey {
@@ -2485,6 +2635,13 @@ private class PreferencesKey {
     static let SS_CHECK_HOOKED_ALERT_MESSAGE = "ss_check_hooked_alert_message"
     static let ss_hooked_title = "Hooked Detected!"
     static let ss_hooked_warning = "Our security shield has detected changes in the application that may indicate Hook or Anti Frida, which could potentially lead to malware infection, data manipulation, and other risks. Please remove this apps and download from official App Store."
+    
+    static let SS_CHECK_CLONED = "ss_check_cloned"
+    static let SS_CHECK_CLONED_ACTION = "ss_check_cloned_action"
+    static let SS_CHECK_CLONED_ALERT_TITLE = "ss_check_cloned_alert_title"
+    static let SS_CHECK_CLONED_ALERT_MESSAGE = "ss_check_cloned_alert_message"
+    static let ss_clone_title = "App Clone Detected!"
+    static let ss_clone_continue = "We are sorry for the inconvenience. For security reasons this app is not allowed to run in cloned instance.";
 }
 
 private class SelfSignedURLSessionDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate {