BNIBookingWebView.swift 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097
  1. //
  2. // BNIBookingWebView.swift
  3. // FMDB
  4. //
  5. // Created by Qindi on 01/04/22.
  6. //
  7. import Foundation
  8. import UIKit
  9. import WebKit
  10. import Speech
  11. import CommonCrypto
  12. import nuSDKService
  13. import NotificationBannerSwift
  14. public class BNIBookingWebView: UIViewController, WKNavigationDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate, WKScriptMessageHandler, SFSpeechRecognizerDelegate, ImageVideoPickerDelegate {
  15. var webView: WKWebView!
  16. let closeButton = UIButton()
  17. public var customUrl = ""
  18. public var isSecureBrowser = false
  19. let textField = UITextField()
  20. var progressView: UIProgressView!
  21. var isAllowSpeech = false
  22. let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "id"))
  23. var recognitionRequest : SFSpeechAudioBufferRecognitionRequest?
  24. var recognitionTask : SFSpeechRecognitionTask?
  25. let audioEngine = AVAudioEngine()
  26. var alertController = LibAlertController()
  27. var indexImageVideoWv = 0
  28. var imageVideoPicker: ImageVideoPicker!
  29. var blockedCertificate = ""
  30. var allowedURLs = Set<String>()
  31. var loadingURL = false
  32. var onDismiss: (() -> Void)?
  33. public override var preferredStatusBarStyle: UIStatusBarStyle {
  34. return .default
  35. }
  36. public override func viewDidDisappear(_ animated: Bool) {
  37. super.viewDidDisappear(animated)
  38. if self.isBeingDismissed || self.isMovingFromParent {
  39. onDismiss?()
  40. }
  41. }
  42. public override func viewDidLoad() {
  43. super.viewDidLoad()
  44. let attributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
  45. let navBarAppearance = UINavigationBarAppearance()
  46. navBarAppearance.configureWithOpaqueBackground()
  47. navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : UIColor.mainColor
  48. navBarAppearance.titleTextAttributes = attributes
  49. navigationController?.navigationBar.standardAppearance = navBarAppearance
  50. navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance
  51. let backButton = UIBarButtonItem(image: UIImage(systemName: "chevron.backward"), style: .plain, target: self, action: #selector(self.didTapExit))
  52. self.navigationItem.leftBarButtonItem = backButton
  53. let configuration = WKWebViewConfiguration()
  54. configuration.allowsInlineMediaPlayback = true
  55. loadContentBlocker(into: configuration) { [self] in
  56. DispatchQueue.main.async {
  57. self.initializeWebView(with: configuration)
  58. }
  59. }
  60. }
  61. @objc func didTapExit(sender: Any) {
  62. self.dismiss(animated: true, completion: nil)
  63. }
  64. func initializeWebView(with configuration: WKWebViewConfiguration) {
  65. 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())"
  66. let finalUserAgent = "\(customUserAgent)"
  67. configuration.applicationNameForUserAgent = finalUserAgent
  68. webView = WKWebView(frame: .zero, configuration: configuration)
  69. let containerView = UIView()
  70. containerView.backgroundColor = .white
  71. if isSecureBrowser {
  72. title = "Secure Browser".localized()
  73. view.addSubview(containerView)
  74. containerView.translatesAutoresizingMaskIntoConstraints = false
  75. containerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
  76. containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
  77. containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
  78. containerView.heightAnchor.constraint(equalToConstant: 44).isActive = true
  79. containerView.addSubview(textField)
  80. textField.translatesAutoresizingMaskIntoConstraints = false
  81. textField.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 10.0).isActive = true
  82. textField.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true
  83. textField.heightAnchor.constraint(equalToConstant: 40).isActive = true
  84. textField.widthAnchor.constraint(equalToConstant: view.bounds.size.width - 80).isActive = true
  85. textField.layer.borderColor = UIColor.lightGray.cgColor
  86. textField.layer.borderWidth = 1.0
  87. textField.layer.cornerRadius = 5.0
  88. textField.clipsToBounds = true
  89. let buttonGo = UIButton(type: .custom)
  90. buttonGo.setTitle("Go".localized(), for: .normal)
  91. buttonGo.setTitleColor(.black, for: .normal)
  92. buttonGo.addTarget(self, action: #selector(goAction), for: .touchUpInside)
  93. containerView.addSubview(buttonGo)
  94. buttonGo.translatesAutoresizingMaskIntoConstraints = false
  95. buttonGo.leftAnchor.constraint(equalTo: textField.rightAnchor, constant: 10.0).isActive = true
  96. buttonGo.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -10.0).isActive = true
  97. buttonGo.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true
  98. buttonGo.heightAnchor.constraint(equalToConstant: 40).isActive = true
  99. }
  100. view.addSubview(webView)
  101. webView.translatesAutoresizingMaskIntoConstraints = false
  102. if isSecureBrowser {
  103. webView.topAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
  104. } else {
  105. webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
  106. }
  107. webView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
  108. webView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
  109. webView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
  110. webView.navigationDelegate = self
  111. webView.allowsBackForwardNavigationGestures = true
  112. webView.scrollView.delegate = self
  113. progressView = UIProgressView(progressViewStyle: .default)
  114. progressView.sizeToFit()
  115. progressView.tintColor = .systemBlue
  116. view.addSubview(progressView)
  117. // Auto Layout for progress bar (stick to top)
  118. progressView.translatesAutoresizingMaskIntoConstraints = false
  119. NSLayoutConstraint.activate([
  120. progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  121. progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  122. progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  123. progressView.heightAnchor.constraint(equalToConstant: 4)
  124. ])
  125. // Observe estimatedProgress
  126. webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
  127. let contentController = webView.configuration.userContentController
  128. contentController.add(self, name: "sendQueueBNI")
  129. contentController.add(self, name: "checkProfile")
  130. contentController.add(self, name: "setIsProductModalOpen")
  131. contentController.add(self, name: "toggleVoiceSearch")
  132. contentController.add(self, name: "blockUser")
  133. contentController.add(self, name: "showAlert")
  134. contentController.add(self, name: "closeProfile")
  135. contentController.add(self, name: "successChangeTheme")
  136. contentController.add(self, name: "finishForm")
  137. contentController.add(self, name: "shareText")
  138. contentController.add(self, name: "openGalleryiOS")
  139. contentController.add(self, name: "openChannel")
  140. contentController.add(self, name: "setFirstTheme")
  141. let source: String = "var meta = document.createElement('meta');" +
  142. "meta.name = 'viewport';" +
  143. "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" +
  144. "var head = document.getElementsByTagName('head')[0];" +
  145. "head.appendChild(meta);" +
  146. "$('#header-layout').find('.col-8').removeClass('col-8').addClass('col');" +
  147. "$('#header-layout').find('.col-4').removeClass('col-4').addClass('col');"
  148. let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
  149. contentController.addUserScript(script)
  150. let cookieScript1 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[0])';"
  151. let cookieScriptInjection1 = WKUserScript(source: cookieScript1, injectionTime: .atDocumentStart, forMainFrameOnly: false)
  152. configuration.userContentController.addUserScript(cookieScriptInjection1)
  153. let cookieScript2 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[1])';"
  154. let cookieScriptInjection2 = WKUserScript(source: cookieScript2, injectionTime: .atDocumentStart, forMainFrameOnly: false)
  155. configuration.userContentController.addUserScript(cookieScriptInjection2)
  156. let refreshControl = UIRefreshControl()
  157. refreshControl.addTarget(self, action: #selector(reloadWebView(_:)), for: .valueChanged)
  158. webView.scrollView.addSubview(refreshControl)
  159. webView.isOpaque = false
  160. webView.backgroundColor = .white
  161. webView.scrollView.backgroundColor = .white
  162. var stringQMS = "https://sqbni.murni.id:4200/bnibookingonline/#/?userid="
  163. if !customUrl.isEmpty {
  164. stringQMS = customUrl
  165. }
  166. if stringQMS.lowercased().contains("?userid=") {
  167. let name = User.getData(pin: User.getMyPin())!.fullName
  168. stringQMS += name
  169. } else if stringQMS.lowercased().contains("?f_pin=") {
  170. stringQMS += User.getMyPin()!
  171. }
  172. if stringQMS.contains("<<f_pin>>") {
  173. stringQMS = stringQMS.replacingOccurrences(of: "<<f_pin>>", with: User.getMyPin()!)
  174. }
  175. let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
  176. var intLang = 0
  177. if lang == "id" {
  178. intLang = 1
  179. }
  180. if stringQMS.contains("?") {
  181. stringQMS = stringQMS + "&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
  182. } else {
  183. stringQMS = stringQMS + "?lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
  184. }
  185. if let url = URL(string: "\(stringQMS)") {
  186. if !isSecureBrowser {
  187. loadURLWithCookie(url: url)
  188. } else {
  189. if let url = URL(string: "https://google.com/") {
  190. loadURLWithCookie(url: url)
  191. }
  192. }
  193. } else if isSecureBrowser {
  194. if let url = URL(string: "https://google.com/") {
  195. loadURLWithCookie(url: url)
  196. }
  197. }
  198. }
  199. public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  200. if keyPath == "estimatedProgress" {
  201. progressView.progress = Float(webView.estimatedProgress)
  202. progressView.isHidden = webView.estimatedProgress >= 1
  203. }
  204. }
  205. func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
  206. // Define ad-blocking rules directly in Swift as a string
  207. let contentRules = #"""
  208. [
  209. {
  210. "trigger": {
  211. "url-filter": "doubleclick.net"
  212. },
  213. "action": {
  214. "type": "block"
  215. }
  216. },
  217. {
  218. "trigger": {
  219. "url-filter": "googlesyndication.com"
  220. },
  221. "action": {
  222. "type": "block"
  223. }
  224. },
  225. {
  226. "trigger": {
  227. "url-filter": "taboola.com"
  228. },
  229. "action": {
  230. "type": "block"
  231. }
  232. },
  233. {
  234. "trigger": {
  235. "url-filter": "outbrain.com"
  236. },
  237. "action": {
  238. "type": "block"
  239. }
  240. }
  241. ]
  242. """#
  243. WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "AdBlocker", encodedContentRuleList: contentRules) { ruleList, error in
  244. if let ruleList = ruleList {
  245. config.userContentController.add(ruleList)
  246. } else {
  247. print("Failed to compile content rule list: \(error?.localizedDescription ?? "Unknown error")")
  248. }
  249. completion()
  250. }
  251. }
  252. @objc func goAction() {
  253. if let text = textField.text, !text.isEmpty {
  254. var urlString = text
  255. if !text.starts(with: "www.") && !text.starts(with: "https://") {
  256. urlString = "https://www.google.com/search?q=\(text)"
  257. }
  258. urlString = urlString.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: " ", with: "+")
  259. if let url = URL(string: urlString) {
  260. loadURLWithCookie(url: url)
  261. }
  262. }
  263. }
  264. func loadURLWithCookie(url: URL) {
  265. var urlRequest = URLRequest(url: url)
  266. 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())"
  267. urlRequest.setValue(customUserAgent, forHTTPHeaderField: "User-Agent")
  268. if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
  269. for cookie in cookies {
  270. webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
  271. }
  272. textField.text = url.absoluteString
  273. webView.load(urlRequest)
  274. }
  275. }
  276. public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
  277. return true
  278. }
  279. public func scrollViewDidScroll(_ scrollView: UIScrollView) {
  280. }
  281. public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
  282. if message.name == "sendQueueBNI" {
  283. guard let dict = message.body as? [String: AnyObject],
  284. let param1 = dict["param1"] as? String else {
  285. return
  286. }
  287. DispatchQueue.global().async {
  288. let _ = Nexilis.writeSync(message: CoreMessage_TMessageBank.queueBNI(service_id: param1), timeout: 30 * 1000)
  289. }
  290. }
  291. if message.name == "checkProfile" {
  292. guard let dict = message.body as? [String: AnyObject],
  293. let param1 = dict["param1"] as? String,
  294. let param2 = dict["param2"] as? String else {
  295. return
  296. }
  297. if Nexilis.checkIsChangePerson() {
  298. if param2 == "like" {
  299. self.webView.evaluateJavaScript("likeProduct('\(param1)',1,true);")
  300. } else if param2 == "comment" {
  301. self.webView.evaluateJavaScript("openComment('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
  302. } else if param2 == "report_user" {
  303. self.webView.evaluateJavaScript("reportUser('\(param1)',true);")
  304. } else if param2 == "report_content" {
  305. self.webView.evaluateJavaScript("reportContent('\(param1.split(separator: "|")[0])','\(param1.split(separator: "|")[1])',true);")
  306. } else if param2 == "block_user" {
  307. self.webView.evaluateJavaScript("blockUser('\(param1)',true);")
  308. } else if param2 == "follow_user" {
  309. self.webView.evaluateJavaScript("followUser('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
  310. } else if param2 == "homepage" || param2 == "gif" {
  311. self.webView.evaluateJavaScript("window.location.href = '\(param1)';")
  312. } else if param2 == "block_content" {
  313. self.webView.evaluateJavaScript("blockContent('\(param1)',true);")
  314. } else {
  315. self.webView.evaluateJavaScript("openNewPost(true);")
  316. }
  317. } else {
  318. self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
  319. }
  320. } else if message.name == "setIsProductModalOpen" {
  321. guard let dict = message.body as? [String: AnyObject],
  322. let param1 = dict["param1"] as? Bool else {
  323. return
  324. }
  325. if param1 {
  326. if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
  327. self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
  328. }
  329. }
  330. } else if message.name == "toggleVoiceSearch" {
  331. if !isAllowSpeech {
  332. setupSpeech()
  333. } else {
  334. runVoice()
  335. }
  336. } else if message.name == "blockUser" {
  337. guard let dict = message.body as? [String: AnyObject],
  338. let param1 = dict["param1"] as? String,
  339. let param2 = dict["param2"] as? Bool else {
  340. return
  341. }
  342. if param2 {
  343. DispatchQueue.global().async {
  344. if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
  345. if response.isOk() {
  346. DispatchQueue.main.async {
  347. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  348. do {
  349. _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
  350. "ex_block" : "1"
  351. ], _where: "f_pin = '\(param1)'")
  352. } catch {
  353. rollback.pointee = true
  354. print("Access database error: \(error.localizedDescription)")
  355. }
  356. })
  357. }
  358. }
  359. }
  360. }
  361. } else {
  362. DispatchQueue.global().async {
  363. if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
  364. if response.isOk() {
  365. DispatchQueue.main.async {
  366. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  367. do {
  368. _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
  369. "ex_block" : "0"
  370. ], _where: "f_pin = '\(param1)'")
  371. } catch {
  372. rollback.pointee = true
  373. print("Access database error: \(error.localizedDescription)")
  374. }
  375. })
  376. }
  377. }
  378. }
  379. }
  380. }
  381. } else if message.name == "showAlert" {
  382. guard let dict = message.body as? [String: AnyObject],
  383. let param1 = dict["param1"] as? String else {
  384. return
  385. }
  386. self.view.makeToast(param1, duration: 3)
  387. } else if message.name == "blockUser" {
  388. guard let dict = message.body as? [String: AnyObject],
  389. let param1 = dict["param1"] as? String,
  390. let param2 = dict["param2"] as? Bool else {
  391. return
  392. }
  393. if param2 {
  394. DispatchQueue.global().async {
  395. if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
  396. if response.isOk() {
  397. DispatchQueue.main.async {
  398. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  399. do {
  400. _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
  401. "ex_block" : "1"
  402. ], _where: "f_pin = '\(param1)'")
  403. } catch {
  404. rollback.pointee = true
  405. print("Access database error: \(error.localizedDescription)")
  406. }
  407. })
  408. }
  409. }
  410. }
  411. }
  412. } else {
  413. DispatchQueue.global().async {
  414. if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
  415. if response.isOk() {
  416. DispatchQueue.main.async {
  417. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  418. do {
  419. _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
  420. "ex_block" : "0"
  421. ], _where: "f_pin = '\(param1)'")
  422. } catch {
  423. rollback.pointee = true
  424. print("Access database error: \(error.localizedDescription)")
  425. }
  426. })
  427. }
  428. }
  429. }
  430. }
  431. }
  432. } else if message.name == "successChangeTheme" {
  433. guard let dict = message.body as? [String: AnyObject],
  434. let param1 = dict["param1"] as? String,
  435. let param2 = dict["param2"] as? Bool else {
  436. return
  437. }
  438. Utils.setMyTheme(value: param1)
  439. Utils.setIsLoadThemeFromOther(value: true)
  440. Utils.resetValueSuperApp()
  441. Utils.setLastTabSelected(value: 0)
  442. if let jsonArray = try! JSONSerialization.jsonObject(with: param1.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {
  443. do {
  444. for json in jsonArray {
  445. if json["KEY"] as! String == "app_builder_url_webview_1" {
  446. Utils.setURLFirstTab(value: json["VALUE"] as! String)
  447. }
  448. if json["KEY"] as! String == "app_builder_url_webview_2" {
  449. Utils.setURLThirdTab(value: json["VALUE"] as! String)
  450. }
  451. if json["KEY"] as! String == "app_builder_url_webview_3" {
  452. Utils.setURLWv3(value: json["VALUE"] as! String)
  453. }
  454. if json["KEY"] as! String == "app_builder_url_webview_4" {
  455. Utils.setURLWv4(value: json["VALUE"] as! String)
  456. }
  457. if json["KEY"] as! String == "app_builder_url_webview_5" {
  458. Utils.setURLWv5(value: json["VALUE"] as! String)
  459. }
  460. if json["KEY"] as! String == "app_builder_url_webview_6" {
  461. Utils.setURLWv6(value: json["VALUE"] as! String)
  462. }
  463. if json["KEY"] as! String == "app_builder_url_status_update" {
  464. Utils.setURLStatusUpdate(value: json["VALUE"] as! String)
  465. }
  466. if json["KEY"] as! String == "app_builder_custom_tab" {
  467. Utils.setCustomTab(cust: json["VALUE"] as! String)
  468. }
  469. if json["KEY"] as! String == "app_builder_icon_dock" {
  470. Utils.setIconDock(value: json["VALUE"] as! String)
  471. }
  472. if json["KEY"] as! String == "app_builder_background" {
  473. Utils.setBackground(value: json["VALUE"] as! String)
  474. }
  475. if json["KEY"] as! String == "app_builder_background_light" {
  476. Utils.setBackgroundLight(value: json["VALUE"] as! String)
  477. }
  478. if json["KEY"] as! String == "app_builder_background_dark" {
  479. Utils.setBackgroundDark(value: json["VALUE"] as! String)
  480. }
  481. if json["KEY"] as! String == "app_builder_background_1" {
  482. Utils.setBackgroundTab1(value: json["VALUE"] as! String)
  483. }
  484. if json["KEY"] as! String == "app_builder_background_2" {
  485. Utils.setBackgroundTab2(value: json["VALUE"] as! String)
  486. }
  487. if json["KEY"] as! String == "app_builder_background_3" {
  488. Utils.setBackgroundTab3(value: json["VALUE"] as! String)
  489. }
  490. if json["KEY"] as! String == "app_builder_background_4" {
  491. Utils.setBackgroundTab4(value: json["VALUE"] as! String)
  492. }
  493. if json["KEY"] as! String == "app_builder_background_5" {
  494. Utils.setBackgroundTab5(value: json["VALUE"] as! String)
  495. }
  496. if json["KEY"] as! String == "app_builder_background_6" {
  497. Utils.setBackgroundTab6(value: json["VALUE"] as! String)
  498. }
  499. if json["KEY"] as! String == "access_model" {
  500. Utils.setCpaasMode(mode: Int(json["VALUE"] as! String) ?? 0)
  501. }
  502. if json["KEY"] as! String == "app_builder_custom_buttons" {
  503. Utils.setCustomButtons(value: json["VALUE"] as! String)
  504. }
  505. if json["KEY"] as! String == "cpaas_icon" {
  506. Utils.setIconDock(value: json["VALUE"] as! String)
  507. }
  508. if json["KEY"] as! String == "tab1_icon" {
  509. Utils.setTab1Icon(value: json["VALUE"] as! String)
  510. }
  511. if json["KEY"] as! String == "tab2_icon" {
  512. Utils.setTab2Icon(value: json["VALUE"] as! String)
  513. }
  514. if json["KEY"] as! String == "tab3_icon" {
  515. Utils.setTab3Icon(value: json["VALUE"] as! String)
  516. }
  517. if json["KEY"] as! String == "tab4_icon" {
  518. Utils.setTab4Icon(value: json["VALUE"] as! String)
  519. }
  520. if json["KEY"] as! String == "tab5_icon" {
  521. Utils.setTab5Icon(value: json["VALUE"] as! String)
  522. }
  523. if json["KEY"] as! String == "tab6_icon" {
  524. Utils.setTab6Icon(value: json["VALUE"] as! String)
  525. }
  526. if json["KEY"] as! String == "app_builder_button_icon" {
  527. Utils.setButtonIcon(value: json["VALUE"] as! String)
  528. }
  529. if json["KEY"] as! String == "reverse_tab_color" {
  530. Utils.setReverseTab(value: json["VALUE"] as! String)
  531. }
  532. if json["KEY"] as! String == "icon_size" {
  533. Utils.setIconDockSize(value: json["VALUE"] as! String)
  534. }
  535. if json["KEY"] as! String == "app_id" {
  536. changeIconApp(appId: json["VALUE"] as! String)
  537. }
  538. }
  539. } catch {
  540. }
  541. }
  542. // Database.shared.database?.inTransaction({ fmdb, rollback in
  543. // do {
  544. // _ = Database.shared.deleteRecord(fmdb: fmdb, table: "GROUPZ", _where: "")
  545. // _ = Database.shared.deleteRecord(fmdb: fmdb, table: "GROUPZ_MEMBER", _where: "")
  546. // _ = Database.shared.deleteRecord(fmdb: fmdb, table: "DISCUSSION_FORUM", _where: "")
  547. // _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: User.getMyPin() ?? ""))
  548. // } catch {
  549. // rollback.pointee = true
  550. // print("Access database error: \(error.localizedDescription)")
  551. // }
  552. // })
  553. let alert = LibAlertController(title: "Successfully changed".localized(), message: "Please open the app again to see the changes".localized(), preferredStyle: .alert)
  554. alert.addAction(UIAlertAction(title: "OK".localized(), style: .default, handler: {(_) in
  555. exit(0)
  556. }))
  557. self.present(alert, animated: true, completion: nil)
  558. } else if message.name == "finishForm" {
  559. if self.webView.canGoBack {
  560. self.webView.goBack()
  561. } else {
  562. self.dismiss(animated: true)
  563. }
  564. } else if message.name == "shareText" {
  565. guard let dict = message.body as? [String: AnyObject],
  566. let param1 = dict["param1"] as? String else {
  567. return
  568. }
  569. if loadingURL {
  570. return
  571. }
  572. let activityViewController = UIActivityViewController(activityItems: [param1], applicationActivities: nil)
  573. self.present(activityViewController, animated: true, completion: nil)
  574. } else if message.name == "openGalleryiOS" {
  575. guard let dict = message.body as? [String: AnyObject],
  576. let param1 = dict["param1"] as? Int else {
  577. return
  578. }
  579. indexImageVideoWv = param1
  580. let alertController = LibAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
  581. if let action = self.actionImageVideo(for: "image", title: "Choose Photo".localized()) {
  582. alertController.addAction(action)
  583. }
  584. if let action = self.actionImageVideo(for: "video", title: "Choose Video".localized()) {
  585. alertController.addAction(action)
  586. }
  587. alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
  588. self.present(alertController, animated: true)
  589. } else if message.name == "setFirstTheme" {
  590. if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
  591. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  592. imageView.tintColor = .white
  593. let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
  594. banner.show()
  595. return
  596. }
  597. Nexilis.showLoader()
  598. DispatchQueue.global().async {
  599. if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.backToSuperApp(), timeout: 30 * 1000) {
  600. DispatchQueue.main.async {
  601. if response.isOk() {
  602. Utils.setMyTheme(value: "")
  603. Utils.setIsLoadThemeFromOther(value: false)
  604. Utils.resetValueSuperApp()
  605. Utils.setValueInitialApp(data: Utils.getPrefTheme())
  606. Utils.setLastTabSelected(value: 0)
  607. Utils.setIsWATheme(value: false)
  608. UIApplication.shared.setAlternateIconName(nil)
  609. Nexilis.hideLoader {
  610. let alert = LibAlertController(title: "Successfully changed".localized(), message: "Please open the app again to see the changes".localized(), preferredStyle: .alert)
  611. alert.addAction(UIAlertAction(title: "OK".localized(), style: .default, handler: {(_) in
  612. exit(0)
  613. }))
  614. self.present(alert, animated: true, completion: nil)
  615. }
  616. } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "10" {
  617. Nexilis.hideLoader(completion: {
  618. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  619. imageView.tintColor = .white
  620. let banner = FloatingNotificationBanner(title: "Failed to back to Company App".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)
  621. banner.show()
  622. })
  623. } else {
  624. Nexilis.hideLoader(completion: {
  625. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  626. imageView.tintColor = .white
  627. let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
  628. banner.show()
  629. })
  630. }
  631. }
  632. } else {
  633. DispatchQueue.main.async {
  634. Nexilis.hideLoader(completion: {
  635. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  636. imageView.tintColor = .white
  637. let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
  638. banner.show()
  639. })
  640. }
  641. }
  642. }
  643. }
  644. }
  645. private func actionImageVideo(for type: String, title: String) -> UIAlertAction? {
  646. return UIAlertAction(title: title, style: .default) { [unowned self] _ in
  647. switch type {
  648. case "image":
  649. imageVideoPicker.present(source: .imageAlbum)
  650. case "video":
  651. imageVideoPicker.present(source: .videoAlbum)
  652. default:
  653. imageVideoPicker.present(source: .imageAlbum)
  654. }
  655. }
  656. }
  657. public func didSelect(imagevideo: Any?) {
  658. if imagevideo != nil {
  659. let imageData = imagevideo! as! [UIImagePickerController.InfoKey : Any]
  660. if (imageData[.mediaType] as! String == "public.image") {
  661. let compressedImage = (imageData[.originalImage] as! UIImage).pngData()!
  662. let base64String = compressedImage.base64EncodedString()
  663. let base64ToWeb = "data:image/jpeg;base64,\(base64String)"
  664. webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
  665. if let error = error {
  666. print("Error executing JavaScript: \(error)")
  667. }
  668. }
  669. } else {
  670. guard var dataVideo = try? Data(contentsOf: imageData[.mediaURL] as! URL) else {
  671. return
  672. }
  673. let sizeOfVideo = Double(dataVideo.count / 1048576)
  674. if (sizeOfVideo > 10.0) {
  675. let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
  676. compressVideo(inputURL: imageData[.mediaURL] as! URL,
  677. outputURL: compressedURL) { exportSession in
  678. guard let session = exportSession else {
  679. return
  680. }
  681. switch session.status {
  682. case .unknown:
  683. break
  684. case .waiting:
  685. break
  686. case .exporting:
  687. break
  688. case .completed:
  689. guard let compressedData = try? Data(contentsOf: compressedURL) else {
  690. return
  691. }
  692. dataVideo = compressedData
  693. case .failed:
  694. break
  695. case .cancelled:
  696. break
  697. @unknown default:
  698. break
  699. }
  700. }
  701. }
  702. let base64String = dataVideo.base64EncodedString()
  703. let base64ToWeb = "data:video/mp4;base64,\(base64String)"
  704. webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
  705. if let error = error {
  706. print("Error executing JavaScript: \(error)")
  707. }
  708. }
  709. }
  710. }
  711. }
  712. func compressVideo(inputURL: URL,
  713. outputURL: URL,
  714. handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
  715. let urlAsset = AVURLAsset(url: inputURL, options: nil)
  716. guard let exportSession = AVAssetExportSession(asset: urlAsset,
  717. presetName: AVAssetExportPresetMediumQuality) else {
  718. handler(nil)
  719. return
  720. }
  721. exportSession.outputURL = outputURL
  722. exportSession.outputFileType = .mp4
  723. exportSession.exportAsynchronously {
  724. handler(exportSession)
  725. }
  726. }
  727. func changeIconApp(appId: String) {
  728. let digisalesKey = "1694457466830"
  729. let nuKey = "1693550500075"
  730. let iknKey = "1693542580518"
  731. let diginetsKey = "1693456149709"
  732. let biKey = "1692873053159"
  733. let nxcookKey = "1692863737543"
  734. let nxsportKey = "1692863037019"
  735. let bpkhKey = "1711023277251"
  736. let disiniKey = "1711024221024"
  737. let gudegKey = "1712052403416"
  738. let kmiKey = "1713407687550"
  739. let waKey = "1744166263877"
  740. if appId == waKey {
  741. Utils.setIsWATheme(value: true)
  742. } else {
  743. Utils.setIsWATheme(value: false)
  744. }
  745. switch appId {
  746. case digisalesKey:
  747. UIApplication.shared.setAlternateIconName("digisales_icon")
  748. case nuKey:
  749. UIApplication.shared.setAlternateIconName("nu_icon")
  750. case iknKey:
  751. UIApplication.shared.setAlternateIconName("ikn_icon")
  752. case diginetsKey:
  753. UIApplication.shared.setAlternateIconName("diginets_icon")
  754. case biKey:
  755. UIApplication.shared.setAlternateIconName("bi_icon")
  756. case nxcookKey:
  757. UIApplication.shared.setAlternateIconName("nxcook_icon")
  758. case nxsportKey:
  759. UIApplication.shared.setAlternateIconName("nxsport_icon")
  760. case bpkhKey:
  761. UIApplication.shared.setAlternateIconName("bpkh_icon")
  762. case disiniKey:
  763. UIApplication.shared.setAlternateIconName("disini_icon")
  764. case gudegKey:
  765. UIApplication.shared.setAlternateIconName("gudeg_icon")
  766. case kmiKey:
  767. UIApplication.shared.setAlternateIconName("kmi_icon")
  768. default:
  769. UIApplication.shared.setAlternateIconName(nil)
  770. }
  771. }
  772. func setupSpeech() {
  773. self.speechRecognizer?.delegate = self
  774. SFSpeechRecognizer.requestAuthorization { (authStatus) in
  775. var isButtonEnabled = false
  776. switch authStatus {
  777. case .authorized:
  778. isButtonEnabled = true
  779. case .denied:
  780. isButtonEnabled = false
  781. //print("User denied access to speech recognition")
  782. case .restricted:
  783. isButtonEnabled = false
  784. //print("Speech recognition restricted on this device")
  785. case .notDetermined:
  786. isButtonEnabled = false
  787. //print("Speech recognition not yet authorized")
  788. @unknown default:
  789. isButtonEnabled = false
  790. }
  791. OperationQueue.main.addOperation() {
  792. self.isAllowSpeech = isButtonEnabled
  793. if isButtonEnabled {
  794. SecureUserDefaults.shared.set(isButtonEnabled, forKey: "allowSpeech")
  795. self.runVoice()
  796. }
  797. }
  798. }
  799. }
  800. func runVoice() {
  801. if !audioEngine.isRunning {
  802. alertController = LibAlertController(title: "Start Recording".localized(), message: "Say something, I'm listening!".localized(), preferredStyle: .alert)
  803. self.present(alertController, animated: true)
  804. self.webView.evaluateJavaScript("toggleVoiceButton(true)")
  805. self.startRecording()
  806. }
  807. }
  808. func startRecording() {
  809. // Clear all previous session data and cancel task
  810. if recognitionTask != nil {
  811. recognitionTask?.cancel()
  812. recognitionTask = nil
  813. }
  814. // Create instance of audio session to record voice
  815. let audioSession = AVAudioSession.sharedInstance()
  816. do {
  817. try audioSession.setCategory(AVAudioSession.Category.record, mode: .default, options: [])
  818. try audioSession.setMode(AVAudioSession.Mode.measurement)
  819. try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
  820. } catch {
  821. //print("audioSession properties weren't set because of an error.")
  822. }
  823. self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
  824. let inputNode = audioEngine.inputNode
  825. guard let recognitionRequest = recognitionRequest else {
  826. fatalError("Unable to create an SFSpeechAudioBufferRecognitionRequest object")
  827. }
  828. recognitionRequest.shouldReportPartialResults = true
  829. self.recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in
  830. var isFinal = false
  831. var text = ""
  832. if result != nil {
  833. text = result?.bestTranscription.formattedString ?? ""
  834. isFinal = (result?.isFinal)!
  835. self.alertController.dismiss(animated: true)
  836. self.audioEngine.stop()
  837. self.recognitionRequest?.endAudio()
  838. } else {
  839. self.alertController.dismiss(animated: true)
  840. }
  841. if error != nil || isFinal {
  842. if error == nil {
  843. self.webView.evaluateJavaScript("toggleVoiceButton(false)")
  844. self.webView.evaluateJavaScript("submitVoiceSearch('\(text)')")
  845. } else {
  846. self.audioEngine.stop()
  847. self.recognitionRequest?.endAudio()
  848. }
  849. inputNode.removeTap(onBus: 0)
  850. self.recognitionRequest = nil
  851. self.recognitionTask = nil
  852. self.isAllowSpeech = true
  853. }
  854. })
  855. let recordingFormat = inputNode.outputFormat(forBus: 0)
  856. inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
  857. self.recognitionRequest?.append(buffer)
  858. }
  859. self.audioEngine.prepare()
  860. do {
  861. try self.audioEngine.start()
  862. } catch {
  863. //print("audioEngine couldn't start because of an error.")
  864. }
  865. }
  866. @objc func reloadWebView(_ sender: UIRefreshControl) {
  867. webView.reload()
  868. sender.endRefreshing()
  869. }
  870. public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
  871. guard let url = navigationAction.request.url else {
  872. // print("return 0")
  873. decisionHandler(.cancel)
  874. return
  875. }
  876. if let scheme = url.scheme?.lowercased(), scheme.hasPrefix("itms") || scheme == "mailto" || scheme == "tel" {
  877. DispatchQueue.main.async {
  878. UIApplication.shared.open(url, options: [:], completionHandler: nil)
  879. }
  880. // print("return 1 \(url.absoluteString)")
  881. decisionHandler(.cancel)
  882. return
  883. }
  884. guard navigationAction.targetFrame?.isMainFrame == true else {
  885. decisionHandler(.allow)
  886. // print("return 2 \(url.absoluteString)")
  887. return
  888. }
  889. if loadingURL {
  890. decisionHandler(.cancel)
  891. // print("return 3 \(url.absoluteString)")
  892. return
  893. }
  894. loadingURL = true
  895. if allowedURLs.contains(url.absoluteString) {
  896. loadingURL = false
  897. decisionHandler(.allow)
  898. // print("return 4 \(url.absoluteString)")
  899. return
  900. }
  901. validateSSLCertificate(url: url) { [weak self] isValid in
  902. guard let self = self else {
  903. // print("return 5 \(url.absoluteString)")
  904. decisionHandler(.cancel)
  905. return
  906. }
  907. DispatchQueue.main.async {
  908. if isValid {
  909. self.allowedURLs.insert(url.absoluteString)
  910. self.loadingURL = false
  911. // print("return 6 \(url.absoluteString)")
  912. decisionHandler(.allow)
  913. } else {
  914. let host = url.host ?? ""
  915. 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()
  916. messageText = messageText.replacingOccurrences(of: "<<domain>>", with: host)
  917. let alert = UIAlertController(title: "Warning Unknown Url!".localized(),
  918. message: messageText,
  919. preferredStyle: .alert)
  920. alert.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
  921. let storedCertificate = Utils.getCertificatePinningWebview()
  922. if let jsonData = storedCertificate.data(using: .utf8),
  923. let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
  924. var certJson = certJson
  925. certJson[host] = self.blockedCertificate
  926. if let jsonData = try? JSONSerialization.data(withJSONObject: certJson, options: []),
  927. let jsonString = String(data: jsonData, encoding: .utf8) {
  928. Utils.setCertificatePinningWebview(value: jsonString)
  929. }
  930. }
  931. self.allowedURLs.insert(url.absoluteString)
  932. self.loadingURL = false
  933. decisionHandler(.allow)
  934. })
  935. alert.addAction(UIAlertAction(title: "No", style: .cancel) { _ in
  936. self.loadingURL = false
  937. decisionHandler(.cancel)
  938. })
  939. if self.presentedViewController == nil {
  940. self.present(alert, animated: true, completion: nil)
  941. } else {
  942. self.loadingURL = false
  943. // print("return 7 \(url.absoluteString)")
  944. decisionHandler(.cancel)
  945. }
  946. }
  947. }
  948. }
  949. }
  950. private func validateSSLCertificate(url: URL, completion: @escaping (Bool) -> Void) {
  951. let session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil)
  952. let request = URLRequest(url: url)
  953. let task = session.dataTask(with: request) { _, response, error in
  954. if let error = error {
  955. completion(false)
  956. return
  957. }
  958. completion(true)
  959. }
  960. task.resume()
  961. }
  962. }
  963. extension BNIBookingWebView: URLSessionDelegate {
  964. public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
  965. guard let serverTrust = challenge.protectionSpace.serverTrust else {
  966. completionHandler(.cancelAuthenticationChallenge, nil)
  967. return
  968. }
  969. if let publicKeyHash = extractPublicKeyHash(from: serverTrust) {
  970. let domain = challenge.protectionSpace.host
  971. let storedCertificate = Utils.getCertificatePinningWebview()
  972. if let jsonData = storedCertificate.data(using: .utf8),
  973. let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
  974. if publicKeyHash == certJson[domain] {
  975. completionHandler(.useCredential, URLCredential(trust: serverTrust))
  976. } else {
  977. blockedCertificate = publicKeyHash
  978. completionHandler(.cancelAuthenticationChallenge, nil)
  979. }
  980. }
  981. } else {
  982. completionHandler(.cancelAuthenticationChallenge, nil)
  983. }
  984. }
  985. func extractPublicKeyHash(from serverTrust: SecTrust) -> String? {
  986. guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return nil }
  987. guard let publicKey = SecCertificateCopyKey(certificate) else { return nil }
  988. var error: Unmanaged<CFError>?
  989. guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
  990. return nil
  991. }
  992. // Compute SHA-256 hash
  993. var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
  994. publicKeyData.withUnsafeBytes {
  995. _ = CC_SHA256($0.baseAddress, CC_LONG(publicKeyData.count), &hash)
  996. }
  997. let hashData = Data(hash)
  998. let base64Hash = hashData.base64EncodedString()
  999. return base64Hash
  1000. }
  1001. }