// FAQViewController.swift // Emoney Info // // Halaman Pusat Bantuan / FAQ // - Search bar berfungsi (filter pertanyaan real-time) // - Filter chip per kategori berfungsi // - Accordion: tap pertanyaan untuk expand/collapse jawaban import UIKit final class FAQViewController: UIViewController { // MARK: - State private var allCategories: [FAQCategory] = FAQData.all private var activeFilter: String? = nil // nil = semua kategori private var searchText: String = "" private var expandedItems: Set = [] // key = "catID|qIndex" // Computed: filtered categories based on search + filter chip private var displayedCategories: [FAQCategory] { var source = allCategories // 1. Filter by category chip if let filter = activeFilter { source = source.filter { $0.id == filter } } // 2. Filter by search text if !searchText.isEmpty { let q = searchText.lowercased() source = source.compactMap { cat in let filtered = cat.items.filter { $0.question.lowercased().contains(q) || $0.answer.lowercased().contains(q) } if filtered.isEmpty { return nil } return FAQCategory(id: cat.id, icon: cat.icon, title: cat.title, items: filtered) } } return source } // MARK: - UI private let scrollView = UIScrollView() private let contentView = UIView() private let headerView = UIView() private let backButton = UIButton(type: .system) private let titleLabel = UILabel() private let subtitleLabel = UILabel() private let searchBar = UISearchBar() // Category filter chips (horizontal scroll) private let chipScrollView = UIScrollView() private let chipStack = UIStackView() // FAQ accordion container private let faqContainerStack = UIStackView() // "Still need help?" footer card private let helpCard = UIView() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = Theme.Color.background setupScrollView() setupHeader() setupChips() setupFAQSection() setupHelpCard() reloadFAQ() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: animated) (tabBarController as? MainTabBarController)?.setTabBarHidden(true, animated: animated) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) (tabBarController as? MainTabBarController)?.setTabBarHidden(false, animated: animated) } // MARK: - Setup: ScrollView private func setupScrollView() { scrollView.showsVerticalScrollIndicator = false scrollView.keyboardDismissMode = .onDrag scrollView.translatesAutoresizingMaskIntoConstraints = false contentView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) scrollView.addSubview(contentView) NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), ]) } // MARK: - Setup: Header private func setupHeader() { // Teal rounded-bottom header card headerView.backgroundColor = Theme.Color.primary headerView.layer.cornerRadius = 24 headerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] headerView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(headerView) // Back button let chevron = UIImage(systemName: "chevron.left", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold)) backButton.setImage(chevron, for: .normal) backButton.tintColor = .white backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside) backButton.translatesAutoresizingMaskIntoConstraints = false // Header nav label "Help Center" let navLabel = UILabel() navLabel.text = L10n.helpCenterTitle navLabel.font = Theme.Font.caption(weight: .semibold) navLabel.textColor = UIColor.white.withAlphaComponent(0.85) navLabel.translatesAutoresizingMaskIntoConstraints = false // Title titleLabel.text = L10n.faqHeaderTitle titleLabel.font = Theme.Font.title(weight: .bold) titleLabel.textColor = .white titleLabel.numberOfLines = 0 titleLabel.translatesAutoresizingMaskIntoConstraints = false // Search bar searchBar.placeholder = L10n.faqSearchPlaceholder searchBar.searchBarStyle = .minimal searchBar.backgroundColor = .white searchBar.layer.cornerRadius = 12 searchBar.clipsToBounds = true searchBar.delegate = self searchBar.translatesAutoresizingMaskIntoConstraints = false if let tf = searchBar.value(forKey: "searchField") as? UITextField { tf.backgroundColor = .white tf.font = Theme.Font.body(weight: .regular) } [backButton, navLabel, titleLabel, searchBar].forEach { headerView.addSubview($0) } NSLayoutConstraint.activate([ headerView.topAnchor.constraint(equalTo: contentView.topAnchor), headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), backButton.topAnchor.constraint(equalTo: headerView.safeAreaLayoutGuide.topAnchor, constant: 16), backButton.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 20), backButton.widthAnchor.constraint(equalToConstant: 32), backButton.heightAnchor.constraint(equalToConstant: 32), navLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor), navLabel.leadingAnchor.constraint(equalTo: backButton.trailingAnchor, constant: 8), titleLabel.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 20), titleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 24), titleLabel.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -24), searchBar.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), searchBar.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 16), searchBar.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -16), searchBar.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -20), searchBar.heightAnchor.constraint(equalToConstant: 44), ]) } // MARK: - Setup: Category Chips private func setupChips() { chipScrollView.showsHorizontalScrollIndicator = false chipScrollView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(chipScrollView) chipStack.axis = .horizontal chipStack.spacing = 10 chipStack.alignment = .center chipStack.translatesAutoresizingMaskIntoConstraints = false chipScrollView.addSubview(chipStack) NSLayoutConstraint.activate([ chipScrollView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 20), chipScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), chipScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), chipScrollView.heightAnchor.constraint(equalToConstant: 44), chipStack.centerYAnchor.constraint(equalTo: chipScrollView.centerYAnchor), chipStack.leadingAnchor.constraint(equalTo: chipScrollView.leadingAnchor, constant: 20), chipStack.trailingAnchor.constraint(equalTo: chipScrollView.trailingAnchor, constant: -20), ]) // "All" chip + one per category buildChips() } private func buildChips() { chipStack.arrangedSubviews.forEach { $0.removeFromSuperview() } let allChip = makeChip(id: nil, icon: "square.grid.2x2", title: L10n.faqFilterAll) chipStack.addArrangedSubview(allChip) for cat in allCategories { let chip = makeChip(id: cat.id, icon: cat.icon, title: cat.title) chipStack.addArrangedSubview(chip) } } private func makeChip(id: String?, icon: String, title: String) -> UIView { let isActive = (id == activeFilter) let container = UIControl() container.backgroundColor = isActive ? Theme.Color.primary : .white container.layer.cornerRadius = 18 container.layer.borderWidth = isActive ? 0 : 1 container.layer.borderColor = UIColor.systemGray5.cgColor container.layer.shadowColor = UIColor.black.cgColor container.layer.shadowOpacity = isActive ? 0.08 : 0.04 container.layer.shadowOffset = CGSize(width: 0, height: 2) container.layer.shadowRadius = 4 let iconView = UIImageView(image: UIImage(systemName: icon, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .medium))) iconView.tintColor = isActive ? .white : Theme.Color.secondary iconView.contentMode = .scaleAspectFit iconView.translatesAutoresizingMaskIntoConstraints = false let label = UILabel() label.text = title label.font = Theme.Font.caption(weight: .semibold) label.textColor = isActive ? .white : Theme.Color.textPrimary label.translatesAutoresizingMaskIntoConstraints = false let stack = UIStackView(arrangedSubviews: [iconView, label]) stack.axis = .horizontal stack.spacing = 5 stack.alignment = .center stack.isUserInteractionEnabled = false stack.translatesAutoresizingMaskIntoConstraints = false container.addSubview(stack) NSLayoutConstraint.activate([ iconView.widthAnchor.constraint(equalToConstant: 14), iconView.heightAnchor.constraint(equalToConstant: 14), stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 14), stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -14), stack.centerYAnchor.constraint(equalTo: container.centerYAnchor), container.heightAnchor.constraint(equalToConstant: 36), ]) // Store filter ID in accessibilityIdentifier for lookup container.accessibilityIdentifier = id ?? "__all__" container.addTarget(self, action: #selector(chipTapped(_:)), for: .touchUpInside) return container } // MARK: - Setup: FAQ Accordion Section private func setupFAQSection() { faqContainerStack.axis = .vertical faqContainerStack.spacing = 12 faqContainerStack.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(faqContainerStack) NSLayoutConstraint.activate([ faqContainerStack.topAnchor.constraint(equalTo: chipScrollView.bottomAnchor, constant: 20), faqContainerStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), faqContainerStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), ]) } // MARK: - Setup: Help Card Footer private func setupHelpCard() { helpCard.backgroundColor = Theme.Color.primary helpCard.layer.cornerRadius = 20 helpCard.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(helpCard) let iconView = UIImageView(image: UIImage(systemName: "questionmark.bubble.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28, weight: .medium))) iconView.tintColor = .white iconView.translatesAutoresizingMaskIntoConstraints = false let helpTitle = UILabel() helpTitle.text = L10n.faqHelpCardTitle helpTitle.font = Theme.Font.subtitle(weight: .bold) helpTitle.textColor = .white helpTitle.numberOfLines = 0 helpTitle.translatesAutoresizingMaskIntoConstraints = false let helpDesc = UILabel() helpDesc.text = L10n.faqHelpCardDesc helpDesc.font = Theme.Font.caption(weight: .regular) helpDesc.textColor = UIColor.white.withAlphaComponent(0.85) helpDesc.numberOfLines = 0 helpDesc.translatesAutoresizingMaskIntoConstraints = false let emailButton = UIButton(type: .system) emailButton.setTitle(L10n.faqEmailSupport, for: .normal) emailButton.setImage(UIImage(systemName: "envelope.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .medium)), for: .normal) emailButton.tintColor = Theme.Color.primary emailButton.backgroundColor = .white emailButton.layer.cornerRadius = 14 emailButton.titleLabel?.font = Theme.Font.body(weight: .semibold) emailButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 20, bottom: 12, right: 20) emailButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: -6, bottom: 0, right: 0) emailButton.addTarget(self, action: #selector(emailTapped), for: .touchUpInside) emailButton.translatesAutoresizingMaskIntoConstraints = false [iconView, helpTitle, helpDesc, emailButton].forEach { helpCard.addSubview($0) } NSLayoutConstraint.activate([ helpCard.topAnchor.constraint(equalTo: faqContainerStack.bottomAnchor, constant: 28), helpCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), helpCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), helpCard.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -140), iconView.topAnchor.constraint(equalTo: helpCard.topAnchor, constant: 24), iconView.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 24), iconView.widthAnchor.constraint(equalToConstant: 36), iconView.heightAnchor.constraint(equalToConstant: 36), helpTitle.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 12), helpTitle.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 24), helpTitle.trailingAnchor.constraint(equalTo: helpCard.trailingAnchor, constant: -24), helpDesc.topAnchor.constraint(equalTo: helpTitle.bottomAnchor, constant: 8), helpDesc.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 24), helpDesc.trailingAnchor.constraint(equalTo: helpCard.trailingAnchor, constant: -24), emailButton.topAnchor.constraint(equalTo: helpDesc.bottomAnchor, constant: 20), emailButton.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 24), emailButton.bottomAnchor.constraint(equalTo: helpCard.bottomAnchor, constant: -24), ]) } // MARK: - Reload FAQ Accordion private func reloadFAQ() { faqContainerStack.arrangedSubviews.forEach { $0.removeFromSuperview() } let categories = displayedCategories if categories.isEmpty { let emptyLabel = UILabel() emptyLabel.text = L10n.faqNoResults emptyLabel.font = Theme.Font.body(weight: .regular) emptyLabel.textColor = Theme.Color.textSecondary emptyLabel.textAlignment = .center emptyLabel.numberOfLines = 0 faqContainerStack.addArrangedSubview(emptyLabel) return } for cat in categories { // Category header label let catLabel = UILabel() catLabel.text = cat.title.uppercased() catLabel.font = Theme.Font.caption(weight: .semibold) catLabel.textColor = Theme.Color.textSecondary faqContainerStack.addArrangedSubview(catLabel) faqContainerStack.setCustomSpacing(8, after: catLabel) // White card holding all items for this category let card = UIView() card.backgroundColor = Theme.Color.card card.layer.cornerRadius = 16 card.layer.shadowColor = UIColor.black.cgColor card.layer.shadowOpacity = 0.06 card.layer.shadowOffset = CGSize(width: 0, height: 2) card.layer.shadowRadius = 8 card.translatesAutoresizingMaskIntoConstraints = false let itemStack = UIStackView() itemStack.axis = .vertical itemStack.spacing = 0 itemStack.translatesAutoresizingMaskIntoConstraints = false card.addSubview(itemStack) NSLayoutConstraint.activate([ itemStack.topAnchor.constraint(equalTo: card.topAnchor, constant: 4), itemStack.leadingAnchor.constraint(equalTo: card.leadingAnchor), itemStack.trailingAnchor.constraint(equalTo: card.trailingAnchor), itemStack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -4), ]) for (qIndex, item) in cat.items.enumerated() { let key = "\(cat.id)|\(qIndex)" let isExpanded = expandedItems.contains(key) let row = FAQItemRow( item: item, isExpanded: isExpanded, onToggle: { [weak self] in self?.toggleItem(key: key) } ) itemStack.addArrangedSubview(row) if qIndex < cat.items.count - 1 { let sep = UIView() sep.backgroundColor = Theme.Color.background sep.translatesAutoresizingMaskIntoConstraints = false sep.heightAnchor.constraint(equalToConstant: 1).isActive = true itemStack.addArrangedSubview(sep) } } faqContainerStack.addArrangedSubview(card) faqContainerStack.setCustomSpacing(20, after: card) } } // MARK: - Toggle accordion item private func toggleItem(key: String) { if expandedItems.contains(key) { expandedItems.remove(key) } else { expandedItems.insert(key) } UIView.animate(withDuration: 0.25, delay: 0, usingSpringWithDamping: 0.85, initialSpringVelocity: 0) { self.reloadFAQ() self.view.layoutIfNeeded() } } // MARK: - Actions @objc private func backTapped() { navigationController?.popViewController(animated: true) } @objc private func chipTapped(_ sender: UIControl) { let rawID = sender.accessibilityIdentifier activeFilter = (rawID == "__all__") ? nil : rawID expandedItems.removeAll() buildChips() reloadFAQ() } @objc private func emailTapped() { let address = "apps@indonesiainyourhand.com" let subject = "Ask Support" let encoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? subject if let url = URL(string: "mailto:\(address)?subject=\(encoded)") { UIApplication.shared.open(url) } } } // MARK: - UISearchBarDelegate extension FAQViewController: UISearchBarDelegate { func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { self.searchText = searchText expandedItems.removeAll() reloadFAQ() } func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { searchBar.resignFirstResponder() } } // MARK: - FAQItemRow (Accordion Row) private final class FAQItemRow: UIView { private let questionLabel = UILabel() private let answerLabel = UILabel() private let chevron = UIImageView() var onToggle: (() -> Void)? init(item: FAQItem, isExpanded: Bool, onToggle: @escaping () -> Void) { self.onToggle = onToggle super.init(frame: .zero) setup(item: item, isExpanded: isExpanded) } required init?(coder: NSCoder) { fatalError() } private func setup(item: FAQItem, isExpanded: Bool) { // Chevron icon let chevronConfig = UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold) chevron.image = UIImage(systemName: "chevron.down", withConfiguration: chevronConfig) chevron.tintColor = Theme.Color.textSecondary chevron.contentMode = .scaleAspectFit chevron.transform = isExpanded ? CGAffineTransform(rotationAngle: .pi) : .identity chevron.translatesAutoresizingMaskIntoConstraints = false // Question questionLabel.text = item.question questionLabel.font = Theme.Font.body(weight: .medium) questionLabel.textColor = Theme.Color.textPrimary questionLabel.numberOfLines = 0 // Answer answerLabel.text = item.answer answerLabel.font = Theme.Font.caption(weight: .regular) answerLabel.textColor = Theme.Color.textSecondary answerLabel.numberOfLines = 0 answerLabel.isHidden = !isExpanded let questionStack = UIStackView(arrangedSubviews: [questionLabel, chevron]) questionStack.axis = .horizontal questionStack.spacing = 12 questionStack.alignment = .top questionStack.translatesAutoresizingMaskIntoConstraints = false let mainStack = UIStackView(arrangedSubviews: [questionStack, answerLabel]) mainStack.axis = .vertical mainStack.spacing = 10 mainStack.translatesAutoresizingMaskIntoConstraints = false addSubview(mainStack) NSLayoutConstraint.activate([ chevron.widthAnchor.constraint(equalToConstant: 16), chevron.heightAnchor.constraint(equalToConstant: 16), mainStack.topAnchor.constraint(equalTo: topAnchor, constant: 16), mainStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), mainStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), mainStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16), ]) let tap = UITapGestureRecognizer(target: self, action: #selector(rowTapped)) addGestureRecognizer(tap) isUserInteractionEnabled = true } @objc private func rowTapped() { onToggle?() } }