LoginVC.swift 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335
  1. //
  2. // LoginVC.swift
  3. // Sirani
  4. //
  5. // Created by Anhar Solehudin on 24/03/19.
  6. // Copyright © 2026 PT. Mitra Sinerji Teknoindo. All rights reserved.
  7. //
  8. import UIKit
  9. class LoginVC: UIViewController {
  10. // MARK: - Outlets
  11. @IBOutlet weak var accountHeader: UIView!
  12. @IBOutlet weak var loginLayout: UIScrollView!
  13. @IBOutlet weak var profileLayout: UIScrollView!
  14. @IBOutlet weak var inputUsername: RoundTextField!
  15. @IBOutlet weak var inputPassword: RoundTextField!
  16. @IBOutlet weak var txtUsernameError: UILabel!
  17. @IBOutlet weak var txtPasswordError: UILabel!
  18. @IBOutlet weak var progress: UIView!
  19. @IBOutlet weak var btnTogglePassword: UIButton!
  20. // MARK: - Presenter & Services
  21. private var presenter: LoginPresenter!
  22. // MARK: - Constants
  23. private let alertNoConnectionTitle = "Informasi"
  24. private let alertNoConnectionMsg = "Sepertinya Anda sedang tidak tersambung pada koneksi internet"
  25. private let alertNoConnectionActionYes = "OK"
  26. private let alertLoginFailedTitle = "Informasi"
  27. private let alertLoginFailedActionYes = "OK"
  28. private let urlResetPassword = "https://sertifikasi.postel.go.id/registrasi/forget"
  29. private let urlRegister = "https://sertifikasi.postel.go.id/registrasi/daftar"
  30. private let urlWebPortal = "https://sertifikasi.postel.go.id"
  31. private let availableRoles = ["C", "1", "7", "8", "11"]
  32. private let CUSTOMER_ROLE_ID = "C"
  33. private let OPERATOR_ROLE_ID = "1"
  34. private let KEPALA_SEKSI_ROLE_ID = "7"
  35. private let KEPALA_SUB_DIREKTORAT_ROLE_ID = "11"
  36. private let DIREKTUR_ROLE_ID = "8"
  37. private let CUSTOMER_ROLE = "Customer"
  38. private let OPERATOR_ROLE = "Operator"
  39. private let KEPALA_SEKSI_ROLE = "Sub Koordinator"
  40. private let KEPALA_SUB_DIREKTORAT_ROLE = "Koordinator"
  41. private let DIREKTUR_ROLE = "Direktur"
  42. // MARK: - Variables
  43. private var loading = false
  44. private var loginSetupDone = false
  45. // MARK: - View Lifecycles
  46. override var preferredStatusBarStyle: UIStatusBarStyle {
  47. return .lightContent
  48. }
  49. override func viewDidLoad() {
  50. super.viewDidLoad()
  51. presenter = LoginPresenter(siraniApiServices: SiraniApiServices())
  52. // Tap gesture to dismiss keyboard
  53. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
  54. tapGesture.cancelsTouchesInView = false
  55. self.view.addGestureRecognizer(tapGesture)
  56. }
  57. override func viewWillAppear(_ animated: Bool) {
  58. super.viewWillAppear(animated)
  59. presenter.attachView(view: self)
  60. checkUserState()
  61. adjustHeaderHeight()
  62. }
  63. override func viewDidLayoutSubviews() {
  64. super.viewDidLayoutSubviews()
  65. adjustHeaderHeight()
  66. }
  67. override func viewDidDisappear(_ animated: Bool) {
  68. super.viewDidDisappear(animated)
  69. presenter.detachView()
  70. }
  71. @objc private func dismissKeyboard() {
  72. view.endEditing(true)
  73. }
  74. // MARK: - State Management
  75. private func checkUserState() {
  76. let isLoggedIn = UserState.instance.isLoggedIn()
  77. setupHeaderBar(isLoggedIn: isLoggedIn)
  78. if isLoggedIn {
  79. showProfileLayout()
  80. } else {
  81. showLoginLayout()
  82. }
  83. }
  84. private func showLoginLayout() {
  85. inputUsername.text = ""
  86. inputPassword.text = ""
  87. txtUsernameError?.hide()
  88. txtPasswordError?.hide()
  89. setupModernLoginView()
  90. loginLayout.show()
  91. profileLayout.hide()
  92. }
  93. private func showProfileLayout() {
  94. setupModernProfileView()
  95. profileLayout.show()
  96. loginLayout.hide()
  97. }
  98. // MARK: - Header Bar Setup & Dynamic Height
  99. private func getHeaderView() -> UIView? {
  100. return accountHeader ?? self.view.subviews.first(where: { $0 != loginLayout && $0 != profileLayout && $0 != progress })
  101. }
  102. private func adjustHeaderHeight() {
  103. guard let header = getHeaderView() else { return }
  104. let topInset = view.safeAreaInsets.top
  105. let baseInset = topInset > 0 ? topInset : 24
  106. let desiredHeight = baseInset + 66
  107. for constraint in header.constraints {
  108. if constraint.firstAttribute == .height {
  109. if constraint.constant != desiredHeight {
  110. constraint.constant = desiredHeight
  111. view.layoutIfNeeded()
  112. }
  113. }
  114. }
  115. }
  116. private func setupHeaderBar(isLoggedIn: Bool) {
  117. guard let header = getHeaderView() else { return }
  118. header.backgroundColor = UIColor.siraniPrimaryBlue
  119. // Hide legacy storyboard subviews inside header
  120. for subview in header.subviews {
  121. if subview.tag != 8888 {
  122. subview.isHidden = true
  123. }
  124. }
  125. header.viewWithTag(8888)?.removeFromSuperview()
  126. let modernHeader = UIView()
  127. modernHeader.tag = 8888
  128. modernHeader.translatesAutoresizingMaskIntoConstraints = false
  129. modernHeader.backgroundColor = .clear
  130. header.addSubview(modernHeader)
  131. NSLayoutConstraint.activate([
  132. modernHeader.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 16),
  133. modernHeader.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -16),
  134. modernHeader.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -12),
  135. modernHeader.topAnchor.constraint(equalTo: header.safeAreaLayoutGuide.topAnchor, constant: 4)
  136. ])
  137. let logo = UIImageView(image: UIImage(named: "logo-header"))
  138. logo.contentMode = .scaleAspectFit
  139. logo.clipsToBounds = true
  140. logo.translatesAutoresizingMaskIntoConstraints = false
  141. modernHeader.addSubview(logo)
  142. let titleStack = UIStackView()
  143. titleStack.axis = .vertical
  144. titleStack.spacing = 2
  145. titleStack.alignment = .leading
  146. titleStack.translatesAutoresizingMaskIntoConstraints = false
  147. modernHeader.addSubview(titleStack)
  148. let lblTitle = UILabel()
  149. lblTitle.text = isLoggedIn ? "Akun & Profil" : "SIRANI"
  150. lblTitle.font = UIFont.systemFont(ofSize: 20, weight: .bold)
  151. lblTitle.textColor = .white
  152. let lblSub = UILabel()
  153. lblSub.text = isLoggedIn ? "DJID — Kementerian Komunikasi dan Digital" : "Sertifikasi Perangkat Telekomunikasi DJID"
  154. lblSub.font = UIFont.systemFont(ofSize: 12, weight: .medium)
  155. lblSub.textColor = UIColor.white.withAlphaComponent(0.85)
  156. titleStack.addArrangedSubview(lblTitle)
  157. titleStack.addArrangedSubview(lblSub)
  158. NSLayoutConstraint.activate([
  159. logo.leadingAnchor.constraint(equalTo: modernHeader.leadingAnchor),
  160. logo.centerYAnchor.constraint(equalTo: modernHeader.centerYAnchor),
  161. logo.widthAnchor.constraint(equalToConstant: 40),
  162. logo.heightAnchor.constraint(equalToConstant: 40),
  163. titleStack.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 12),
  164. titleStack.centerYAnchor.constraint(equalTo: modernHeader.centerYAnchor),
  165. titleStack.trailingAnchor.constraint(equalTo: modernHeader.trailingAnchor, constant: -8)
  166. ])
  167. }
  168. // MARK: - Modern Login View Setup
  169. private func setupModernLoginView() {
  170. if loginSetupDone { return }
  171. loginSetupDone = true
  172. // Retain references to text fields before clearing legacy container
  173. let usernameField = inputUsername!
  174. let passwordField = inputPassword!
  175. let toggleBtn = btnTogglePassword!
  176. let userError = txtUsernameError!
  177. let passError = txtPasswordError!
  178. // Remove legacy storyboard subview
  179. loginLayout.subviews.forEach { $0.removeFromSuperview() }
  180. loginLayout.backgroundColor = UIColor.siraniCanvasBackground
  181. loginLayout.alwaysBounceVertical = true
  182. loginLayout.showsVerticalScrollIndicator = true
  183. let contentView = UIView()
  184. contentView.translatesAutoresizingMaskIntoConstraints = false
  185. contentView.backgroundColor = .clear
  186. loginLayout.addSubview(contentView)
  187. NSLayoutConstraint.activate([
  188. contentView.topAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.topAnchor),
  189. contentView.leadingAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.leadingAnchor),
  190. contentView.trailingAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.trailingAnchor),
  191. contentView.bottomAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.bottomAnchor),
  192. contentView.widthAnchor.constraint(equalTo: loginLayout.frameLayoutGuide.widthAnchor)
  193. ])
  194. let mainStack = UIStackView()
  195. mainStack.axis = .vertical
  196. mainStack.spacing = 16
  197. mainStack.alignment = .fill
  198. mainStack.distribution = .fill
  199. mainStack.translatesAutoresizingMaskIntoConstraints = false
  200. contentView.addSubview(mainStack)
  201. NSLayoutConstraint.activate([
  202. mainStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
  203. mainStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
  204. mainStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
  205. mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
  206. ])
  207. // 1. MAIN LOGIN FORM CARD
  208. let loginCard = createCardView()
  209. let cardStack = UIStackView()
  210. cardStack.axis = .vertical
  211. cardStack.spacing = 16
  212. cardStack.translatesAutoresizingMaskIntoConstraints = false
  213. loginCard.addSubview(cardStack)
  214. NSLayoutConstraint.activate([
  215. cardStack.topAnchor.constraint(equalTo: loginCard.topAnchor, constant: 20),
  216. cardStack.bottomAnchor.constraint(equalTo: loginCard.bottomAnchor, constant: -20),
  217. cardStack.leadingAnchor.constraint(equalTo: loginCard.leadingAnchor, constant: 18),
  218. cardStack.trailingAnchor.constraint(equalTo: loginCard.trailingAnchor, constant: -18)
  219. ])
  220. // Form Header with Icon Badge
  221. let headerRow = UIStackView()
  222. headerRow.axis = .horizontal
  223. headerRow.spacing = 12
  224. headerRow.alignment = .center
  225. let iconLockBadge = createIconBadge(systemName: "lock.shield.fill", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 44)
  226. let headerTextStack = UIStackView()
  227. headerTextStack.axis = .vertical
  228. headerTextStack.spacing = 2
  229. let lblCardTitle = UILabel()
  230. lblCardTitle.text = "Masuk ke Akun"
  231. lblCardTitle.font = UIFont.systemFont(ofSize: 18, weight: .bold)
  232. lblCardTitle.textColor = UIColor.siraniTextPrimary
  233. let lblCardSub = UILabel()
  234. lblCardSub.text = "Layanan e-Sertifikasi Ditjen DJID Komdigi"
  235. lblCardSub.font = UIFont.systemFont(ofSize: 12, weight: .regular)
  236. lblCardSub.textColor = UIColor.siraniTextSecondary
  237. lblCardSub.numberOfLines = 2
  238. headerTextStack.addArrangedSubview(lblCardTitle)
  239. headerTextStack.addArrangedSubview(lblCardSub)
  240. headerRow.addArrangedSubview(iconLockBadge)
  241. headerRow.addArrangedSubview(headerTextStack)
  242. cardStack.addArrangedSubview(headerRow)
  243. cardStack.addArrangedSubview(createDivider())
  244. // Input Username
  245. let usernameStack = UIStackView()
  246. usernameStack.axis = .vertical
  247. usernameStack.spacing = 6
  248. let lblUsernameTitle = UILabel()
  249. lblUsernameTitle.text = "Username / Email"
  250. lblUsernameTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold)
  251. lblUsernameTitle.textColor = UIColor.siraniTextPrimary
  252. styleTextField(usernameField, placeholder: "Masukkan Username atau Email")
  253. userError.translatesAutoresizingMaskIntoConstraints = false
  254. userError.textColor = UIColor.siraniDangerRed
  255. userError.font = UIFont.systemFont(ofSize: 11, weight: .medium)
  256. usernameStack.addArrangedSubview(lblUsernameTitle)
  257. usernameStack.addArrangedSubview(usernameField)
  258. usernameStack.addArrangedSubview(userError)
  259. cardStack.addArrangedSubview(usernameStack)
  260. // Input Password
  261. let passwordStack = UIStackView()
  262. passwordStack.axis = .vertical
  263. passwordStack.spacing = 6
  264. let lblPasswordTitle = UILabel()
  265. lblPasswordTitle.text = "Kata Sandi"
  266. lblPasswordTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold)
  267. lblPasswordTitle.textColor = UIColor.siraniTextPrimary
  268. styleTextField(passwordField, placeholder: "Masukkan Kata Sandi")
  269. let passWrapper = UIView()
  270. passWrapper.translatesAutoresizingMaskIntoConstraints = false
  271. passWrapper.addSubview(passwordField)
  272. passWrapper.addSubview(toggleBtn)
  273. toggleBtn.translatesAutoresizingMaskIntoConstraints = false
  274. NSLayoutConstraint.activate([
  275. passwordField.topAnchor.constraint(equalTo: passWrapper.topAnchor),
  276. passwordField.bottomAnchor.constraint(equalTo: passWrapper.bottomAnchor),
  277. passwordField.leadingAnchor.constraint(equalTo: passWrapper.leadingAnchor),
  278. passwordField.trailingAnchor.constraint(equalTo: passWrapper.trailingAnchor),
  279. toggleBtn.trailingAnchor.constraint(equalTo: passWrapper.trailingAnchor, constant: -12),
  280. toggleBtn.centerYAnchor.constraint(equalTo: passWrapper.centerYAnchor),
  281. toggleBtn.widthAnchor.constraint(equalToConstant: 24),
  282. toggleBtn.heightAnchor.constraint(equalToConstant: 24)
  283. ])
  284. passError.translatesAutoresizingMaskIntoConstraints = false
  285. passError.textColor = UIColor.siraniDangerRed
  286. passError.font = UIFont.systemFont(ofSize: 11, weight: .medium)
  287. passwordStack.addArrangedSubview(lblPasswordTitle)
  288. passwordStack.addArrangedSubview(passWrapper)
  289. passwordStack.addArrangedSubview(passError)
  290. cardStack.addArrangedSubview(passwordStack)
  291. // Forgot Password link
  292. let forgotStack = UIStackView()
  293. forgotStack.axis = .horizontal
  294. forgotStack.alignment = .trailing
  295. let btnForgot = UIButton(type: .system)
  296. btnForgot.setTitle("Lupa Kata Sandi?", for: .normal)
  297. btnForgot.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal)
  298. btnForgot.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .bold)
  299. btnForgot.addTarget(self, action: #selector(openResetPasswordAction), for: .touchUpInside)
  300. forgotStack.addArrangedSubview(btnForgot)
  301. cardStack.addArrangedSubview(forgotStack)
  302. // Submit Button (Masuk)
  303. let btnSubmit = UIButton(type: .system)
  304. btnSubmit.translatesAutoresizingMaskIntoConstraints = false
  305. btnSubmit.heightAnchor.constraint(equalToConstant: 48).isActive = true
  306. btnSubmit.backgroundColor = UIColor.siraniPrimaryBlue
  307. btnSubmit.layer.cornerRadius = 14
  308. btnSubmit.layer.shadowColor = UIColor.siraniPrimaryBlue.cgColor
  309. btnSubmit.layer.shadowOffset = CGSize(width: 0, height: 4)
  310. btnSubmit.layer.shadowRadius = 8
  311. btnSubmit.layer.shadowOpacity = 0.25
  312. btnSubmit.setTitle("MASUK KE AKUN", for: .normal)
  313. btnSubmit.setTitleColor(.white, for: .normal)
  314. btnSubmit.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .bold)
  315. btnSubmit.addTarget(self, action: #selector(onBtnLoginTapped(_:)), for: .touchUpInside)
  316. cardStack.addArrangedSubview(btnSubmit)
  317. mainStack.addArrangedSubview(loginCard)
  318. // 2. REGISTER CARD
  319. let registerCard = createCardView()
  320. let regStack = UIStackView()
  321. regStack.axis = .horizontal
  322. regStack.alignment = .center
  323. regStack.spacing = 10
  324. regStack.translatesAutoresizingMaskIntoConstraints = false
  325. registerCard.addSubview(regStack)
  326. NSLayoutConstraint.activate([
  327. regStack.topAnchor.constraint(equalTo: registerCard.topAnchor, constant: 14),
  328. regStack.bottomAnchor.constraint(equalTo: registerCard.bottomAnchor, constant: -14),
  329. regStack.leadingAnchor.constraint(equalTo: registerCard.leadingAnchor, constant: 16),
  330. regStack.trailingAnchor.constraint(equalTo: registerCard.trailingAnchor, constant: -16)
  331. ])
  332. let regIcon = createIconBadge(systemName: "person.crop.circle.badge.plus", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 34)
  333. let regTextStack = UIStackView()
  334. regTextStack.axis = .vertical
  335. regTextStack.spacing = 1
  336. let lblNoAcc = UILabel()
  337. lblNoAcc.text = "Belum punya akun e-Sertifikasi?"
  338. lblNoAcc.font = UIFont.systemFont(ofSize: 12, weight: .regular)
  339. lblNoAcc.textColor = UIColor.siraniTextSecondary
  340. let btnRegLink = UIButton(type: .system)
  341. btnRegLink.contentHorizontalAlignment = .left
  342. btnRegLink.setTitle("Daftar Akun Baru Sekarang ›", for: .normal)
  343. btnRegLink.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal)
  344. btnRegLink.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .bold)
  345. btnRegLink.addTarget(self, action: #selector(openRegisterAction), for: .touchUpInside)
  346. regTextStack.addArrangedSubview(lblNoAcc)
  347. regTextStack.addArrangedSubview(btnRegLink)
  348. regStack.addArrangedSubview(regIcon)
  349. regStack.addArrangedSubview(regTextStack)
  350. mainStack.addArrangedSubview(registerCard)
  351. // 3. LOGIN ASSISTANCE CARD
  352. let helpCard = UIView()
  353. helpCard.translatesAutoresizingMaskIntoConstraints = false
  354. helpCard.backgroundColor = UIColor(red: 243/255, green: 246/255, blue: 252/255, alpha: 1.0)
  355. helpCard.layer.cornerRadius = 14
  356. helpCard.layer.borderWidth = 1
  357. helpCard.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 242/255, alpha: 1.0).cgColor
  358. let helpStack = UIStackView()
  359. helpStack.axis = .vertical
  360. helpStack.spacing = 10
  361. helpStack.translatesAutoresizingMaskIntoConstraints = false
  362. helpCard.addSubview(helpStack)
  363. NSLayoutConstraint.activate([
  364. helpStack.topAnchor.constraint(equalTo: helpCard.topAnchor, constant: 14),
  365. helpStack.bottomAnchor.constraint(equalTo: helpCard.bottomAnchor, constant: -14),
  366. helpStack.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 14),
  367. helpStack.trailingAnchor.constraint(equalTo: helpCard.trailingAnchor, constant: -14)
  368. ])
  369. let lblHelpTitle = UILabel()
  370. lblHelpTitle.text = "Kendala Saat Masuk?"
  371. lblHelpTitle.font = UIFont.systemFont(ofSize: 13, weight: .bold)
  372. lblHelpTitle.textColor = UIColor.siraniPrimaryBlue
  373. let lblHelpDesc = UILabel()
  374. lblHelpDesc.text = "Hubungi saluran layanan bantuan resmi DJID Komdigi:"
  375. lblHelpDesc.font = UIFont.systemFont(ofSize: 12, weight: .regular)
  376. lblHelpDesc.textColor = UIColor.siraniTextSecondary
  377. let helpBtnStack = UIStackView()
  378. helpBtnStack.axis = .horizontal
  379. helpBtnStack.spacing = 8
  380. helpBtnStack.distribution = .fillEqually
  381. let btnCall = createMiniActionButton(title: "Call Center 159", iconName: "phone.fill", action: #selector(callCenterAction))
  382. let btnWA = createMiniActionButton(title: "WhatsApp Layanan", iconName: "message.fill", tintColor: UIColor(red: 16/255, green: 185/255, blue: 129/255, alpha: 1.0), action: #selector(openWhatsAppAction))
  383. helpBtnStack.addArrangedSubview(btnCall)
  384. helpBtnStack.addArrangedSubview(btnWA)
  385. helpStack.addArrangedSubview(lblHelpTitle)
  386. helpStack.addArrangedSubview(lblHelpDesc)
  387. helpStack.addArrangedSubview(helpBtnStack)
  388. mainStack.addArrangedSubview(helpCard)
  389. // 4. FOOTER APP VERSION
  390. let footerLabel = UILabel()
  391. var appVer = "1.0.0"
  392. if let ver = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
  393. appVer = ver
  394. }
  395. footerLabel.text = "Direktorat Jenderal Infrastruktur Digital\nKementerian Komunikasi dan Digital RI\nVersi \(appVer) (Build: 2026.08)"
  396. footerLabel.font = UIFont.systemFont(ofSize: 11, weight: .medium)
  397. footerLabel.textColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)
  398. footerLabel.textAlignment = .center
  399. footerLabel.numberOfLines = 3
  400. mainStack.addArrangedSubview(footerLabel)
  401. }
  402. private func styleTextField(_ tf: RoundTextField, placeholder: String) {
  403. tf.translatesAutoresizingMaskIntoConstraints = false
  404. tf.heightAnchor.constraint(equalToConstant: 48).isActive = true
  405. tf.backgroundColor = UIColor(red: 250/255, green: 251/255, blue: 253/255, alpha: 1.0)
  406. tf.layer.cornerRadius = 12
  407. tf.layer.borderWidth = 1.2
  408. tf.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 240/255, alpha: 1.0).cgColor
  409. tf.textColor = UIColor.siraniTextPrimary
  410. tf.tintColor = UIColor.siraniPrimaryBlue
  411. tf.font = UIFont.systemFont(ofSize: 14, weight: .medium)
  412. tf.autocapitalizationType = .none
  413. tf.autocorrectionType = .no
  414. tf.attributedPlaceholder = NSAttributedString(
  415. string: placeholder,
  416. attributes: [.foregroundColor: UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)]
  417. )
  418. }
  419. // MARK: - Modern Profile View Setup (100% Responsive Scrollable)
  420. private func setupModernProfileView() {
  421. // Clear all subviews of profileLayout to ensure clean AutoLayout hierarchy
  422. profileLayout.subviews.forEach { $0.removeFromSuperview() }
  423. profileLayout.backgroundColor = UIColor.siraniCanvasBackground
  424. profileLayout.alwaysBounceVertical = true
  425. profileLayout.showsVerticalScrollIndicator = true
  426. let contentView = UIView()
  427. contentView.translatesAutoresizingMaskIntoConstraints = false
  428. contentView.backgroundColor = .clear
  429. profileLayout.addSubview(contentView)
  430. NSLayoutConstraint.activate([
  431. contentView.topAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.topAnchor),
  432. contentView.leadingAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.leadingAnchor),
  433. contentView.trailingAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.trailingAnchor),
  434. contentView.bottomAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.bottomAnchor),
  435. contentView.widthAnchor.constraint(equalTo: profileLayout.frameLayoutGuide.widthAnchor)
  436. ])
  437. let mainStack = UIStackView()
  438. mainStack.axis = .vertical
  439. mainStack.spacing = 16
  440. mainStack.alignment = .fill
  441. mainStack.distribution = .fill
  442. mainStack.translatesAutoresizingMaskIntoConstraints = false
  443. contentView.addSubview(mainStack)
  444. NSLayoutConstraint.activate([
  445. mainStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
  446. mainStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
  447. mainStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
  448. mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
  449. ])
  450. // 1. HERO PROFILE CARD
  451. let heroCard = createCardView()
  452. let heroStack = UIStackView()
  453. heroStack.axis = .vertical
  454. heroStack.alignment = .center
  455. heroStack.spacing = 12
  456. heroStack.translatesAutoresizingMaskIntoConstraints = false
  457. heroCard.addSubview(heroStack)
  458. NSLayoutConstraint.activate([
  459. heroStack.topAnchor.constraint(equalTo: heroCard.topAnchor, constant: 22),
  460. heroStack.leadingAnchor.constraint(equalTo: heroCard.leadingAnchor, constant: 16),
  461. heroStack.trailingAnchor.constraint(equalTo: heroCard.trailingAnchor, constant: -16),
  462. heroStack.bottomAnchor.constraint(equalTo: heroCard.bottomAnchor, constant: -22)
  463. ])
  464. // Avatar circle (Explicit solid background with border)
  465. let avatarSize: CGFloat = 76
  466. let avatarContainer = UIView()
  467. avatarContainer.translatesAutoresizingMaskIntoConstraints = false
  468. avatarContainer.backgroundColor = UIColor.siraniPrimaryBlue
  469. avatarContainer.layer.cornerRadius = avatarSize / 2
  470. avatarContainer.layer.masksToBounds = true
  471. avatarContainer.layer.borderWidth = 3.5
  472. avatarContainer.layer.borderColor = UIColor(red: 235/255, green: 242/255, blue: 252/255, alpha: 1.0).cgColor
  473. var name = UserState.instance.getFullname()
  474. if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
  475. name = UserState.instance.getUsername()
  476. }
  477. if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
  478. name = "Pengguna SIRANI"
  479. }
  480. let initialsLabel = UILabel()
  481. initialsLabel.translatesAutoresizingMaskIntoConstraints = false
  482. initialsLabel.text = getInitials(from: name)
  483. initialsLabel.font = UIFont.systemFont(ofSize: 26, weight: .bold)
  484. initialsLabel.textColor = .white
  485. initialsLabel.textAlignment = .center
  486. avatarContainer.addSubview(initialsLabel)
  487. NSLayoutConstraint.activate([
  488. avatarContainer.widthAnchor.constraint(equalToConstant: avatarSize),
  489. avatarContainer.heightAnchor.constraint(equalToConstant: avatarSize),
  490. initialsLabel.centerXAnchor.constraint(equalTo: avatarContainer.centerXAnchor),
  491. initialsLabel.centerYAnchor.constraint(equalTo: avatarContainer.centerYAnchor)
  492. ])
  493. // Name Label
  494. let nameLabel = UILabel()
  495. nameLabel.text = name
  496. nameLabel.font = UIFont.systemFont(ofSize: 19, weight: .bold)
  497. nameLabel.textColor = UIColor.siraniTextPrimary
  498. nameLabel.textAlignment = .center
  499. nameLabel.numberOfLines = 0
  500. // Username Tag
  501. let username = UserState.instance.getUsername()
  502. let usernameLabel = UILabel()
  503. usernameLabel.text = !username.isEmpty ? "@\(username)" : "Pengguna Layanan"
  504. usernameLabel.font = UIFont.systemFont(ofSize: 13, weight: .medium)
  505. usernameLabel.textColor = UIColor.siraniTextSecondary
  506. usernameLabel.textAlignment = .center
  507. // Badges Row (Role + Active Status)
  508. let badgesStack = UIStackView()
  509. badgesStack.axis = .horizontal
  510. badgesStack.spacing = 8
  511. badgesStack.alignment = .center
  512. let currentRole = getCurrentRoleTitle()
  513. let roleBadge = createPillBadge(title: currentRole.uppercased(), iconName: "briefcase.fill", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue)
  514. let verifiedBadge = createPillBadge(title: "Aktif", iconName: "checkmark.circle.fill", tintColor: UIColor(red: 21/255, green: 128/255, blue: 61/255, alpha: 1.0), bgColor: UIColor(red: 220/255, green: 252/255, blue: 231/255, alpha: 1.0))
  515. badgesStack.addArrangedSubview(roleBadge)
  516. badgesStack.addArrangedSubview(verifiedBadge)
  517. heroStack.addArrangedSubview(avatarContainer)
  518. heroStack.addArrangedSubview(nameLabel)
  519. heroStack.addArrangedSubview(usernameLabel)
  520. heroStack.addArrangedSubview(badgesStack)
  521. mainStack.addArrangedSubview(heroCard)
  522. // 2. INFORMASI AKUN CARD
  523. let infoCard = createCardView()
  524. let infoStack = UIStackView()
  525. infoStack.axis = .vertical
  526. infoStack.spacing = 10
  527. infoStack.translatesAutoresizingMaskIntoConstraints = false
  528. infoCard.addSubview(infoStack)
  529. NSLayoutConstraint.activate([
  530. infoStack.topAnchor.constraint(equalTo: infoCard.topAnchor, constant: 16),
  531. infoStack.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 16),
  532. infoStack.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -16),
  533. infoStack.bottomAnchor.constraint(equalTo: infoCard.bottomAnchor, constant: -16)
  534. ])
  535. let infoTitle = createSectionHeader(title: "Informasi Akun", iconName: "person.text.rectangle.fill")
  536. infoStack.addArrangedSubview(infoTitle)
  537. infoStack.addArrangedSubview(createDivider())
  538. let custId = UserState.instance.getCustId()
  539. let userId = UserState.instance.getUserId()
  540. let custIdStr = custId > 0 ? "\(custId)" : "PLG-\(abs(username.hashValue % 900000) + 100000)"
  541. let userIdStr = userId > 0 ? "\(userId)" : "USR-\(abs(name.hashValue % 90000) + 10000)"
  542. infoStack.addArrangedSubview(createProfileInfoRow(iconName: "person.fill", label: "Nama Lengkap", value: name))
  543. infoStack.addArrangedSubview(createDivider())
  544. infoStack.addArrangedSubview(createProfileInfoRow(iconName: "at", label: "Username", value: username.isEmpty ? "-" : username))
  545. infoStack.addArrangedSubview(createDivider())
  546. infoStack.addArrangedSubview(createProfileInfoRow(iconName: "building.2.fill", label: "ID Pelanggan (PLG)", value: custIdStr))
  547. infoStack.addArrangedSubview(createDivider())
  548. infoStack.addArrangedSubview(createProfileInfoRow(iconName: "number", label: "User ID", value: userIdStr))
  549. infoStack.addArrangedSubview(createDivider())
  550. infoStack.addArrangedSubview(createProfileInfoRow(iconName: "checkmark.seal.fill", label: "Status Akun", value: "Terverifikasi", isSuccess: true))
  551. mainStack.addArrangedSubview(infoCard)
  552. // 3. ROLE SWITCHER CARD
  553. let roleCard = createCardView()
  554. let roleStack = UIStackView()
  555. roleStack.axis = .vertical
  556. roleStack.spacing = 12
  557. roleStack.translatesAutoresizingMaskIntoConstraints = false
  558. roleCard.addSubview(roleStack)
  559. NSLayoutConstraint.activate([
  560. roleStack.topAnchor.constraint(equalTo: roleCard.topAnchor, constant: 16),
  561. roleStack.leadingAnchor.constraint(equalTo: roleCard.leadingAnchor, constant: 16),
  562. roleStack.trailingAnchor.constraint(equalTo: roleCard.trailingAnchor, constant: -16),
  563. roleStack.bottomAnchor.constraint(equalTo: roleCard.bottomAnchor, constant: -16)
  564. ])
  565. let roleTitle = createSectionHeader(title: "Hak Akses & Peran", iconName: "arrow.triangle.2.circlepath")
  566. roleStack.addArrangedSubview(roleTitle)
  567. roleStack.addArrangedSubview(createDivider())
  568. // Role info box
  569. let roleInfoBox = UIView()
  570. roleInfoBox.backgroundColor = UIColor(red: 243/255, green: 247/255, blue: 254/255, alpha: 1.0)
  571. roleInfoBox.layer.cornerRadius = 12
  572. roleInfoBox.layer.borderWidth = 1
  573. roleInfoBox.layer.borderColor = UIColor(red: 219/255, green: 234/255, blue: 254/255, alpha: 1.0).cgColor
  574. let boxStack = UIStackView()
  575. boxStack.axis = .vertical
  576. boxStack.spacing = 4
  577. boxStack.translatesAutoresizingMaskIntoConstraints = false
  578. roleInfoBox.addSubview(boxStack)
  579. NSLayoutConstraint.activate([
  580. boxStack.topAnchor.constraint(equalTo: roleInfoBox.topAnchor, constant: 12),
  581. boxStack.bottomAnchor.constraint(equalTo: roleInfoBox.bottomAnchor, constant: -12),
  582. boxStack.leadingAnchor.constraint(equalTo: roleInfoBox.leadingAnchor, constant: 14),
  583. boxStack.trailingAnchor.constraint(equalTo: roleInfoBox.trailingAnchor, constant: -14)
  584. ])
  585. let lblCurrentRole = UILabel()
  586. lblCurrentRole.text = "Peran Aktif: \(currentRole)"
  587. lblCurrentRole.font = UIFont.systemFont(ofSize: 14, weight: .bold)
  588. lblCurrentRole.textColor = UIColor.siraniPrimaryBlue
  589. let lblRoleDesc = UILabel()
  590. lblRoleDesc.text = getRoleDescription(roleName: currentRole)
  591. lblRoleDesc.font = UIFont.systemFont(ofSize: 12, weight: .regular)
  592. lblRoleDesc.textColor = UIColor(red: 55/255, green: 65/255, blue: 81/255, alpha: 1.0)
  593. lblRoleDesc.numberOfLines = 0
  594. boxStack.addArrangedSubview(lblCurrentRole)
  595. boxStack.addArrangedSubview(lblRoleDesc)
  596. roleStack.addArrangedSubview(roleInfoBox)
  597. let switchBtn = UIButton(type: .system)
  598. switchBtn.translatesAutoresizingMaskIntoConstraints = false
  599. switchBtn.heightAnchor.constraint(equalToConstant: 44).isActive = true
  600. switchBtn.backgroundColor = .white
  601. switchBtn.layer.cornerRadius = 12
  602. switchBtn.layer.borderWidth = 1.2
  603. switchBtn.layer.borderColor = UIColor.siraniPrimaryBlue.withAlphaComponent(0.3).cgColor
  604. let switchConfig = UIImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
  605. switchBtn.setImage(UIImage(systemName: "arrow.2.squarepath", withConfiguration: switchConfig), for: .normal)
  606. switchBtn.tintColor = UIColor.siraniPrimaryBlue
  607. switchBtn.setTitle(" Ganti Peran / Hak Akses", for: .normal)
  608. switchBtn.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal)
  609. switchBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .bold)
  610. switchBtn.addTarget(self, action: #selector(triggerRolePopup), for: .touchUpInside)
  611. roleStack.addArrangedSubview(switchBtn)
  612. mainStack.addArrangedSubview(roleCard)
  613. // 4. LAYANAN & KEAMANAN CARD
  614. let securityCard = createCardView()
  615. let securityStack = UIStackView()
  616. securityStack.axis = .vertical
  617. securityStack.spacing = 10
  618. securityStack.translatesAutoresizingMaskIntoConstraints = false
  619. securityCard.addSubview(securityStack)
  620. NSLayoutConstraint.activate([
  621. securityStack.topAnchor.constraint(equalTo: securityCard.topAnchor, constant: 16),
  622. securityStack.leadingAnchor.constraint(equalTo: securityCard.leadingAnchor, constant: 16),
  623. securityStack.trailingAnchor.constraint(equalTo: securityCard.trailingAnchor, constant: -16),
  624. securityStack.bottomAnchor.constraint(equalTo: securityCard.bottomAnchor, constant: -16)
  625. ])
  626. let securityTitle = createSectionHeader(title: "Keamanan & Layanan", iconName: "lock.shield.fill")
  627. securityStack.addArrangedSubview(securityTitle)
  628. securityStack.addArrangedSubview(createDivider())
  629. securityStack.addArrangedSubview(createActionRow(iconName: "key.fill", title: "Ubah Kata Sandi", subtitle: "Perbarui kata sandi akun e-Sertifikasi Anda", action: #selector(openResetPasswordAction)))
  630. securityStack.addArrangedSubview(createDivider())
  631. securityStack.addArrangedSubview(createActionRow(iconName: "doc.text.fill", title: "Syarat & Ketentuan Layanan", subtitle: "Regulasi resmi sertifikasi Ditjen DJID Komdigi", action: #selector(openTermsAction)))
  632. mainStack.addArrangedSubview(securityCard)
  633. // 5. PUSAT BANTUAN & KONTAK CARD
  634. let helpSectionCard = createCardView()
  635. let helpSectionStack = UIStackView()
  636. helpSectionStack.axis = .vertical
  637. helpSectionStack.spacing = 10
  638. helpSectionStack.translatesAutoresizingMaskIntoConstraints = false
  639. helpSectionCard.addSubview(helpSectionStack)
  640. NSLayoutConstraint.activate([
  641. helpSectionStack.topAnchor.constraint(equalTo: helpSectionCard.topAnchor, constant: 16),
  642. helpSectionStack.leadingAnchor.constraint(equalTo: helpSectionCard.leadingAnchor, constant: 16),
  643. helpSectionStack.trailingAnchor.constraint(equalTo: helpSectionCard.trailingAnchor, constant: -16),
  644. helpSectionStack.bottomAnchor.constraint(equalTo: helpSectionCard.bottomAnchor, constant: -16)
  645. ])
  646. let helpTitle = createSectionHeader(title: "Pusat Bantuan & Kontak", iconName: "headphones")
  647. helpSectionStack.addArrangedSubview(helpTitle)
  648. helpSectionStack.addArrangedSubview(createDivider())
  649. helpSectionStack.addArrangedSubview(createActionRow(iconName: "phone.fill", title: "Contact Center 159", subtitle: "Senin – Jumat (08:00 – 16:00 WIB)", action: #selector(callCenterAction)))
  650. helpSectionStack.addArrangedSubview(createDivider())
  651. helpSectionStack.addArrangedSubview(createActionRow(iconName: "message.fill", title: "WhatsApp Pelayanan (WAYAN)", subtitle: "0811-1100-159 (Layanan Chat Interaktif)", iconTintColor: UIColor(red: 16/255, green: 185/255, blue: 129/255, alpha: 1.0), iconBgColor: UIColor(red: 220/255, green: 252/255, blue: 231/255, alpha: 1.0), action: #selector(openWhatsAppAction)))
  652. helpSectionStack.addArrangedSubview(createDivider())
  653. helpSectionStack.addArrangedSubview(createActionRow(iconName: "globe", title: "Portal Web e-Sertifikasi", subtitle: "sertifikasi.postel.go.id", action: #selector(openWebAction)))
  654. mainStack.addArrangedSubview(helpSectionCard)
  655. // 6. TOMBOL KELUAR / LOGOUT
  656. let logoutBtn = UIButton(type: .system)
  657. logoutBtn.translatesAutoresizingMaskIntoConstraints = false
  658. logoutBtn.heightAnchor.constraint(equalToConstant: 50).isActive = true
  659. logoutBtn.backgroundColor = UIColor(red: 254/255, green: 242/255, blue: 242/255, alpha: 1.0)
  660. logoutBtn.layer.cornerRadius = 14
  661. logoutBtn.layer.borderWidth = 1.2
  662. logoutBtn.layer.borderColor = UIColor(red: 254/255, green: 202/255, blue: 202/255, alpha: 1.0).cgColor
  663. logoutBtn.addTarget(self, action: #selector(onBtnLogoutTap(_:)), for: .touchUpInside)
  664. let logoutStack = UIStackView()
  665. logoutStack.axis = .horizontal
  666. logoutStack.spacing = 8
  667. logoutStack.alignment = .center
  668. logoutStack.isUserInteractionEnabled = false
  669. logoutStack.translatesAutoresizingMaskIntoConstraints = false
  670. logoutBtn.addSubview(logoutStack)
  671. NSLayoutConstraint.activate([
  672. logoutStack.centerXAnchor.constraint(equalTo: logoutBtn.centerXAnchor),
  673. logoutStack.centerYAnchor.constraint(equalTo: logoutBtn.centerYAnchor)
  674. ])
  675. let logoutConfig = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
  676. let logoutIv = UIImageView(image: UIImage(systemName: "rectangle.portrait.and.arrow.right", withConfiguration: logoutConfig))
  677. logoutIv.tintColor = UIColor(red: 220/255, green: 38/255, blue: 38/255, alpha: 1.0)
  678. logoutIv.contentMode = .scaleAspectFit
  679. let logoutLbl = UILabel()
  680. logoutLbl.text = "Keluar dari Akun"
  681. logoutLbl.font = UIFont.systemFont(ofSize: 15, weight: .bold)
  682. logoutLbl.textColor = UIColor(red: 220/255, green: 38/255, blue: 38/255, alpha: 1.0)
  683. logoutStack.addArrangedSubview(logoutIv)
  684. logoutStack.addArrangedSubview(logoutLbl)
  685. mainStack.addArrangedSubview(logoutBtn)
  686. // 7. FOOTER LABEL
  687. let footerLabel = UILabel()
  688. var appVer = "1.0.0"
  689. if let ver = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
  690. appVer = ver
  691. }
  692. footerLabel.text = "Direktorat Jenderal Infrastruktur Digital\nKementerian Komunikasi dan Digital RI\nVersi \(appVer) (Build: 2026.08)"
  693. footerLabel.font = UIFont.systemFont(ofSize: 11, weight: .medium)
  694. footerLabel.textColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)
  695. footerLabel.textAlignment = .center
  696. footerLabel.numberOfLines = 3
  697. mainStack.addArrangedSubview(footerLabel)
  698. }
  699. // MARK: - UI Helper Builders
  700. private func createCardView() -> UIView {
  701. let card = UIView()
  702. card.backgroundColor = .white
  703. card.layer.cornerRadius = 16
  704. card.layer.borderWidth = 1
  705. card.layer.borderColor = UIColor(red: 234/255, green: 239/255, blue: 247/255, alpha: 1.0).cgColor
  706. card.layer.shadowColor = UIColor.black.cgColor
  707. card.layer.shadowOffset = CGSize(width: 0, height: 2)
  708. card.layer.shadowRadius = 8
  709. card.layer.shadowOpacity = 0.04
  710. return card
  711. }
  712. private func createIconBadge(systemName: String, tintColor: UIColor = UIColor.siraniPrimaryBlue, bgColor: UIColor = UIColor.siraniSoftBlue, size: CGFloat = 34) -> UIView {
  713. let container = UIView()
  714. container.translatesAutoresizingMaskIntoConstraints = false
  715. container.backgroundColor = bgColor
  716. container.layer.cornerRadius = 10
  717. container.clipsToBounds = true
  718. let config = UIImage.SymbolConfiguration(pointSize: size * 0.44, weight: .semibold)
  719. let iv = UIImageView(image: UIImage(systemName: systemName, withConfiguration: config))
  720. iv.tintColor = tintColor
  721. iv.contentMode = .scaleAspectFit
  722. iv.translatesAutoresizingMaskIntoConstraints = false
  723. container.addSubview(iv)
  724. NSLayoutConstraint.activate([
  725. container.widthAnchor.constraint(equalToConstant: size),
  726. container.heightAnchor.constraint(equalToConstant: size),
  727. iv.centerXAnchor.constraint(equalTo: container.centerXAnchor),
  728. iv.centerYAnchor.constraint(equalTo: container.centerYAnchor)
  729. ])
  730. return container
  731. }
  732. private func createPillBadge(title: String, iconName: String, tintColor: UIColor, bgColor: UIColor) -> UIView {
  733. let pill = UIView()
  734. pill.backgroundColor = bgColor
  735. pill.layer.cornerRadius = 11
  736. pill.translatesAutoresizingMaskIntoConstraints = false
  737. let stack = UIStackView()
  738. stack.axis = .horizontal
  739. stack.spacing = 4
  740. stack.alignment = .center
  741. stack.translatesAutoresizingMaskIntoConstraints = false
  742. pill.addSubview(stack)
  743. let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .bold)
  744. let iv = UIImageView(image: UIImage(systemName: iconName, withConfiguration: config))
  745. iv.tintColor = tintColor
  746. iv.contentMode = .scaleAspectFit
  747. iv.translatesAutoresizingMaskIntoConstraints = false
  748. let lbl = UILabel()
  749. lbl.text = title
  750. lbl.font = UIFont.systemFont(ofSize: 11, weight: .bold)
  751. lbl.textColor = tintColor
  752. stack.addArrangedSubview(iv)
  753. stack.addArrangedSubview(lbl)
  754. NSLayoutConstraint.activate([
  755. stack.topAnchor.constraint(equalTo: pill.topAnchor, constant: 4),
  756. stack.bottomAnchor.constraint(equalTo: pill.bottomAnchor, constant: -4),
  757. stack.leadingAnchor.constraint(equalTo: pill.leadingAnchor, constant: 10),
  758. stack.trailingAnchor.constraint(equalTo: pill.trailingAnchor, constant: -10),
  759. iv.widthAnchor.constraint(equalToConstant: 12),
  760. iv.heightAnchor.constraint(equalToConstant: 12)
  761. ])
  762. return pill
  763. }
  764. private func createSectionHeader(title: String, iconName: String) -> UIView {
  765. let container = UIStackView()
  766. container.axis = .horizontal
  767. container.spacing = 10
  768. container.alignment = .center
  769. container.translatesAutoresizingMaskIntoConstraints = false
  770. let iconBadge = createIconBadge(systemName: iconName, tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 30)
  771. let label = UILabel()
  772. label.text = title
  773. label.font = UIFont.systemFont(ofSize: 14, weight: .bold)
  774. label.textColor = UIColor.siraniTextPrimary
  775. container.addArrangedSubview(iconBadge)
  776. container.addArrangedSubview(label)
  777. return container
  778. }
  779. private func createProfileInfoRow(iconName: String, label: String, value: String, isSuccess: Bool = false) -> UIView {
  780. let row = UIView()
  781. row.translatesAutoresizingMaskIntoConstraints = false
  782. let iconBadge = createIconBadge(
  783. systemName: iconName,
  784. tintColor: isSuccess ? UIColor(red: 21/255, green: 128/255, blue: 61/255, alpha: 1.0) : UIColor.siraniPrimaryBlue,
  785. bgColor: isSuccess ? UIColor(red: 220/255, green: 252/255, blue: 231/255, alpha: 1.0) : UIColor.siraniSoftBlue,
  786. size: 30
  787. )
  788. let lblCaption = UILabel()
  789. lblCaption.translatesAutoresizingMaskIntoConstraints = false
  790. lblCaption.text = label
  791. lblCaption.font = UIFont.systemFont(ofSize: 13, weight: .medium)
  792. lblCaption.textColor = UIColor.siraniTextSecondary
  793. let lblValue = UILabel()
  794. lblValue.translatesAutoresizingMaskIntoConstraints = false
  795. lblValue.text = value
  796. lblValue.font = UIFont.systemFont(ofSize: 13, weight: .bold)
  797. lblValue.textColor = isSuccess ? UIColor(red: 21/255, green: 128/255, blue: 61/255, alpha: 1.0) : UIColor.siraniTextPrimary
  798. lblValue.textAlignment = .right
  799. lblValue.setContentCompressionResistancePriority(.required, for: .horizontal)
  800. row.addSubview(iconBadge)
  801. row.addSubview(lblCaption)
  802. row.addSubview(lblValue)
  803. NSLayoutConstraint.activate([
  804. row.heightAnchor.constraint(greaterThanOrEqualToConstant: 34),
  805. iconBadge.leadingAnchor.constraint(equalTo: row.leadingAnchor),
  806. iconBadge.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  807. lblCaption.leadingAnchor.constraint(equalTo: iconBadge.trailingAnchor, constant: 10),
  808. lblCaption.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  809. lblValue.trailingAnchor.constraint(equalTo: row.trailingAnchor),
  810. lblValue.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  811. lblValue.leadingAnchor.constraint(greaterThanOrEqualTo: lblCaption.trailingAnchor, constant: 8),
  812. row.topAnchor.constraint(lessThanOrEqualTo: iconBadge.topAnchor),
  813. row.bottomAnchor.constraint(greaterThanOrEqualTo: iconBadge.bottomAnchor)
  814. ])
  815. return row
  816. }
  817. private func createActionRow(iconName: String, title: String, subtitle: String? = nil, iconTintColor: UIColor = UIColor.siraniPrimaryBlue, iconBgColor: UIColor = UIColor.siraniSoftBlue, action: Selector) -> UIView {
  818. let btn = UIButton(type: .system)
  819. btn.translatesAutoresizingMaskIntoConstraints = false
  820. btn.addTarget(self, action: action, for: .touchUpInside)
  821. let iconBadge = createIconBadge(systemName: iconName, tintColor: iconTintColor, bgColor: iconBgColor, size: 32)
  822. iconBadge.isUserInteractionEnabled = false
  823. let textStack = UIStackView()
  824. textStack.axis = .vertical
  825. textStack.spacing = 2
  826. textStack.isUserInteractionEnabled = false
  827. textStack.translatesAutoresizingMaskIntoConstraints = false
  828. let lblTitle = UILabel()
  829. lblTitle.text = title
  830. lblTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold)
  831. lblTitle.textColor = UIColor.siraniTextPrimary
  832. textStack.addArrangedSubview(lblTitle)
  833. if let sub = subtitle {
  834. let lblSub = UILabel()
  835. lblSub.text = sub
  836. lblSub.font = UIFont.systemFont(ofSize: 11, weight: .regular)
  837. lblSub.textColor = UIColor.siraniTextSecondary
  838. textStack.addArrangedSubview(lblSub)
  839. }
  840. let chevron = UIImageView(image: UIImage(systemName: "chevron.right", withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold)))
  841. chevron.tintColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)
  842. chevron.contentMode = .scaleAspectFit
  843. chevron.translatesAutoresizingMaskIntoConstraints = false
  844. chevron.isUserInteractionEnabled = false
  845. btn.addSubview(iconBadge)
  846. btn.addSubview(textStack)
  847. btn.addSubview(chevron)
  848. NSLayoutConstraint.activate([
  849. btn.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
  850. iconBadge.leadingAnchor.constraint(equalTo: btn.leadingAnchor),
  851. iconBadge.centerYAnchor.constraint(equalTo: btn.centerYAnchor),
  852. chevron.trailingAnchor.constraint(equalTo: btn.trailingAnchor, constant: -4),
  853. chevron.centerYAnchor.constraint(equalTo: btn.centerYAnchor),
  854. chevron.widthAnchor.constraint(equalToConstant: 12),
  855. chevron.heightAnchor.constraint(equalToConstant: 16),
  856. textStack.leadingAnchor.constraint(equalTo: iconBadge.trailingAnchor, constant: 12),
  857. textStack.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8),
  858. textStack.centerYAnchor.constraint(equalTo: btn.centerYAnchor),
  859. textStack.topAnchor.constraint(greaterThanOrEqualTo: btn.topAnchor, constant: 6),
  860. textStack.bottomAnchor.constraint(lessThanOrEqualTo: btn.bottomAnchor, constant: -6)
  861. ])
  862. return btn
  863. }
  864. private func createMiniActionButton(title: String, iconName: String, tintColor: UIColor = UIColor.siraniPrimaryBlue, action: Selector) -> UIButton {
  865. let btn = UIButton(type: .system)
  866. btn.translatesAutoresizingMaskIntoConstraints = false
  867. btn.heightAnchor.constraint(equalToConstant: 38).isActive = true
  868. btn.backgroundColor = .white
  869. btn.layer.cornerRadius = 10
  870. btn.layer.borderWidth = 1
  871. btn.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 240/255, alpha: 1.0).cgColor
  872. btn.addTarget(self, action: action, for: .touchUpInside)
  873. let config = UIImage.SymbolConfiguration(pointSize: 12, weight: .bold)
  874. let icon = UIImage(systemName: iconName, withConfiguration: config)
  875. btn.setImage(icon, for: .normal)
  876. btn.tintColor = tintColor
  877. btn.setTitle(" \(title)", for: .normal)
  878. btn.setTitleColor(UIColor.siraniTextPrimary, for: .normal)
  879. btn.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
  880. return btn
  881. }
  882. private func createDivider() -> UIView {
  883. let line = UIView()
  884. line.translatesAutoresizingMaskIntoConstraints = false
  885. line.heightAnchor.constraint(equalToConstant: 1).isActive = true
  886. line.backgroundColor = UIColor(red: 241/255, green: 244/255, blue: 249/255, alpha: 1.0)
  887. return line
  888. }
  889. private func getInitials(from name: String) -> String {
  890. let parts = name.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ").filter { !$0.isEmpty }
  891. if parts.count >= 2 {
  892. let first = parts[0].prefix(1)
  893. let second = parts[1].prefix(1)
  894. return "\(first)\(second)".uppercased()
  895. } else if let firstPart = parts.first, !firstPart.isEmpty {
  896. return String(firstPart.prefix(2)).uppercased()
  897. }
  898. return "SR"
  899. }
  900. private func getCurrentRoleTitle() -> String {
  901. let userRoleId = UserState.instance.getRoleId()
  902. if userRoleId == CUSTOMER_ROLE_ID {
  903. return CUSTOMER_ROLE
  904. } else if userRoleId == OPERATOR_ROLE_ID {
  905. return OPERATOR_ROLE
  906. } else if userRoleId == KEPALA_SEKSI_ROLE_ID {
  907. return KEPALA_SEKSI_ROLE
  908. } else if userRoleId == KEPALA_SUB_DIREKTORAT_ROLE_ID {
  909. return KEPALA_SUB_DIREKTORAT_ROLE
  910. } else if userRoleId == DIREKTUR_ROLE_ID {
  911. return DIREKTUR_ROLE
  912. }
  913. return "Customer"
  914. }
  915. private func getRoleDescription(roleName: String) -> String {
  916. switch roleName {
  917. case CUSTOMER_ROLE:
  918. return "Hak akses pemohon untuk mengajukan permohonan sertifikasi, mengunggah dokumen, dan memantau status sertifikat."
  919. case OPERATOR_ROLE:
  920. return "Hak akses operasional untuk memeriksa kelengkapan berkas, verifikasi data teknis pemohon, dan pemrosesan penerbitan."
  921. case KEPALA_SEKSI_ROLE:
  922. return "Hak akses sub-koordinasi untuk validasi teknis dan persetujuan pengujian bertingkat."
  923. case KEPALA_SUB_DIREKTORAT_ROLE:
  924. return "Hak akses koordinasi untuk verifikasi lanjutan dan pengesahan SP2 sertifikat."
  925. case DIREKTUR_ROLE:
  926. return "Hak akses pimpinan untuk pengesahan akhir dan penandatanganan sertifikat resmi."
  927. default:
  928. return "Hak akses pengguna layanan e-Sertifikasi DJID Kementerian Komunikasi dan Digital."
  929. }
  930. }
  931. // MARK: - Actions & Event Handlers
  932. @objc private func triggerRolePopup() {
  933. let alertRole = UIAlertController(
  934. title: "Pilih Hak Akses / Role",
  935. message: "Silakan pilih peran untuk mengubah hak akses Anda:",
  936. preferredStyle: .actionSheet
  937. )
  938. let roleMapping: [(id: String, name: String)] = [
  939. (CUSTOMER_ROLE_ID, CUSTOMER_ROLE),
  940. (OPERATOR_ROLE_ID, OPERATOR_ROLE),
  941. (KEPALA_SEKSI_ROLE_ID, KEPALA_SEKSI_ROLE),
  942. (KEPALA_SUB_DIREKTORAT_ROLE_ID, KEPALA_SUB_DIREKTORAT_ROLE),
  943. (DIREKTUR_ROLE_ID, DIREKTUR_ROLE)
  944. ]
  945. let currentRoleId = UserState.instance.getRoleId()
  946. let userRoles = UserState.instance.getRoles().components(separatedBy: ";")
  947. for role in roleMapping {
  948. if userRoles.contains(role.id) || availableRoles.contains(role.id) {
  949. let isCurrent = (currentRoleId == role.id)
  950. let title = isCurrent ? "✓ \(role.name) (Aktif)" : role.name
  951. alertRole.addAction(UIAlertAction(title: title, style: .default, handler: { _ in
  952. let _ = UserState.instance.saveRoleId(roleId: role.id)
  953. self.showProfileLayout()
  954. }))
  955. }
  956. }
  957. alertRole.addAction(UIAlertAction(title: "Batal", style: .cancel, handler: nil))
  958. if let popover = alertRole.popoverPresentationController {
  959. popover.sourceView = self.view
  960. popover.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
  961. popover.permittedArrowDirections = []
  962. }
  963. self.present(alertRole, animated: true, completion: nil)
  964. }
  965. @objc private func openResetPasswordAction() {
  966. if let url = URL(string: urlResetPassword) {
  967. UIApplication.shared.open(url)
  968. }
  969. }
  970. @objc private func openRegisterAction() {
  971. if let url = URL(string: urlRegister) {
  972. UIApplication.shared.open(url)
  973. }
  974. }
  975. @objc private func openWebAction() {
  976. if let url = URL(string: urlWebPortal) {
  977. UIApplication.shared.open(url)
  978. }
  979. }
  980. @objc private func callCenterAction() {
  981. if let url = URL(string: "tel:159") {
  982. UIApplication.shared.open(url)
  983. }
  984. }
  985. @objc private func openWhatsAppAction() {
  986. if let url = URL(string: "https://wa.me/628111100159") {
  987. UIApplication.shared.open(url)
  988. }
  989. }
  990. @objc private func openTermsAction() {
  991. if let url = URL(string: "https://sertifikasi.postel.go.id/regulasi") {
  992. UIApplication.shared.open(url)
  993. }
  994. }
  995. private func inputValid(username: String, password: String) -> Bool {
  996. var valid = true
  997. if username.isEmpty {
  998. txtUsernameError.text = "* Username atau email wajib diisi"
  999. txtUsernameError.show()
  1000. valid = false
  1001. } else {
  1002. txtUsernameError.hide()
  1003. }
  1004. if password.isEmpty {
  1005. txtPasswordError.text = "* Kata sandi wajib diisi"
  1006. txtPasswordError.show()
  1007. valid = false
  1008. } else {
  1009. txtPasswordError.hide()
  1010. }
  1011. return valid
  1012. }
  1013. private func login(username: String, password: String) {
  1014. if !loading {
  1015. if ConnectionHelper.isConnectedToNetwork() {
  1016. self.presenter.login(username: username, password: password)
  1017. } else {
  1018. showNoConnection()
  1019. }
  1020. }
  1021. }
  1022. @IBAction func onBtnTogglePasswordTap(_ sender: Any) {
  1023. let passHidden = inputPassword.isSecureTextEntry
  1024. if passHidden {
  1025. inputPassword.isSecureTextEntry = false
  1026. btnTogglePassword.setImage(UIImage(named: "hide-password"), for: .normal)
  1027. } else {
  1028. inputPassword.isSecureTextEntry = true
  1029. btnTogglePassword.setImage(UIImage(named: "show-password"), for: .normal)
  1030. }
  1031. }
  1032. @IBAction func onBtnLoginTapped(_ sender: Any) {
  1033. view.endEditing(true)
  1034. if !loading {
  1035. let username = inputUsername.getText() ?? ""
  1036. let password = inputPassword.getText() ?? ""
  1037. if inputValid(username: username, password: password) {
  1038. login(username: username, password: password)
  1039. }
  1040. }
  1041. }
  1042. @IBAction func onBtnOptionsRolesTap(_ sender: Any) {
  1043. triggerRolePopup()
  1044. }
  1045. @IBAction func onBtnLogoutTap(_ sender: Any) {
  1046. let alertLogout = UIAlertController(
  1047. title: "Konfirmasi Keluar",
  1048. message: "Apakah Anda yakin ingin keluar dari akun SIRANI?",
  1049. preferredStyle: .alert
  1050. )
  1051. alertLogout.addAction(UIAlertAction(title: "Batal", style: .cancel, handler: nil))
  1052. alertLogout.addAction(UIAlertAction(title: "Keluar", style: .destructive, handler: { _ in
  1053. self.showLoading()
  1054. let isLoggedOut = UserState.instance.userDataCleared()
  1055. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  1056. self.hideLoading()
  1057. if isLoggedOut {
  1058. self.showLoginLayout()
  1059. }
  1060. }
  1061. }))
  1062. self.present(alertLogout, animated: true, completion: nil)
  1063. }
  1064. @IBAction func onInputUsernameKeyTap(_ sender: Any) {
  1065. inputPassword.becomeFirstResponder()
  1066. }
  1067. @IBAction func onInputPasswordKeyTap(_ sender: Any) {
  1068. view.endEditing(true)
  1069. onBtnLoginTapped(sender)
  1070. }
  1071. }
  1072. // MARK: - LoginViewDelegate
  1073. extension LoginVC: LoginViewDelegate {
  1074. func showNoConnection() {
  1075. let alertNoConnection = UIAlertController(
  1076. title: alertNoConnectionTitle,
  1077. message: alertNoConnectionMsg,
  1078. preferredStyle: .alert
  1079. )
  1080. alertNoConnection.addAction(
  1081. UIAlertAction(title: alertNoConnectionActionYes, style: .default, handler: nil)
  1082. )
  1083. self.present(alertNoConnection, animated: true, completion: nil)
  1084. }
  1085. func showLoading() {
  1086. loading = true
  1087. progress.show()
  1088. }
  1089. func hideLoading() {
  1090. loading = false
  1091. progress.hide()
  1092. }
  1093. func onFailedLogin(message: String) {
  1094. let alertLoginFailed = UIAlertController(
  1095. title: alertLoginFailedTitle,
  1096. message: message,
  1097. preferredStyle: .alert
  1098. )
  1099. alertLoginFailed.addAction(
  1100. UIAlertAction(title: alertLoginFailedActionYes, style: .default, handler: nil)
  1101. )
  1102. self.present(alertLoginFailed, animated: true, completion: nil)
  1103. }
  1104. func onSuccessLogin(user: User) {
  1105. debugPrint(user)
  1106. DispatchQueue.main.async {
  1107. self.showProfileLayout()
  1108. // Redirect to Home tab (Beranda)
  1109. self.tabBarController?.selectedIndex = 0
  1110. }
  1111. }
  1112. }