| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- //
- // TFAPasswordVC.swift
- // Pods
- //
- // Created by Qindi on 22/10/25.
- //
- import UIKit
- import Foundation
- import os
- import LocalAuthentication
- import nuSDKService
- class TFAPasswordVC: UIViewController {
-
- static let STEP_FIDO = "1";
- static let STEP_FIDO_PWD = "1,2";
- static let STEP_FIDO_PWD_BIOFINGER = "1,2,3";
- static let STEP_FIDO_PWD_BIOFACE = "1,2,4";
- static let STEP_FIDO_BIOFINGER = "1,3";
- static let STEP_FIDO_BIOFACE = "1,4";
-
- var STEP_NEEDED = STEP_FIDO_PWD
- var METHOD = "Sign In"
- private let imageViewBackground = UIImageView()
- private let scrollView = UIScrollView()
- private let mainStackView = UIStackView()
- private let headerImageView1 = UIImageView()
- private let headerImageView2 = UIImageView()
- private let headerTitleLabel = UILabel()
- private let subtitleLabel = UILabel()
- private let passwordTextField = UITextField()
- private let passwordVisibilityButton = UIButton(type: .system)
- private let poweredStackView = UIStackView()
- private let noteLabel = UILabel()
- private let poweredLabel = UILabel()
- private let poweredImageView = UIImageView()
- private var isPasswordVisible = false
- var isFromSU = false
- override func viewDidLoad() {
- super.viewDidLoad()
- if isFromSU {
- SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
- }
- navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit".localized(), style: .plain, target: self, action: #selector(submitAction))
- let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
- tapGesture.cancelsTouchesInView = false
- self.view.addGestureRecognizer(tapGesture)
- setupUI()
- setupLayout()
- loadData()
- updateUIBasedOnMethod()
- DispatchQueue.global().async {
- if Utils.isMiddleMode() && Utils.getBiometricState() != nil {
- self.biometricAuth()
- }
- }
- }
-
- override func viewWillAppear(_ animated: Bool) {
- let attributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 16.0), NSAttributedString.Key.foregroundColor: UIColor.white]
- let navBarAppearance = UINavigationBarAppearance()
- navBarAppearance.configureWithOpaqueBackground()
- navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : UIColor.mainColor
- navBarAppearance.titleTextAttributes = attributes
- navigationController?.navigationBar.standardAppearance = navBarAppearance
- navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance
-
- NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow),
- name: UIResponder.keyboardWillShowNotification, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide),
- name: UIResponder.keyboardWillHideNotification, object: nil)
- }
-
- override func viewWillDisappear(_ animated: Bool) {
- NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
- NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
- }
-
- @objc private func keyboardWillShow(notification: Notification) {
- guard let userInfo = notification.userInfo,
- let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
-
- let keyboardHeight = keyboardFrame.height
- let bottomInset = keyboardHeight - view.safeAreaInsets.bottom
- scrollView.contentInset.bottom = bottomInset - 20
- scrollView.verticalScrollIndicatorInsets.bottom = bottomInset - 20
-
- // ✅ Scroll password field above keyboard
- let passwordFrameInScroll = scrollView.convert(passwordTextField.frame, from: passwordTextField.superview)
- scrollView.scrollRectToVisible(passwordFrameInScroll, animated: true)
- }
- @objc private func keyboardWillHide(notification: Notification) {
- scrollView.contentInset = .zero
- scrollView.verticalScrollIndicatorInsets = .zero
- }
-
- @objc func cancel(sender: Any) {
- navigationController?.dismiss(animated: true, completion: nil)
- }
-
- @objc func dismissKeyboard() {
- passwordTextField.resignFirstResponder()
- }
- // MARK: - UI Setup
- private func setupUI() {
- view.backgroundColor = .systemBackground
- // Background Image View
- imageViewBackground.contentMode = .scaleAspectFill
- imageViewBackground.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(imageViewBackground)
- // Scroll View
- scrollView.showsVerticalScrollIndicator = false
- scrollView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(scrollView)
- // Main Stack View
- mainStackView.axis = .vertical
- mainStackView.alignment = .center
- mainStackView.spacing = 16
- mainStackView.translatesAutoresizingMaskIntoConstraints = false
- scrollView.addSubview(mainStackView)
- // Header Images
- headerImageView1.contentMode = .scaleAspectFit
- headerImageView1.image = UIImage(named: "pb_ic_attach_spc_badge", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
- headerImageView1.heightAnchor.constraint(equalToConstant: 100).isActive = true
- mainStackView.addArrangedSubview(headerImageView1)
- headerImageView2.contentMode = .scaleAspectFit
- headerImageView2.image = UIImage(named: "pb_mfa_splash", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
- headerImageView2.heightAnchor.constraint(equalToConstant: 200).isActive = true
- mainStackView.addArrangedSubview(headerImageView2)
- // Header Title Label
- headerTitleLabel.font = .boldSystemFont(ofSize: 17)
- headerTitleLabel.textAlignment = .center
- headerTitleLabel.numberOfLines = 0
- mainStackView.addArrangedSubview(headerTitleLabel)
- // Subtitle Label
- subtitleLabel.text = "Please input your password to continue"
- subtitleLabel.font = .systemFont(ofSize: 12)
- subtitleLabel.textAlignment = .center
- subtitleLabel.numberOfLines = 0
- mainStackView.addArrangedSubview(subtitleLabel)
-
- // Password Input Container
- let passwordContainerView = UIView()
- passwordContainerView.translatesAutoresizingMaskIntoConstraints = false
- passwordContainerView.widthAnchor.constraint(equalToConstant: 300).isActive = true
- passwordContainerView.heightAnchor.constraint(equalToConstant: 48).isActive = true
- mainStackView.addArrangedSubview(passwordContainerView)
-
- // Password Text Field
- passwordTextField.placeholder = "Type your password..."
- passwordTextField.isSecureTextEntry = true
- passwordTextField.font = .systemFont(ofSize: 15)
- passwordTextField.borderStyle = .roundedRect
- passwordTextField.keyboardType = .default
- passwordTextField.autocapitalizationType = .none
- passwordTextField.autocorrectionType = .no
- passwordTextField.translatesAutoresizingMaskIntoConstraints = false
- passwordContainerView.addSubview(passwordTextField)
-
- // Password Visibility Button
- passwordVisibilityButton.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
- passwordVisibilityButton.addTarget(self, action: #selector(togglePasswordVisibility), for: .touchUpInside)
- passwordVisibilityButton.translatesAutoresizingMaskIntoConstraints = false
- passwordVisibilityButton.tintColor = .black
- passwordContainerView.addSubview(passwordVisibilityButton)
-
- if isFromSU {
- print("MASUK SINI GA?")
- noteLabel.attributedText = "_Note: If you have not changed the password provided by this user or are unsure of it, please note that the default password is *abcd1234*_".localized().richText()
- noteLabel.numberOfLines = 0
- mainStackView.addArrangedSubview(noteLabel)
- }
-
-
- mainStackView.setCustomSpacing(12, after: subtitleLabel)
- mainStackView.setCustomSpacing(24, after: passwordContainerView)
- // Powered By StackView
- poweredStackView.axis = .horizontal
- poweredStackView.alignment = .center
- poweredStackView.spacing = 8
- poweredStackView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(poweredStackView)
- poweredLabel.text = "Powered by"
- poweredLabel.font = .systemFont(ofSize: 12)
- poweredStackView.addArrangedSubview(poweredLabel)
- poweredImageView.contentMode = .scaleAspectFit
- poweredImageView.image = UIImage(named: "pb_powered_button")
- poweredImageView.widthAnchor.constraint(equalToConstant: 25).isActive = true
- poweredImageView.heightAnchor.constraint(equalToConstant: 25).isActive = true
- poweredStackView.addArrangedSubview(poweredImageView)
- }
- private func setupLayout() {
- NSLayoutConstraint.activate([
- // Background
- imageViewBackground.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
- imageViewBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- imageViewBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- imageViewBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- // Scroll View
- scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
- scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
- scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
- scrollView.bottomAnchor.constraint(equalTo: poweredStackView.topAnchor, constant: -8),
- // Main Stack View
- mainStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
- mainStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
- mainStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
- mainStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
- mainStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
-
- // Password Field
- passwordTextField.leadingAnchor.constraint(equalTo: passwordTextField.superview!.leadingAnchor),
- passwordTextField.trailingAnchor.constraint(equalTo: passwordTextField.superview!.trailingAnchor),
- passwordTextField.topAnchor.constraint(equalTo: passwordTextField.superview!.topAnchor),
- passwordTextField.bottomAnchor.constraint(equalTo: passwordTextField.superview!.bottomAnchor),
-
- // Password Visibility Button
- passwordVisibilityButton.trailingAnchor.constraint(equalTo: passwordTextField.trailingAnchor, constant: -8),
- passwordVisibilityButton.centerYAnchor.constraint(equalTo: passwordTextField.centerYAnchor),
- passwordVisibilityButton.widthAnchor.constraint(equalToConstant: 40),
- passwordVisibilityButton.heightAnchor.constraint(equalToConstant: 40),
- // Powered by
- poweredStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
- poweredStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8),
- ])
- }
-
- // MARK: - Data & Logic
- private func loadData() {
- let poweredText = "Nexilis"
- if !poweredText.isEmpty {
- poweredLabel.text = "Powered by \(poweredText)"
- }
- }
-
- // MARK: - Actions
- @objc private func togglePasswordVisibility() {
- isPasswordVisible.toggle()
- passwordTextField.isSecureTextEntry = !isPasswordVisible
- let iconName = isPasswordVisible ? "eye.fill" : "eye.slash.fill"
- passwordVisibilityButton.setImage(UIImage(systemName: iconName), for: .normal)
- }
- @objc private func submitAction() {
- guard let password = passwordTextField.text, !password.trimmingCharacters(in: .whitespaces).isEmpty else {
- self.view.makeToast("Password cannot be empty.".localized(), duration: 2.0, position: .center)
- return
- }
- guard password.count >= 6 else {
- self.view.makeToast("Password must be at least 6 characters.".localized(), duration: 2.0, position: .center)
- return
- }
- submit()
- }
- private func submit(fromBiometric: Bool = false) {
- guard let password = passwordTextField.text else { return }
- Nexilis.showLoader()
-
- DispatchQueue.global().async {
- do {
- // 1. Encrypt password
- let encryptedPwd = password
-
- // 2. Create message for the server
- let me = User.getMyPin() ?? ""
- let tMessage = CoreMessage_TMessageBank.getMFAValidation(data: me)
- if !fromBiometric {
- tMessage.mBodies[CoreMessage_TMessageKey.PSWD] = encryptedPwd
- }
- tMessage.mBodies[CoreMessage_TMessageKey.SUBMIT_DATE] = "\(Date().currentTimeMillis())"
- tMessage.mBodies[CoreMessage_TMessageKey.ACTVITY] = self.METHOD
- guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: Utils.isHSAMode()) else {
- DispatchQueue.main.async {
- Nexilis.hideLoader {
- let errorMessage = "Biometric Failed".localized()
- let dialog = DialogErrorMFA()
- dialog.modalTransitionStyle = .crossDissolve
- dialog.modalPresentationStyle = .overCurrentContext
- dialog.errorDesc = errorMessage
- dialog.method = self.METHOD
- UIApplication.shared.visibleViewController?.present(dialog, animated: true)
- }
- }
- return
- }
- var id = ""
- if Utils.isMiddleMode() || Utils.isHSAMode() {
- id = Nexilis.justInit()
- } else {
- id = User.getMyPin() ?? ""
- }
- if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
- if response.isOk() {
- let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
- if data.isEmpty {
- DispatchQueue.main.async {
- Nexilis.hideLoader {
- let errorMessage = "Auth Failure".localized()
- let dialog = DialogErrorMFA()
- dialog.modalTransitionStyle = .crossDissolve
- dialog.modalPresentationStyle = .overCurrentContext
- dialog.errorDesc = errorMessage
- dialog.method = self.METHOD
- UIApplication.shared.visibleViewController?.present(dialog, animated: true)
- }
- }
- return
- }
- let df = HMACDeviceFingerprintNexilis.generate()
- tMessage.mBodies[CoreMessage_TMessageKey.FINGERPRINT] = df
- var sign = ""
- if let dataSign = "\(data)!\(df)".data(using: .utf8) {
- if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
- sign = signature.base64EncodedString()
- }
- }
- tMessage.mBodies[CoreMessage_TMessageKey.SIGNATURE] = sign
- let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
- tMessage.mBodies[CoreMessage_TMessageKey.TOTP] = otp
- if let response = Nexilis.writeAndWait(message: tMessage) {
- if response.isOk() {
- if Utils.isMiddleMode() && Utils.getBiometricState() == nil {
- SecureUserDefaults.shared.set(Date(), forKey: "lastAuthenticationTime")
- }
- Nexilis.setInitCallback() { res in
- if res == 1 {
- Nexilis.successSui?()
- closePage()
- }
- }
- Nexilis.startConnect(withInit: false)
- func closePage() {
- DispatchQueue.main.async {
- Nexilis.hideLoader {
- self.navigationController?.dismiss(animated: true, completion: {
- UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
- self.dismissKeyboard()
- })
- }
- }
- }
- }
- else {
- DispatchQueue.main.async {
- Nexilis.hideLoader {
- let errorMessage = response.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT, default_value: "Auth Failure".localized())
- let dialog = DialogErrorMFA()
- dialog.modalTransitionStyle = .crossDissolve
- dialog.modalPresentationStyle = .overCurrentContext
- dialog.errorDesc = errorMessage
- dialog.method = self.METHOD
- UIApplication.shared.visibleViewController?.present(dialog, animated: true)
- }
- }
- }
- }
- }
- } else {
- DispatchQueue.main.async {
- Nexilis.hideLoader {
- let errorMessage = "Unable to access servers. Check your internet connection and try again later".localized()
- let dialog = DialogErrorMFA()
- dialog.modalTransitionStyle = .crossDissolve
- dialog.modalPresentationStyle = .overCurrentContext
- dialog.errorDesc = errorMessage
- dialog.method = self.METHOD
- UIApplication.shared.visibleViewController?.present(dialog, animated: true)
- }
- }
- }
- } catch {
-
- }
- }
- }
- private func biometricAuth() {
- let semaphore = DispatchSemaphore(value: 0)
- var result = true
- var stateErr = 0
- let manager = BiometricStateManager()
- manager.hasBiometricStateChanged { (res, state) in
- result = res
- stateErr = state
- semaphore.signal()
- }
-
- semaphore.wait()
- if result {
- if Utils.isMiddleMode() {
- DispatchQueue.main.async {
- self.submit(fromBiometric: true)
- }
- }
- } else if stateErr == 1 {
- DispatchQueue.main.async {
- Nexilis.hideLoader {
- let errorMessage = "Terjadi Perubahan Biometric (Touch/Face ID)"
- let dialog = DialogErrorMFA()
- dialog.modalTransitionStyle = .crossDissolve
- dialog.modalPresentationStyle = .overCurrentContext
- dialog.errorDesc = errorMessage
- dialog.method = self.METHOD
- dialog.hideTryAgain = (stateErr == 1)
- UIApplication.shared.visibleViewController?.present(dialog, animated: true)
- }
- }
- }
- }
-
- private func updateUIBasedOnMethod() {
- headerTitleLabel.text = METHOD
- }
- }
|