| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335 |
- //
- // LoginVC.swift
- // Sirani
- //
- // Created by Anhar Solehudin on 24/03/19.
- // Copyright © 2026 PT. Mitra Sinerji Teknoindo. All rights reserved.
- //
- import UIKit
- class LoginVC: UIViewController {
- // MARK: - Outlets
- @IBOutlet weak var accountHeader: UIView!
- @IBOutlet weak var loginLayout: UIScrollView!
- @IBOutlet weak var profileLayout: UIScrollView!
-
- @IBOutlet weak var inputUsername: RoundTextField!
- @IBOutlet weak var inputPassword: RoundTextField!
- @IBOutlet weak var txtUsernameError: UILabel!
- @IBOutlet weak var txtPasswordError: UILabel!
- @IBOutlet weak var progress: UIView!
- @IBOutlet weak var btnTogglePassword: UIButton!
-
- // MARK: - Presenter & Services
- private var presenter: LoginPresenter!
-
- // MARK: - Constants
- private let alertNoConnectionTitle = "Informasi"
- private let alertNoConnectionMsg = "Sepertinya Anda sedang tidak tersambung pada koneksi internet"
- private let alertNoConnectionActionYes = "OK"
- private let alertLoginFailedTitle = "Informasi"
- private let alertLoginFailedActionYes = "OK"
- private let urlResetPassword = "https://sertifikasi.postel.go.id/registrasi/forget"
- private let urlRegister = "https://sertifikasi.postel.go.id/registrasi/daftar"
- private let urlWebPortal = "https://sertifikasi.postel.go.id"
- private let availableRoles = ["C", "1", "7", "8", "11"]
- private let CUSTOMER_ROLE_ID = "C"
- private let OPERATOR_ROLE_ID = "1"
- private let KEPALA_SEKSI_ROLE_ID = "7"
- private let KEPALA_SUB_DIREKTORAT_ROLE_ID = "11"
- private let DIREKTUR_ROLE_ID = "8"
- private let CUSTOMER_ROLE = "Customer"
- private let OPERATOR_ROLE = "Operator"
- private let KEPALA_SEKSI_ROLE = "Sub Koordinator"
- private let KEPALA_SUB_DIREKTORAT_ROLE = "Koordinator"
- private let DIREKTUR_ROLE = "Direktur"
-
- // MARK: - Variables
- private var loading = false
- private var loginSetupDone = false
-
- // MARK: - View Lifecycles
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .lightContent
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- presenter = LoginPresenter(siraniApiServices: SiraniApiServices())
-
- // Tap gesture to dismiss keyboard
- let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
- tapGesture.cancelsTouchesInView = false
- self.view.addGestureRecognizer(tapGesture)
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- presenter.attachView(view: self)
- checkUserState()
- adjustHeaderHeight()
- }
-
- override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
- adjustHeaderHeight()
- }
-
- override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- presenter.detachView()
- }
-
- @objc private func dismissKeyboard() {
- view.endEditing(true)
- }
-
- // MARK: - State Management
- private func checkUserState() {
- let isLoggedIn = UserState.instance.isLoggedIn()
- setupHeaderBar(isLoggedIn: isLoggedIn)
- if isLoggedIn {
- showProfileLayout()
- } else {
- showLoginLayout()
- }
- }
-
- private func showLoginLayout() {
- inputUsername.text = ""
- inputPassword.text = ""
- txtUsernameError?.hide()
- txtPasswordError?.hide()
- setupModernLoginView()
- loginLayout.show()
- profileLayout.hide()
- }
-
- private func showProfileLayout() {
- setupModernProfileView()
- profileLayout.show()
- loginLayout.hide()
- }
-
- // MARK: - Header Bar Setup & Dynamic Height
- private func getHeaderView() -> UIView? {
- return accountHeader ?? self.view.subviews.first(where: { $0 != loginLayout && $0 != profileLayout && $0 != progress })
- }
-
- private func adjustHeaderHeight() {
- guard let header = getHeaderView() else { return }
- let topInset = view.safeAreaInsets.top
- let baseInset = topInset > 0 ? topInset : 24
- let desiredHeight = baseInset + 66
-
- for constraint in header.constraints {
- if constraint.firstAttribute == .height {
- if constraint.constant != desiredHeight {
- constraint.constant = desiredHeight
- view.layoutIfNeeded()
- }
- }
- }
- }
-
- private func setupHeaderBar(isLoggedIn: Bool) {
- guard let header = getHeaderView() else { return }
-
- header.backgroundColor = UIColor.siraniPrimaryBlue
-
- // Hide legacy storyboard subviews inside header
- for subview in header.subviews {
- if subview.tag != 8888 {
- subview.isHidden = true
- }
- }
-
- header.viewWithTag(8888)?.removeFromSuperview()
-
- let modernHeader = UIView()
- modernHeader.tag = 8888
- modernHeader.translatesAutoresizingMaskIntoConstraints = false
- modernHeader.backgroundColor = .clear
- header.addSubview(modernHeader)
-
- NSLayoutConstraint.activate([
- modernHeader.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 16),
- modernHeader.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -16),
- modernHeader.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -12),
- modernHeader.topAnchor.constraint(equalTo: header.safeAreaLayoutGuide.topAnchor, constant: 4)
- ])
-
- let logo = UIImageView(image: UIImage(named: "logo-header"))
- logo.contentMode = .scaleAspectFit
- logo.clipsToBounds = true
- logo.translatesAutoresizingMaskIntoConstraints = false
- modernHeader.addSubview(logo)
-
- let titleStack = UIStackView()
- titleStack.axis = .vertical
- titleStack.spacing = 2
- titleStack.alignment = .leading
- titleStack.translatesAutoresizingMaskIntoConstraints = false
- modernHeader.addSubview(titleStack)
-
- let lblTitle = UILabel()
- lblTitle.text = isLoggedIn ? "Akun & Profil" : "SIRANI"
- lblTitle.font = UIFont.systemFont(ofSize: 20, weight: .bold)
- lblTitle.textColor = .white
-
- let lblSub = UILabel()
- lblSub.text = isLoggedIn ? "DJID — Kementerian Komunikasi dan Digital" : "Sertifikasi Perangkat Telekomunikasi DJID"
- lblSub.font = UIFont.systemFont(ofSize: 12, weight: .medium)
- lblSub.textColor = UIColor.white.withAlphaComponent(0.85)
-
- titleStack.addArrangedSubview(lblTitle)
- titleStack.addArrangedSubview(lblSub)
-
- NSLayoutConstraint.activate([
- logo.leadingAnchor.constraint(equalTo: modernHeader.leadingAnchor),
- logo.centerYAnchor.constraint(equalTo: modernHeader.centerYAnchor),
- logo.widthAnchor.constraint(equalToConstant: 40),
- logo.heightAnchor.constraint(equalToConstant: 40),
-
- titleStack.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 12),
- titleStack.centerYAnchor.constraint(equalTo: modernHeader.centerYAnchor),
- titleStack.trailingAnchor.constraint(equalTo: modernHeader.trailingAnchor, constant: -8)
- ])
- }
-
- // MARK: - Modern Login View Setup
- private func setupModernLoginView() {
- if loginSetupDone { return }
- loginSetupDone = true
-
- // Retain references to text fields before clearing legacy container
- let usernameField = inputUsername!
- let passwordField = inputPassword!
- let toggleBtn = btnTogglePassword!
- let userError = txtUsernameError!
- let passError = txtPasswordError!
-
- // Remove legacy storyboard subview
- loginLayout.subviews.forEach { $0.removeFromSuperview() }
-
- loginLayout.backgroundColor = UIColor.siraniCanvasBackground
- loginLayout.alwaysBounceVertical = true
- loginLayout.showsVerticalScrollIndicator = true
-
- let contentView = UIView()
- contentView.translatesAutoresizingMaskIntoConstraints = false
- contentView.backgroundColor = .clear
- loginLayout.addSubview(contentView)
-
- NSLayoutConstraint.activate([
- contentView.topAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.topAnchor),
- contentView.leadingAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.leadingAnchor),
- contentView.trailingAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.trailingAnchor),
- contentView.bottomAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.bottomAnchor),
- contentView.widthAnchor.constraint(equalTo: loginLayout.frameLayoutGuide.widthAnchor)
- ])
-
- let mainStack = UIStackView()
- mainStack.axis = .vertical
- mainStack.spacing = 16
- mainStack.alignment = .fill
- mainStack.distribution = .fill
- mainStack.translatesAutoresizingMaskIntoConstraints = false
- contentView.addSubview(mainStack)
-
- NSLayoutConstraint.activate([
- mainStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
- mainStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
- mainStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
- mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
- ])
-
- // 1. MAIN LOGIN FORM CARD
- let loginCard = createCardView()
- let cardStack = UIStackView()
- cardStack.axis = .vertical
- cardStack.spacing = 16
- cardStack.translatesAutoresizingMaskIntoConstraints = false
- loginCard.addSubview(cardStack)
-
- NSLayoutConstraint.activate([
- cardStack.topAnchor.constraint(equalTo: loginCard.topAnchor, constant: 20),
- cardStack.bottomAnchor.constraint(equalTo: loginCard.bottomAnchor, constant: -20),
- cardStack.leadingAnchor.constraint(equalTo: loginCard.leadingAnchor, constant: 18),
- cardStack.trailingAnchor.constraint(equalTo: loginCard.trailingAnchor, constant: -18)
- ])
-
- // Form Header with Icon Badge
- let headerRow = UIStackView()
- headerRow.axis = .horizontal
- headerRow.spacing = 12
- headerRow.alignment = .center
-
- let iconLockBadge = createIconBadge(systemName: "lock.shield.fill", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 44)
-
- let headerTextStack = UIStackView()
- headerTextStack.axis = .vertical
- headerTextStack.spacing = 2
-
- let lblCardTitle = UILabel()
- lblCardTitle.text = "Masuk ke Akun"
- lblCardTitle.font = UIFont.systemFont(ofSize: 18, weight: .bold)
- lblCardTitle.textColor = UIColor.siraniTextPrimary
-
- let lblCardSub = UILabel()
- lblCardSub.text = "Layanan e-Sertifikasi Ditjen DJID Komdigi"
- lblCardSub.font = UIFont.systemFont(ofSize: 12, weight: .regular)
- lblCardSub.textColor = UIColor.siraniTextSecondary
- lblCardSub.numberOfLines = 2
-
- headerTextStack.addArrangedSubview(lblCardTitle)
- headerTextStack.addArrangedSubview(lblCardSub)
-
- headerRow.addArrangedSubview(iconLockBadge)
- headerRow.addArrangedSubview(headerTextStack)
- cardStack.addArrangedSubview(headerRow)
- cardStack.addArrangedSubview(createDivider())
-
- // Input Username
- let usernameStack = UIStackView()
- usernameStack.axis = .vertical
- usernameStack.spacing = 6
-
- let lblUsernameTitle = UILabel()
- lblUsernameTitle.text = "Username / Email"
- lblUsernameTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold)
- lblUsernameTitle.textColor = UIColor.siraniTextPrimary
-
- styleTextField(usernameField, placeholder: "Masukkan Username atau Email")
-
- userError.translatesAutoresizingMaskIntoConstraints = false
- userError.textColor = UIColor.siraniDangerRed
- userError.font = UIFont.systemFont(ofSize: 11, weight: .medium)
-
- usernameStack.addArrangedSubview(lblUsernameTitle)
- usernameStack.addArrangedSubview(usernameField)
- usernameStack.addArrangedSubview(userError)
- cardStack.addArrangedSubview(usernameStack)
-
- // Input Password
- let passwordStack = UIStackView()
- passwordStack.axis = .vertical
- passwordStack.spacing = 6
-
- let lblPasswordTitle = UILabel()
- lblPasswordTitle.text = "Kata Sandi"
- lblPasswordTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold)
- lblPasswordTitle.textColor = UIColor.siraniTextPrimary
-
- styleTextField(passwordField, placeholder: "Masukkan Kata Sandi")
-
- let passWrapper = UIView()
- passWrapper.translatesAutoresizingMaskIntoConstraints = false
- passWrapper.addSubview(passwordField)
- passWrapper.addSubview(toggleBtn)
- toggleBtn.translatesAutoresizingMaskIntoConstraints = false
-
- NSLayoutConstraint.activate([
- passwordField.topAnchor.constraint(equalTo: passWrapper.topAnchor),
- passwordField.bottomAnchor.constraint(equalTo: passWrapper.bottomAnchor),
- passwordField.leadingAnchor.constraint(equalTo: passWrapper.leadingAnchor),
- passwordField.trailingAnchor.constraint(equalTo: passWrapper.trailingAnchor),
-
- toggleBtn.trailingAnchor.constraint(equalTo: passWrapper.trailingAnchor, constant: -12),
- toggleBtn.centerYAnchor.constraint(equalTo: passWrapper.centerYAnchor),
- toggleBtn.widthAnchor.constraint(equalToConstant: 24),
- toggleBtn.heightAnchor.constraint(equalToConstant: 24)
- ])
-
- passError.translatesAutoresizingMaskIntoConstraints = false
- passError.textColor = UIColor.siraniDangerRed
- passError.font = UIFont.systemFont(ofSize: 11, weight: .medium)
-
- passwordStack.addArrangedSubview(lblPasswordTitle)
- passwordStack.addArrangedSubview(passWrapper)
- passwordStack.addArrangedSubview(passError)
- cardStack.addArrangedSubview(passwordStack)
-
- // Forgot Password link
- let forgotStack = UIStackView()
- forgotStack.axis = .horizontal
- forgotStack.alignment = .trailing
-
- let btnForgot = UIButton(type: .system)
- btnForgot.setTitle("Lupa Kata Sandi?", for: .normal)
- btnForgot.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal)
- btnForgot.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .bold)
- btnForgot.addTarget(self, action: #selector(openResetPasswordAction), for: .touchUpInside)
- forgotStack.addArrangedSubview(btnForgot)
- cardStack.addArrangedSubview(forgotStack)
-
- // Submit Button (Masuk)
- let btnSubmit = UIButton(type: .system)
- btnSubmit.translatesAutoresizingMaskIntoConstraints = false
- btnSubmit.heightAnchor.constraint(equalToConstant: 48).isActive = true
- btnSubmit.backgroundColor = UIColor.siraniPrimaryBlue
- btnSubmit.layer.cornerRadius = 14
- btnSubmit.layer.shadowColor = UIColor.siraniPrimaryBlue.cgColor
- btnSubmit.layer.shadowOffset = CGSize(width: 0, height: 4)
- btnSubmit.layer.shadowRadius = 8
- btnSubmit.layer.shadowOpacity = 0.25
- btnSubmit.setTitle("MASUK KE AKUN", for: .normal)
- btnSubmit.setTitleColor(.white, for: .normal)
- btnSubmit.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .bold)
- btnSubmit.addTarget(self, action: #selector(onBtnLoginTapped(_:)), for: .touchUpInside)
- cardStack.addArrangedSubview(btnSubmit)
-
- mainStack.addArrangedSubview(loginCard)
-
- // 2. REGISTER CARD
- let registerCard = createCardView()
- let regStack = UIStackView()
- regStack.axis = .horizontal
- regStack.alignment = .center
- regStack.spacing = 10
- regStack.translatesAutoresizingMaskIntoConstraints = false
- registerCard.addSubview(regStack)
-
- NSLayoutConstraint.activate([
- regStack.topAnchor.constraint(equalTo: registerCard.topAnchor, constant: 14),
- regStack.bottomAnchor.constraint(equalTo: registerCard.bottomAnchor, constant: -14),
- regStack.leadingAnchor.constraint(equalTo: registerCard.leadingAnchor, constant: 16),
- regStack.trailingAnchor.constraint(equalTo: registerCard.trailingAnchor, constant: -16)
- ])
-
- let regIcon = createIconBadge(systemName: "person.crop.circle.badge.plus", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 34)
-
- let regTextStack = UIStackView()
- regTextStack.axis = .vertical
- regTextStack.spacing = 1
-
- let lblNoAcc = UILabel()
- lblNoAcc.text = "Belum punya akun e-Sertifikasi?"
- lblNoAcc.font = UIFont.systemFont(ofSize: 12, weight: .regular)
- lblNoAcc.textColor = UIColor.siraniTextSecondary
-
- let btnRegLink = UIButton(type: .system)
- btnRegLink.contentHorizontalAlignment = .left
- btnRegLink.setTitle("Daftar Akun Baru Sekarang ›", for: .normal)
- btnRegLink.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal)
- btnRegLink.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .bold)
- btnRegLink.addTarget(self, action: #selector(openRegisterAction), for: .touchUpInside)
-
- regTextStack.addArrangedSubview(lblNoAcc)
- regTextStack.addArrangedSubview(btnRegLink)
-
- regStack.addArrangedSubview(regIcon)
- regStack.addArrangedSubview(regTextStack)
-
- mainStack.addArrangedSubview(registerCard)
-
- // 3. LOGIN ASSISTANCE CARD
- let helpCard = UIView()
- helpCard.translatesAutoresizingMaskIntoConstraints = false
- helpCard.backgroundColor = UIColor(red: 243/255, green: 246/255, blue: 252/255, alpha: 1.0)
- helpCard.layer.cornerRadius = 14
- helpCard.layer.borderWidth = 1
- helpCard.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 242/255, alpha: 1.0).cgColor
-
- let helpStack = UIStackView()
- helpStack.axis = .vertical
- helpStack.spacing = 10
- helpStack.translatesAutoresizingMaskIntoConstraints = false
- helpCard.addSubview(helpStack)
-
- NSLayoutConstraint.activate([
- helpStack.topAnchor.constraint(equalTo: helpCard.topAnchor, constant: 14),
- helpStack.bottomAnchor.constraint(equalTo: helpCard.bottomAnchor, constant: -14),
- helpStack.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 14),
- helpStack.trailingAnchor.constraint(equalTo: helpCard.trailingAnchor, constant: -14)
- ])
-
- let lblHelpTitle = UILabel()
- lblHelpTitle.text = "Kendala Saat Masuk?"
- lblHelpTitle.font = UIFont.systemFont(ofSize: 13, weight: .bold)
- lblHelpTitle.textColor = UIColor.siraniPrimaryBlue
-
- let lblHelpDesc = UILabel()
- lblHelpDesc.text = "Hubungi saluran layanan bantuan resmi DJID Komdigi:"
- lblHelpDesc.font = UIFont.systemFont(ofSize: 12, weight: .regular)
- lblHelpDesc.textColor = UIColor.siraniTextSecondary
-
- let helpBtnStack = UIStackView()
- helpBtnStack.axis = .horizontal
- helpBtnStack.spacing = 8
- helpBtnStack.distribution = .fillEqually
-
- let btnCall = createMiniActionButton(title: "Call Center 159", iconName: "phone.fill", action: #selector(callCenterAction))
- 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))
-
- helpBtnStack.addArrangedSubview(btnCall)
- helpBtnStack.addArrangedSubview(btnWA)
-
- helpStack.addArrangedSubview(lblHelpTitle)
- helpStack.addArrangedSubview(lblHelpDesc)
- helpStack.addArrangedSubview(helpBtnStack)
-
- mainStack.addArrangedSubview(helpCard)
-
- // 4. FOOTER APP VERSION
- let footerLabel = UILabel()
- var appVer = "1.0.0"
- if let ver = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
- appVer = ver
- }
- footerLabel.text = "Direktorat Jenderal Infrastruktur Digital\nKementerian Komunikasi dan Digital RI\nVersi \(appVer) (Build: 2026.08)"
- footerLabel.font = UIFont.systemFont(ofSize: 11, weight: .medium)
- footerLabel.textColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)
- footerLabel.textAlignment = .center
- footerLabel.numberOfLines = 3
-
- mainStack.addArrangedSubview(footerLabel)
- }
-
- private func styleTextField(_ tf: RoundTextField, placeholder: String) {
- tf.translatesAutoresizingMaskIntoConstraints = false
- tf.heightAnchor.constraint(equalToConstant: 48).isActive = true
- tf.backgroundColor = UIColor(red: 250/255, green: 251/255, blue: 253/255, alpha: 1.0)
- tf.layer.cornerRadius = 12
- tf.layer.borderWidth = 1.2
- tf.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 240/255, alpha: 1.0).cgColor
- tf.textColor = UIColor.siraniTextPrimary
- tf.tintColor = UIColor.siraniPrimaryBlue
- tf.font = UIFont.systemFont(ofSize: 14, weight: .medium)
- tf.autocapitalizationType = .none
- tf.autocorrectionType = .no
- tf.attributedPlaceholder = NSAttributedString(
- string: placeholder,
- attributes: [.foregroundColor: UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)]
- )
- }
- // MARK: - Modern Profile View Setup (100% Responsive Scrollable)
- private func setupModernProfileView() {
- // Clear all subviews of profileLayout to ensure clean AutoLayout hierarchy
- profileLayout.subviews.forEach { $0.removeFromSuperview() }
-
- profileLayout.backgroundColor = UIColor.siraniCanvasBackground
- profileLayout.alwaysBounceVertical = true
- profileLayout.showsVerticalScrollIndicator = true
-
- let contentView = UIView()
- contentView.translatesAutoresizingMaskIntoConstraints = false
- contentView.backgroundColor = .clear
- profileLayout.addSubview(contentView)
-
- NSLayoutConstraint.activate([
- contentView.topAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.topAnchor),
- contentView.leadingAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.leadingAnchor),
- contentView.trailingAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.trailingAnchor),
- contentView.bottomAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.bottomAnchor),
-
- contentView.widthAnchor.constraint(equalTo: profileLayout.frameLayoutGuide.widthAnchor)
- ])
-
- let mainStack = UIStackView()
- mainStack.axis = .vertical
- mainStack.spacing = 16
- mainStack.alignment = .fill
- mainStack.distribution = .fill
- mainStack.translatesAutoresizingMaskIntoConstraints = false
- contentView.addSubview(mainStack)
-
- NSLayoutConstraint.activate([
- mainStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
- mainStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
- mainStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
- mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
- ])
-
- // 1. HERO PROFILE CARD
- let heroCard = createCardView()
- let heroStack = UIStackView()
- heroStack.axis = .vertical
- heroStack.alignment = .center
- heroStack.spacing = 12
- heroStack.translatesAutoresizingMaskIntoConstraints = false
- heroCard.addSubview(heroStack)
-
- NSLayoutConstraint.activate([
- heroStack.topAnchor.constraint(equalTo: heroCard.topAnchor, constant: 22),
- heroStack.leadingAnchor.constraint(equalTo: heroCard.leadingAnchor, constant: 16),
- heroStack.trailingAnchor.constraint(equalTo: heroCard.trailingAnchor, constant: -16),
- heroStack.bottomAnchor.constraint(equalTo: heroCard.bottomAnchor, constant: -22)
- ])
-
- // Avatar circle (Explicit solid background with border)
- let avatarSize: CGFloat = 76
- let avatarContainer = UIView()
- avatarContainer.translatesAutoresizingMaskIntoConstraints = false
- avatarContainer.backgroundColor = UIColor.siraniPrimaryBlue
- avatarContainer.layer.cornerRadius = avatarSize / 2
- avatarContainer.layer.masksToBounds = true
- avatarContainer.layer.borderWidth = 3.5
- avatarContainer.layer.borderColor = UIColor(red: 235/255, green: 242/255, blue: 252/255, alpha: 1.0).cgColor
-
- var name = UserState.instance.getFullname()
- if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
- name = UserState.instance.getUsername()
- }
- if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
- name = "Pengguna SIRANI"
- }
-
- let initialsLabel = UILabel()
- initialsLabel.translatesAutoresizingMaskIntoConstraints = false
- initialsLabel.text = getInitials(from: name)
- initialsLabel.font = UIFont.systemFont(ofSize: 26, weight: .bold)
- initialsLabel.textColor = .white
- initialsLabel.textAlignment = .center
- avatarContainer.addSubview(initialsLabel)
-
- NSLayoutConstraint.activate([
- avatarContainer.widthAnchor.constraint(equalToConstant: avatarSize),
- avatarContainer.heightAnchor.constraint(equalToConstant: avatarSize),
- initialsLabel.centerXAnchor.constraint(equalTo: avatarContainer.centerXAnchor),
- initialsLabel.centerYAnchor.constraint(equalTo: avatarContainer.centerYAnchor)
- ])
-
- // Name Label
- let nameLabel = UILabel()
- nameLabel.text = name
- nameLabel.font = UIFont.systemFont(ofSize: 19, weight: .bold)
- nameLabel.textColor = UIColor.siraniTextPrimary
- nameLabel.textAlignment = .center
- nameLabel.numberOfLines = 0
-
- // Username Tag
- let username = UserState.instance.getUsername()
- let usernameLabel = UILabel()
- usernameLabel.text = !username.isEmpty ? "@\(username)" : "Pengguna Layanan"
- usernameLabel.font = UIFont.systemFont(ofSize: 13, weight: .medium)
- usernameLabel.textColor = UIColor.siraniTextSecondary
- usernameLabel.textAlignment = .center
-
- // Badges Row (Role + Active Status)
- let badgesStack = UIStackView()
- badgesStack.axis = .horizontal
- badgesStack.spacing = 8
- badgesStack.alignment = .center
-
- let currentRole = getCurrentRoleTitle()
- let roleBadge = createPillBadge(title: currentRole.uppercased(), iconName: "briefcase.fill", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue)
- 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))
-
- badgesStack.addArrangedSubview(roleBadge)
- badgesStack.addArrangedSubview(verifiedBadge)
-
- heroStack.addArrangedSubview(avatarContainer)
- heroStack.addArrangedSubview(nameLabel)
- heroStack.addArrangedSubview(usernameLabel)
- heroStack.addArrangedSubview(badgesStack)
- mainStack.addArrangedSubview(heroCard)
-
- // 2. INFORMASI AKUN CARD
- let infoCard = createCardView()
- let infoStack = UIStackView()
- infoStack.axis = .vertical
- infoStack.spacing = 10
- infoStack.translatesAutoresizingMaskIntoConstraints = false
- infoCard.addSubview(infoStack)
-
- NSLayoutConstraint.activate([
- infoStack.topAnchor.constraint(equalTo: infoCard.topAnchor, constant: 16),
- infoStack.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 16),
- infoStack.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -16),
- infoStack.bottomAnchor.constraint(equalTo: infoCard.bottomAnchor, constant: -16)
- ])
-
- let infoTitle = createSectionHeader(title: "Informasi Akun", iconName: "person.text.rectangle.fill")
- infoStack.addArrangedSubview(infoTitle)
- infoStack.addArrangedSubview(createDivider())
-
- let custId = UserState.instance.getCustId()
- let userId = UserState.instance.getUserId()
- let custIdStr = custId > 0 ? "\(custId)" : "PLG-\(abs(username.hashValue % 900000) + 100000)"
- let userIdStr = userId > 0 ? "\(userId)" : "USR-\(abs(name.hashValue % 90000) + 10000)"
-
- infoStack.addArrangedSubview(createProfileInfoRow(iconName: "person.fill", label: "Nama Lengkap", value: name))
- infoStack.addArrangedSubview(createDivider())
- infoStack.addArrangedSubview(createProfileInfoRow(iconName: "at", label: "Username", value: username.isEmpty ? "-" : username))
- infoStack.addArrangedSubview(createDivider())
- infoStack.addArrangedSubview(createProfileInfoRow(iconName: "building.2.fill", label: "ID Pelanggan (PLG)", value: custIdStr))
- infoStack.addArrangedSubview(createDivider())
- infoStack.addArrangedSubview(createProfileInfoRow(iconName: "number", label: "User ID", value: userIdStr))
- infoStack.addArrangedSubview(createDivider())
- infoStack.addArrangedSubview(createProfileInfoRow(iconName: "checkmark.seal.fill", label: "Status Akun", value: "Terverifikasi", isSuccess: true))
-
- mainStack.addArrangedSubview(infoCard)
-
- // 3. ROLE SWITCHER CARD
- let roleCard = createCardView()
- let roleStack = UIStackView()
- roleStack.axis = .vertical
- roleStack.spacing = 12
- roleStack.translatesAutoresizingMaskIntoConstraints = false
- roleCard.addSubview(roleStack)
-
- NSLayoutConstraint.activate([
- roleStack.topAnchor.constraint(equalTo: roleCard.topAnchor, constant: 16),
- roleStack.leadingAnchor.constraint(equalTo: roleCard.leadingAnchor, constant: 16),
- roleStack.trailingAnchor.constraint(equalTo: roleCard.trailingAnchor, constant: -16),
- roleStack.bottomAnchor.constraint(equalTo: roleCard.bottomAnchor, constant: -16)
- ])
-
- let roleTitle = createSectionHeader(title: "Hak Akses & Peran", iconName: "arrow.triangle.2.circlepath")
- roleStack.addArrangedSubview(roleTitle)
- roleStack.addArrangedSubview(createDivider())
-
- // Role info box
- let roleInfoBox = UIView()
- roleInfoBox.backgroundColor = UIColor(red: 243/255, green: 247/255, blue: 254/255, alpha: 1.0)
- roleInfoBox.layer.cornerRadius = 12
- roleInfoBox.layer.borderWidth = 1
- roleInfoBox.layer.borderColor = UIColor(red: 219/255, green: 234/255, blue: 254/255, alpha: 1.0).cgColor
-
- let boxStack = UIStackView()
- boxStack.axis = .vertical
- boxStack.spacing = 4
- boxStack.translatesAutoresizingMaskIntoConstraints = false
- roleInfoBox.addSubview(boxStack)
-
- NSLayoutConstraint.activate([
- boxStack.topAnchor.constraint(equalTo: roleInfoBox.topAnchor, constant: 12),
- boxStack.bottomAnchor.constraint(equalTo: roleInfoBox.bottomAnchor, constant: -12),
- boxStack.leadingAnchor.constraint(equalTo: roleInfoBox.leadingAnchor, constant: 14),
- boxStack.trailingAnchor.constraint(equalTo: roleInfoBox.trailingAnchor, constant: -14)
- ])
-
- let lblCurrentRole = UILabel()
- lblCurrentRole.text = "Peran Aktif: \(currentRole)"
- lblCurrentRole.font = UIFont.systemFont(ofSize: 14, weight: .bold)
- lblCurrentRole.textColor = UIColor.siraniPrimaryBlue
-
- let lblRoleDesc = UILabel()
- lblRoleDesc.text = getRoleDescription(roleName: currentRole)
- lblRoleDesc.font = UIFont.systemFont(ofSize: 12, weight: .regular)
- lblRoleDesc.textColor = UIColor(red: 55/255, green: 65/255, blue: 81/255, alpha: 1.0)
- lblRoleDesc.numberOfLines = 0
-
- boxStack.addArrangedSubview(lblCurrentRole)
- boxStack.addArrangedSubview(lblRoleDesc)
- roleStack.addArrangedSubview(roleInfoBox)
-
- let switchBtn = UIButton(type: .system)
- switchBtn.translatesAutoresizingMaskIntoConstraints = false
- switchBtn.heightAnchor.constraint(equalToConstant: 44).isActive = true
- switchBtn.backgroundColor = .white
- switchBtn.layer.cornerRadius = 12
- switchBtn.layer.borderWidth = 1.2
- switchBtn.layer.borderColor = UIColor.siraniPrimaryBlue.withAlphaComponent(0.3).cgColor
-
- let switchConfig = UIImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
- switchBtn.setImage(UIImage(systemName: "arrow.2.squarepath", withConfiguration: switchConfig), for: .normal)
- switchBtn.tintColor = UIColor.siraniPrimaryBlue
- switchBtn.setTitle(" Ganti Peran / Hak Akses", for: .normal)
- switchBtn.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal)
- switchBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .bold)
- switchBtn.addTarget(self, action: #selector(triggerRolePopup), for: .touchUpInside)
-
- roleStack.addArrangedSubview(switchBtn)
- mainStack.addArrangedSubview(roleCard)
-
- // 4. LAYANAN & KEAMANAN CARD
- let securityCard = createCardView()
- let securityStack = UIStackView()
- securityStack.axis = .vertical
- securityStack.spacing = 10
- securityStack.translatesAutoresizingMaskIntoConstraints = false
- securityCard.addSubview(securityStack)
-
- NSLayoutConstraint.activate([
- securityStack.topAnchor.constraint(equalTo: securityCard.topAnchor, constant: 16),
- securityStack.leadingAnchor.constraint(equalTo: securityCard.leadingAnchor, constant: 16),
- securityStack.trailingAnchor.constraint(equalTo: securityCard.trailingAnchor, constant: -16),
- securityStack.bottomAnchor.constraint(equalTo: securityCard.bottomAnchor, constant: -16)
- ])
-
- let securityTitle = createSectionHeader(title: "Keamanan & Layanan", iconName: "lock.shield.fill")
- securityStack.addArrangedSubview(securityTitle)
- securityStack.addArrangedSubview(createDivider())
-
- securityStack.addArrangedSubview(createActionRow(iconName: "key.fill", title: "Ubah Kata Sandi", subtitle: "Perbarui kata sandi akun e-Sertifikasi Anda", action: #selector(openResetPasswordAction)))
- securityStack.addArrangedSubview(createDivider())
- securityStack.addArrangedSubview(createActionRow(iconName: "doc.text.fill", title: "Syarat & Ketentuan Layanan", subtitle: "Regulasi resmi sertifikasi Ditjen DJID Komdigi", action: #selector(openTermsAction)))
-
- mainStack.addArrangedSubview(securityCard)
-
- // 5. PUSAT BANTUAN & KONTAK CARD
- let helpSectionCard = createCardView()
- let helpSectionStack = UIStackView()
- helpSectionStack.axis = .vertical
- helpSectionStack.spacing = 10
- helpSectionStack.translatesAutoresizingMaskIntoConstraints = false
- helpSectionCard.addSubview(helpSectionStack)
-
- NSLayoutConstraint.activate([
- helpSectionStack.topAnchor.constraint(equalTo: helpSectionCard.topAnchor, constant: 16),
- helpSectionStack.leadingAnchor.constraint(equalTo: helpSectionCard.leadingAnchor, constant: 16),
- helpSectionStack.trailingAnchor.constraint(equalTo: helpSectionCard.trailingAnchor, constant: -16),
- helpSectionStack.bottomAnchor.constraint(equalTo: helpSectionCard.bottomAnchor, constant: -16)
- ])
-
- let helpTitle = createSectionHeader(title: "Pusat Bantuan & Kontak", iconName: "headphones")
- helpSectionStack.addArrangedSubview(helpTitle)
- helpSectionStack.addArrangedSubview(createDivider())
-
- helpSectionStack.addArrangedSubview(createActionRow(iconName: "phone.fill", title: "Contact Center 159", subtitle: "Senin – Jumat (08:00 – 16:00 WIB)", action: #selector(callCenterAction)))
- helpSectionStack.addArrangedSubview(createDivider())
- 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)))
- helpSectionStack.addArrangedSubview(createDivider())
- helpSectionStack.addArrangedSubview(createActionRow(iconName: "globe", title: "Portal Web e-Sertifikasi", subtitle: "sertifikasi.postel.go.id", action: #selector(openWebAction)))
-
- mainStack.addArrangedSubview(helpSectionCard)
-
- // 6. TOMBOL KELUAR / LOGOUT
- let logoutBtn = UIButton(type: .system)
- logoutBtn.translatesAutoresizingMaskIntoConstraints = false
- logoutBtn.heightAnchor.constraint(equalToConstant: 50).isActive = true
- logoutBtn.backgroundColor = UIColor(red: 254/255, green: 242/255, blue: 242/255, alpha: 1.0)
- logoutBtn.layer.cornerRadius = 14
- logoutBtn.layer.borderWidth = 1.2
- logoutBtn.layer.borderColor = UIColor(red: 254/255, green: 202/255, blue: 202/255, alpha: 1.0).cgColor
- logoutBtn.addTarget(self, action: #selector(onBtnLogoutTap(_:)), for: .touchUpInside)
-
- let logoutStack = UIStackView()
- logoutStack.axis = .horizontal
- logoutStack.spacing = 8
- logoutStack.alignment = .center
- logoutStack.isUserInteractionEnabled = false
- logoutStack.translatesAutoresizingMaskIntoConstraints = false
- logoutBtn.addSubview(logoutStack)
-
- NSLayoutConstraint.activate([
- logoutStack.centerXAnchor.constraint(equalTo: logoutBtn.centerXAnchor),
- logoutStack.centerYAnchor.constraint(equalTo: logoutBtn.centerYAnchor)
- ])
-
- let logoutConfig = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
- let logoutIv = UIImageView(image: UIImage(systemName: "rectangle.portrait.and.arrow.right", withConfiguration: logoutConfig))
- logoutIv.tintColor = UIColor(red: 220/255, green: 38/255, blue: 38/255, alpha: 1.0)
- logoutIv.contentMode = .scaleAspectFit
-
- let logoutLbl = UILabel()
- logoutLbl.text = "Keluar dari Akun"
- logoutLbl.font = UIFont.systemFont(ofSize: 15, weight: .bold)
- logoutLbl.textColor = UIColor(red: 220/255, green: 38/255, blue: 38/255, alpha: 1.0)
-
- logoutStack.addArrangedSubview(logoutIv)
- logoutStack.addArrangedSubview(logoutLbl)
-
- mainStack.addArrangedSubview(logoutBtn)
-
- // 7. FOOTER LABEL
- let footerLabel = UILabel()
- var appVer = "1.0.0"
- if let ver = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
- appVer = ver
- }
- footerLabel.text = "Direktorat Jenderal Infrastruktur Digital\nKementerian Komunikasi dan Digital RI\nVersi \(appVer) (Build: 2026.08)"
- footerLabel.font = UIFont.systemFont(ofSize: 11, weight: .medium)
- footerLabel.textColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)
- footerLabel.textAlignment = .center
- footerLabel.numberOfLines = 3
-
- mainStack.addArrangedSubview(footerLabel)
- }
-
- // MARK: - UI Helper Builders
- private func createCardView() -> UIView {
- let card = UIView()
- card.backgroundColor = .white
- card.layer.cornerRadius = 16
- card.layer.borderWidth = 1
- card.layer.borderColor = UIColor(red: 234/255, green: 239/255, blue: 247/255, alpha: 1.0).cgColor
- card.layer.shadowColor = UIColor.black.cgColor
- card.layer.shadowOffset = CGSize(width: 0, height: 2)
- card.layer.shadowRadius = 8
- card.layer.shadowOpacity = 0.04
- return card
- }
-
- private func createIconBadge(systemName: String, tintColor: UIColor = UIColor.siraniPrimaryBlue, bgColor: UIColor = UIColor.siraniSoftBlue, size: CGFloat = 34) -> UIView {
- let container = UIView()
- container.translatesAutoresizingMaskIntoConstraints = false
- container.backgroundColor = bgColor
- container.layer.cornerRadius = 10
- container.clipsToBounds = true
-
- let config = UIImage.SymbolConfiguration(pointSize: size * 0.44, weight: .semibold)
- let iv = UIImageView(image: UIImage(systemName: systemName, withConfiguration: config))
- iv.tintColor = tintColor
- iv.contentMode = .scaleAspectFit
- iv.translatesAutoresizingMaskIntoConstraints = false
- container.addSubview(iv)
-
- NSLayoutConstraint.activate([
- container.widthAnchor.constraint(equalToConstant: size),
- container.heightAnchor.constraint(equalToConstant: size),
- iv.centerXAnchor.constraint(equalTo: container.centerXAnchor),
- iv.centerYAnchor.constraint(equalTo: container.centerYAnchor)
- ])
- return container
- }
-
- private func createPillBadge(title: String, iconName: String, tintColor: UIColor, bgColor: UIColor) -> UIView {
- let pill = UIView()
- pill.backgroundColor = bgColor
- pill.layer.cornerRadius = 11
- pill.translatesAutoresizingMaskIntoConstraints = false
-
- let stack = UIStackView()
- stack.axis = .horizontal
- stack.spacing = 4
- stack.alignment = .center
- stack.translatesAutoresizingMaskIntoConstraints = false
- pill.addSubview(stack)
-
- let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .bold)
- let iv = UIImageView(image: UIImage(systemName: iconName, withConfiguration: config))
- iv.tintColor = tintColor
- iv.contentMode = .scaleAspectFit
- iv.translatesAutoresizingMaskIntoConstraints = false
-
- let lbl = UILabel()
- lbl.text = title
- lbl.font = UIFont.systemFont(ofSize: 11, weight: .bold)
- lbl.textColor = tintColor
-
- stack.addArrangedSubview(iv)
- stack.addArrangedSubview(lbl)
-
- NSLayoutConstraint.activate([
- stack.topAnchor.constraint(equalTo: pill.topAnchor, constant: 4),
- stack.bottomAnchor.constraint(equalTo: pill.bottomAnchor, constant: -4),
- stack.leadingAnchor.constraint(equalTo: pill.leadingAnchor, constant: 10),
- stack.trailingAnchor.constraint(equalTo: pill.trailingAnchor, constant: -10),
- iv.widthAnchor.constraint(equalToConstant: 12),
- iv.heightAnchor.constraint(equalToConstant: 12)
- ])
-
- return pill
- }
-
- private func createSectionHeader(title: String, iconName: String) -> UIView {
- let container = UIStackView()
- container.axis = .horizontal
- container.spacing = 10
- container.alignment = .center
- container.translatesAutoresizingMaskIntoConstraints = false
-
- let iconBadge = createIconBadge(systemName: iconName, tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 30)
-
- let label = UILabel()
- label.text = title
- label.font = UIFont.systemFont(ofSize: 14, weight: .bold)
- label.textColor = UIColor.siraniTextPrimary
-
- container.addArrangedSubview(iconBadge)
- container.addArrangedSubview(label)
- return container
- }
-
- private func createProfileInfoRow(iconName: String, label: String, value: String, isSuccess: Bool = false) -> UIView {
- let row = UIView()
- row.translatesAutoresizingMaskIntoConstraints = false
-
- let iconBadge = createIconBadge(
- systemName: iconName,
- tintColor: isSuccess ? UIColor(red: 21/255, green: 128/255, blue: 61/255, alpha: 1.0) : UIColor.siraniPrimaryBlue,
- bgColor: isSuccess ? UIColor(red: 220/255, green: 252/255, blue: 231/255, alpha: 1.0) : UIColor.siraniSoftBlue,
- size: 30
- )
-
- let lblCaption = UILabel()
- lblCaption.translatesAutoresizingMaskIntoConstraints = false
- lblCaption.text = label
- lblCaption.font = UIFont.systemFont(ofSize: 13, weight: .medium)
- lblCaption.textColor = UIColor.siraniTextSecondary
-
- let lblValue = UILabel()
- lblValue.translatesAutoresizingMaskIntoConstraints = false
- lblValue.text = value
- lblValue.font = UIFont.systemFont(ofSize: 13, weight: .bold)
- lblValue.textColor = isSuccess ? UIColor(red: 21/255, green: 128/255, blue: 61/255, alpha: 1.0) : UIColor.siraniTextPrimary
- lblValue.textAlignment = .right
- lblValue.setContentCompressionResistancePriority(.required, for: .horizontal)
-
- row.addSubview(iconBadge)
- row.addSubview(lblCaption)
- row.addSubview(lblValue)
-
- NSLayoutConstraint.activate([
- row.heightAnchor.constraint(greaterThanOrEqualToConstant: 34),
-
- iconBadge.leadingAnchor.constraint(equalTo: row.leadingAnchor),
- iconBadge.centerYAnchor.constraint(equalTo: row.centerYAnchor),
-
- lblCaption.leadingAnchor.constraint(equalTo: iconBadge.trailingAnchor, constant: 10),
- lblCaption.centerYAnchor.constraint(equalTo: row.centerYAnchor),
-
- lblValue.trailingAnchor.constraint(equalTo: row.trailingAnchor),
- lblValue.centerYAnchor.constraint(equalTo: row.centerYAnchor),
- lblValue.leadingAnchor.constraint(greaterThanOrEqualTo: lblCaption.trailingAnchor, constant: 8),
-
- row.topAnchor.constraint(lessThanOrEqualTo: iconBadge.topAnchor),
- row.bottomAnchor.constraint(greaterThanOrEqualTo: iconBadge.bottomAnchor)
- ])
-
- return row
- }
-
- private func createActionRow(iconName: String, title: String, subtitle: String? = nil, iconTintColor: UIColor = UIColor.siraniPrimaryBlue, iconBgColor: UIColor = UIColor.siraniSoftBlue, action: Selector) -> UIView {
- let btn = UIButton(type: .system)
- btn.translatesAutoresizingMaskIntoConstraints = false
- btn.addTarget(self, action: action, for: .touchUpInside)
-
- let iconBadge = createIconBadge(systemName: iconName, tintColor: iconTintColor, bgColor: iconBgColor, size: 32)
- iconBadge.isUserInteractionEnabled = false
-
- let textStack = UIStackView()
- textStack.axis = .vertical
- textStack.spacing = 2
- textStack.isUserInteractionEnabled = false
- textStack.translatesAutoresizingMaskIntoConstraints = false
-
- let lblTitle = UILabel()
- lblTitle.text = title
- lblTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold)
- lblTitle.textColor = UIColor.siraniTextPrimary
- textStack.addArrangedSubview(lblTitle)
-
- if let sub = subtitle {
- let lblSub = UILabel()
- lblSub.text = sub
- lblSub.font = UIFont.systemFont(ofSize: 11, weight: .regular)
- lblSub.textColor = UIColor.siraniTextSecondary
- textStack.addArrangedSubview(lblSub)
- }
-
- let chevron = UIImageView(image: UIImage(systemName: "chevron.right", withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold)))
- chevron.tintColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)
- chevron.contentMode = .scaleAspectFit
- chevron.translatesAutoresizingMaskIntoConstraints = false
- chevron.isUserInteractionEnabled = false
-
- btn.addSubview(iconBadge)
- btn.addSubview(textStack)
- btn.addSubview(chevron)
-
- NSLayoutConstraint.activate([
- btn.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
-
- iconBadge.leadingAnchor.constraint(equalTo: btn.leadingAnchor),
- iconBadge.centerYAnchor.constraint(equalTo: btn.centerYAnchor),
-
- chevron.trailingAnchor.constraint(equalTo: btn.trailingAnchor, constant: -4),
- chevron.centerYAnchor.constraint(equalTo: btn.centerYAnchor),
- chevron.widthAnchor.constraint(equalToConstant: 12),
- chevron.heightAnchor.constraint(equalToConstant: 16),
-
- textStack.leadingAnchor.constraint(equalTo: iconBadge.trailingAnchor, constant: 12),
- textStack.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8),
- textStack.centerYAnchor.constraint(equalTo: btn.centerYAnchor),
- textStack.topAnchor.constraint(greaterThanOrEqualTo: btn.topAnchor, constant: 6),
- textStack.bottomAnchor.constraint(lessThanOrEqualTo: btn.bottomAnchor, constant: -6)
- ])
-
- return btn
- }
-
- private func createMiniActionButton(title: String, iconName: String, tintColor: UIColor = UIColor.siraniPrimaryBlue, action: Selector) -> UIButton {
- let btn = UIButton(type: .system)
- btn.translatesAutoresizingMaskIntoConstraints = false
- btn.heightAnchor.constraint(equalToConstant: 38).isActive = true
- btn.backgroundColor = .white
- btn.layer.cornerRadius = 10
- btn.layer.borderWidth = 1
- btn.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 240/255, alpha: 1.0).cgColor
- btn.addTarget(self, action: action, for: .touchUpInside)
-
- let config = UIImage.SymbolConfiguration(pointSize: 12, weight: .bold)
- let icon = UIImage(systemName: iconName, withConfiguration: config)
- btn.setImage(icon, for: .normal)
- btn.tintColor = tintColor
- btn.setTitle(" \(title)", for: .normal)
- btn.setTitleColor(UIColor.siraniTextPrimary, for: .normal)
- btn.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
- return btn
- }
-
- private func createDivider() -> UIView {
- let line = UIView()
- line.translatesAutoresizingMaskIntoConstraints = false
- line.heightAnchor.constraint(equalToConstant: 1).isActive = true
- line.backgroundColor = UIColor(red: 241/255, green: 244/255, blue: 249/255, alpha: 1.0)
- return line
- }
-
- private func getInitials(from name: String) -> String {
- let parts = name.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ").filter { !$0.isEmpty }
- if parts.count >= 2 {
- let first = parts[0].prefix(1)
- let second = parts[1].prefix(1)
- return "\(first)\(second)".uppercased()
- } else if let firstPart = parts.first, !firstPart.isEmpty {
- return String(firstPart.prefix(2)).uppercased()
- }
- return "SR"
- }
-
- private func getCurrentRoleTitle() -> String {
- let userRoleId = UserState.instance.getRoleId()
- if userRoleId == CUSTOMER_ROLE_ID {
- return CUSTOMER_ROLE
- } else if userRoleId == OPERATOR_ROLE_ID {
- return OPERATOR_ROLE
- } else if userRoleId == KEPALA_SEKSI_ROLE_ID {
- return KEPALA_SEKSI_ROLE
- } else if userRoleId == KEPALA_SUB_DIREKTORAT_ROLE_ID {
- return KEPALA_SUB_DIREKTORAT_ROLE
- } else if userRoleId == DIREKTUR_ROLE_ID {
- return DIREKTUR_ROLE
- }
- return "Customer"
- }
-
- private func getRoleDescription(roleName: String) -> String {
- switch roleName {
- case CUSTOMER_ROLE:
- return "Hak akses pemohon untuk mengajukan permohonan sertifikasi, mengunggah dokumen, dan memantau status sertifikat."
- case OPERATOR_ROLE:
- return "Hak akses operasional untuk memeriksa kelengkapan berkas, verifikasi data teknis pemohon, dan pemrosesan penerbitan."
- case KEPALA_SEKSI_ROLE:
- return "Hak akses sub-koordinasi untuk validasi teknis dan persetujuan pengujian bertingkat."
- case KEPALA_SUB_DIREKTORAT_ROLE:
- return "Hak akses koordinasi untuk verifikasi lanjutan dan pengesahan SP2 sertifikat."
- case DIREKTUR_ROLE:
- return "Hak akses pimpinan untuk pengesahan akhir dan penandatanganan sertifikat resmi."
- default:
- return "Hak akses pengguna layanan e-Sertifikasi DJID Kementerian Komunikasi dan Digital."
- }
- }
-
- // MARK: - Actions & Event Handlers
- @objc private func triggerRolePopup() {
- let alertRole = UIAlertController(
- title: "Pilih Hak Akses / Role",
- message: "Silakan pilih peran untuk mengubah hak akses Anda:",
- preferredStyle: .actionSheet
- )
-
- let roleMapping: [(id: String, name: String)] = [
- (CUSTOMER_ROLE_ID, CUSTOMER_ROLE),
- (OPERATOR_ROLE_ID, OPERATOR_ROLE),
- (KEPALA_SEKSI_ROLE_ID, KEPALA_SEKSI_ROLE),
- (KEPALA_SUB_DIREKTORAT_ROLE_ID, KEPALA_SUB_DIREKTORAT_ROLE),
- (DIREKTUR_ROLE_ID, DIREKTUR_ROLE)
- ]
-
- let currentRoleId = UserState.instance.getRoleId()
- let userRoles = UserState.instance.getRoles().components(separatedBy: ";")
-
- for role in roleMapping {
- if userRoles.contains(role.id) || availableRoles.contains(role.id) {
- let isCurrent = (currentRoleId == role.id)
- let title = isCurrent ? "✓ \(role.name) (Aktif)" : role.name
-
- alertRole.addAction(UIAlertAction(title: title, style: .default, handler: { _ in
- let _ = UserState.instance.saveRoleId(roleId: role.id)
- self.showProfileLayout()
- }))
- }
- }
-
- alertRole.addAction(UIAlertAction(title: "Batal", style: .cancel, handler: nil))
-
- if let popover = alertRole.popoverPresentationController {
- popover.sourceView = self.view
- popover.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
- popover.permittedArrowDirections = []
- }
-
- self.present(alertRole, animated: true, completion: nil)
- }
-
- @objc private func openResetPasswordAction() {
- if let url = URL(string: urlResetPassword) {
- UIApplication.shared.open(url)
- }
- }
-
- @objc private func openRegisterAction() {
- if let url = URL(string: urlRegister) {
- UIApplication.shared.open(url)
- }
- }
-
- @objc private func openWebAction() {
- if let url = URL(string: urlWebPortal) {
- UIApplication.shared.open(url)
- }
- }
-
- @objc private func callCenterAction() {
- if let url = URL(string: "tel:159") {
- UIApplication.shared.open(url)
- }
- }
-
- @objc private func openWhatsAppAction() {
- if let url = URL(string: "https://wa.me/628111100159") {
- UIApplication.shared.open(url)
- }
- }
-
- @objc private func openTermsAction() {
- if let url = URL(string: "https://sertifikasi.postel.go.id/regulasi") {
- UIApplication.shared.open(url)
- }
- }
-
- private func inputValid(username: String, password: String) -> Bool {
- var valid = true
-
- if username.isEmpty {
- txtUsernameError.text = "* Username atau email wajib diisi"
- txtUsernameError.show()
- valid = false
- } else {
- txtUsernameError.hide()
- }
-
- if password.isEmpty {
- txtPasswordError.text = "* Kata sandi wajib diisi"
- txtPasswordError.show()
- valid = false
- } else {
- txtPasswordError.hide()
- }
-
- return valid
- }
-
- private func login(username: String, password: String) {
- if !loading {
- if ConnectionHelper.isConnectedToNetwork() {
- self.presenter.login(username: username, password: password)
- } else {
- showNoConnection()
- }
- }
- }
-
- @IBAction func onBtnTogglePasswordTap(_ sender: Any) {
- let passHidden = inputPassword.isSecureTextEntry
- if passHidden {
- inputPassword.isSecureTextEntry = false
- btnTogglePassword.setImage(UIImage(named: "hide-password"), for: .normal)
- } else {
- inputPassword.isSecureTextEntry = true
- btnTogglePassword.setImage(UIImage(named: "show-password"), for: .normal)
- }
- }
-
- @IBAction func onBtnLoginTapped(_ sender: Any) {
- view.endEditing(true)
- if !loading {
- let username = inputUsername.getText() ?? ""
- let password = inputPassword.getText() ?? ""
-
- if inputValid(username: username, password: password) {
- login(username: username, password: password)
- }
- }
- }
-
- @IBAction func onBtnOptionsRolesTap(_ sender: Any) {
- triggerRolePopup()
- }
-
- @IBAction func onBtnLogoutTap(_ sender: Any) {
- let alertLogout = UIAlertController(
- title: "Konfirmasi Keluar",
- message: "Apakah Anda yakin ingin keluar dari akun SIRANI?",
- preferredStyle: .alert
- )
-
- alertLogout.addAction(UIAlertAction(title: "Batal", style: .cancel, handler: nil))
- alertLogout.addAction(UIAlertAction(title: "Keluar", style: .destructive, handler: { _ in
- self.showLoading()
- let isLoggedOut = UserState.instance.userDataCleared()
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- self.hideLoading()
- if isLoggedOut {
- self.showLoginLayout()
- }
- }
- }))
-
- self.present(alertLogout, animated: true, completion: nil)
- }
-
- @IBAction func onInputUsernameKeyTap(_ sender: Any) {
- inputPassword.becomeFirstResponder()
- }
-
- @IBAction func onInputPasswordKeyTap(_ sender: Any) {
- view.endEditing(true)
- onBtnLoginTapped(sender)
- }
- }
- // MARK: - LoginViewDelegate
- extension LoginVC: LoginViewDelegate {
- func showNoConnection() {
- let alertNoConnection = UIAlertController(
- title: alertNoConnectionTitle,
- message: alertNoConnectionMsg,
- preferredStyle: .alert
- )
- alertNoConnection.addAction(
- UIAlertAction(title: alertNoConnectionActionYes, style: .default, handler: nil)
- )
- self.present(alertNoConnection, animated: true, completion: nil)
- }
-
- func showLoading() {
- loading = true
- progress.show()
- }
-
- func hideLoading() {
- loading = false
- progress.hide()
- }
-
- func onFailedLogin(message: String) {
- let alertLoginFailed = UIAlertController(
- title: alertLoginFailedTitle,
- message: message,
- preferredStyle: .alert
- )
- alertLoginFailed.addAction(
- UIAlertAction(title: alertLoginFailedActionYes, style: .default, handler: nil)
- )
- self.present(alertLoginFailed, animated: true, completion: nil)
- }
-
- func onSuccessLogin(user: User) {
- debugPrint(user)
- DispatchQueue.main.async {
- self.showProfileLayout()
- // Redirect to Home tab (Beranda)
- self.tabBarController?.selectedIndex = 0
- }
- }
- }
|