Initial commit
This commit is contained in:
356
Emoney Info/TermsViewController.swift
Normal file
356
Emoney Info/TermsViewController.swift
Normal file
@ -0,0 +1,356 @@
|
||||
// TermsViewController.swift
|
||||
// Emoney Info
|
||||
|
||||
import UIKit
|
||||
|
||||
final class TermsViewController: UIViewController {
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = Theme.Color.background
|
||||
setupUI()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
private func setupUI() {
|
||||
let scrollView = UIScrollView()
|
||||
let contentView = UIView()
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
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: Back + nav label
|
||||
let backButton = UIButton(type: .system)
|
||||
let chevron = UIImage(systemName: "chevron.left",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold))
|
||||
backButton.setImage(chevron, for: .normal)
|
||||
backButton.tintColor = Theme.Color.textPrimary
|
||||
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
|
||||
backButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let navLabel = UILabel()
|
||||
navLabel.text = L10n.aboutTerms
|
||||
navLabel.font = Theme.Font.caption(weight: .semibold)
|
||||
navLabel.textColor = Theme.Color.textSecondary
|
||||
navLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Last updated chip
|
||||
let updatedLabel = makePaddedLabel(text: L10n.termsLastUpdated,
|
||||
color: Theme.Color.secondary,
|
||||
bg: Theme.Color.primary.withAlphaComponent(0.15))
|
||||
|
||||
// MARK: Hero title (mixed weight)
|
||||
let heroLabel = UILabel()
|
||||
let heroAttr = NSMutableAttributedString(
|
||||
string: L10n.termsTitleRegular + " ",
|
||||
attributes: [
|
||||
.font: UIFont.systemFont(ofSize: 32, weight: .regular),
|
||||
.foregroundColor: Theme.Color.textPrimary,
|
||||
]
|
||||
)
|
||||
heroAttr.append(NSAttributedString(
|
||||
string: L10n.termsTitleBold,
|
||||
attributes: [
|
||||
.font: UIFont.systemFont(ofSize: 32, weight: .bold),
|
||||
.foregroundColor: Theme.Color.textPrimary,
|
||||
]
|
||||
))
|
||||
heroLabel.attributedText = heroAttr
|
||||
heroLabel.numberOfLines = 0
|
||||
heroLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Subtitle
|
||||
let subtitleLabel = UILabel()
|
||||
subtitleLabel.text = L10n.termsSubtitle
|
||||
subtitleLabel.font = Theme.Font.body(weight: .regular)
|
||||
subtitleLabel.textColor = Theme.Color.textSecondary
|
||||
subtitleLabel.numberOfLines = 0
|
||||
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Terms sections
|
||||
let termsStack = UIStackView()
|
||||
termsStack.axis = .vertical
|
||||
termsStack.spacing = 16
|
||||
termsStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
for (index, section) in TermsData.sections.enumerated() {
|
||||
let row = makeSectionRow(number: index + 1, section: section)
|
||||
termsStack.addArrangedSubview(row)
|
||||
}
|
||||
|
||||
// MARK: Contact card (teal)
|
||||
let contactCard = UIView()
|
||||
contactCard.backgroundColor = Theme.Color.primary
|
||||
contactCard.layer.cornerRadius = 20
|
||||
contactCard.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let contactTitle = UILabel()
|
||||
contactTitle.text = L10n.termsContactTitle
|
||||
contactTitle.font = Theme.Font.subtitle(weight: .bold)
|
||||
contactTitle.textColor = .white
|
||||
contactTitle.numberOfLines = 0
|
||||
contactTitle.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let contactDesc = UILabel()
|
||||
contactDesc.text = L10n.termsContactDesc
|
||||
contactDesc.font = Theme.Font.caption(weight: .regular)
|
||||
contactDesc.textColor = UIColor.white.withAlphaComponent(0.85)
|
||||
contactDesc.numberOfLines = 0
|
||||
contactDesc.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let contactButton = UIButton(type: .system)
|
||||
contactButton.setTitle(L10n.termsContactButton, for: .normal)
|
||||
contactButton.titleLabel?.font = Theme.Font.body(weight: .semibold)
|
||||
contactButton.setTitleColor(Theme.Color.primary, for: .normal)
|
||||
contactButton.backgroundColor = .white
|
||||
contactButton.layer.cornerRadius = 14
|
||||
contactButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 28, bottom: 12, right: 28)
|
||||
contactButton.addTarget(self, action: #selector(contactTapped), for: .touchUpInside)
|
||||
contactButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
[contactTitle, contactDesc, contactButton].forEach { contactCard.addSubview($0) }
|
||||
|
||||
// MARK: Footer
|
||||
let footerLabel = UILabel()
|
||||
footerLabel.text = L10n.footerCopyright
|
||||
footerLabel.font = Theme.Font.caption(weight: .regular)
|
||||
footerLabel.textColor = Theme.Color.textSecondary
|
||||
footerLabel.textAlignment = .center
|
||||
footerLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
[backButton, navLabel, updatedLabel, heroLabel, subtitleLabel,
|
||||
termsStack, contactCard, footerLabel].forEach { contentView.addSubview($0) }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Nav row
|
||||
backButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 56),
|
||||
backButton.leadingAnchor.constraint(equalTo: contentView.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),
|
||||
|
||||
// Chip
|
||||
updatedLabel.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 20),
|
||||
updatedLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
|
||||
// Hero
|
||||
heroLabel.topAnchor.constraint(equalTo: updatedLabel.bottomAnchor, constant: 12),
|
||||
heroLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
heroLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
|
||||
// Subtitle
|
||||
subtitleLabel.topAnchor.constraint(equalTo: heroLabel.bottomAnchor, constant: 12),
|
||||
subtitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
subtitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
|
||||
// Terms stack
|
||||
termsStack.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 28),
|
||||
termsStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
|
||||
termsStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
|
||||
|
||||
// Contact card
|
||||
contactCard.topAnchor.constraint(equalTo: termsStack.bottomAnchor, constant: 28),
|
||||
contactCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
|
||||
contactCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
|
||||
|
||||
contactTitle.topAnchor.constraint(equalTo: contactCard.topAnchor, constant: 24),
|
||||
contactTitle.leadingAnchor.constraint(equalTo: contactCard.leadingAnchor, constant: 20),
|
||||
contactTitle.trailingAnchor.constraint(equalTo: contactCard.trailingAnchor, constant: -20),
|
||||
|
||||
contactDesc.topAnchor.constraint(equalTo: contactTitle.bottomAnchor, constant: 8),
|
||||
contactDesc.leadingAnchor.constraint(equalTo: contactCard.leadingAnchor, constant: 20),
|
||||
contactDesc.trailingAnchor.constraint(equalTo: contactCard.trailingAnchor, constant: -20),
|
||||
|
||||
contactButton.topAnchor.constraint(equalTo: contactDesc.bottomAnchor, constant: 20),
|
||||
contactButton.leadingAnchor.constraint(equalTo: contactCard.leadingAnchor, constant: 20),
|
||||
contactButton.bottomAnchor.constraint(equalTo: contactCard.bottomAnchor, constant: -24),
|
||||
|
||||
// Footer
|
||||
footerLabel.topAnchor.constraint(equalTo: contactCard.bottomAnchor, constant: 24),
|
||||
footerLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
footerLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -32),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func makePaddedLabel(text: String, color: UIColor, bg: UIColor) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = Theme.Font.caption(weight: .semibold)
|
||||
label.textColor = color
|
||||
label.backgroundColor = bg
|
||||
label.layer.cornerRadius = 8
|
||||
label.clipsToBounds = true
|
||||
label.textAlignment = .center
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.heightAnchor.constraint(equalToConstant: 26).isActive = true
|
||||
return label
|
||||
}
|
||||
|
||||
private func makeSectionRow(number: Int, section: TermsSection) -> UIView {
|
||||
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
|
||||
|
||||
// Number badge
|
||||
let badge = UILabel()
|
||||
badge.text = "\(number)."
|
||||
badge.font = Theme.Font.body(weight: .bold)
|
||||
badge.textColor = Theme.Color.secondary
|
||||
badge.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = section.title
|
||||
titleLabel.font = Theme.Font.body(weight: .semibold)
|
||||
titleLabel.textColor = Theme.Color.textPrimary
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Header row
|
||||
let headerStack = UIStackView(arrangedSubviews: [badge, titleLabel])
|
||||
headerStack.axis = .horizontal
|
||||
headerStack.spacing = 8
|
||||
headerStack.alignment = .top
|
||||
headerStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Body
|
||||
let bodyLabel = UILabel()
|
||||
bodyLabel.text = section.body
|
||||
bodyLabel.font = Theme.Font.caption(weight: .regular)
|
||||
bodyLabel.textColor = Theme.Color.textSecondary
|
||||
bodyLabel.numberOfLines = 0
|
||||
bodyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Bullet points
|
||||
let bulletStack = UIStackView()
|
||||
bulletStack.axis = .vertical
|
||||
bulletStack.spacing = 6
|
||||
bulletStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
for bullet in section.bullets {
|
||||
let bulletRow = UIStackView()
|
||||
bulletRow.axis = .horizontal
|
||||
bulletRow.spacing = 8
|
||||
bulletRow.alignment = .top
|
||||
|
||||
let dot = UILabel()
|
||||
dot.text = "•"
|
||||
dot.font = Theme.Font.caption(weight: .semibold)
|
||||
dot.textColor = Theme.Color.secondary
|
||||
|
||||
let bulletLabel = UILabel()
|
||||
bulletLabel.text = bullet
|
||||
bulletLabel.font = Theme.Font.caption(weight: .regular)
|
||||
bulletLabel.textColor = Theme.Color.textSecondary
|
||||
bulletLabel.numberOfLines = 0
|
||||
|
||||
bulletRow.addArrangedSubview(dot)
|
||||
bulletRow.addArrangedSubview(bulletLabel)
|
||||
bulletStack.addArrangedSubview(bulletRow)
|
||||
}
|
||||
|
||||
let mainStack = UIStackView(arrangedSubviews: [headerStack, bodyLabel])
|
||||
if !section.bullets.isEmpty { mainStack.addArrangedSubview(bulletStack) }
|
||||
mainStack.axis = .vertical
|
||||
mainStack.spacing = 8
|
||||
mainStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
card.addSubview(mainStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
badge.widthAnchor.constraint(equalToConstant: 24),
|
||||
mainStack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
|
||||
mainStack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
||||
mainStack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
||||
mainStack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16),
|
||||
])
|
||||
|
||||
return card
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func backTapped() {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func contactTapped() {
|
||||
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: - Terms Data Model
|
||||
|
||||
struct TermsSection {
|
||||
let title: String
|
||||
let body: String
|
||||
let bullets: [String]
|
||||
}
|
||||
|
||||
enum TermsData {
|
||||
static var sections: [TermsSection] {[
|
||||
TermsSection(
|
||||
title: L10n.termsSec1Title,
|
||||
body: L10n.termsSec1Body,
|
||||
bullets: []
|
||||
),
|
||||
TermsSection(
|
||||
title: L10n.termsSec2Title,
|
||||
body: L10n.termsSec2Body,
|
||||
bullets: [
|
||||
L10n.termsSec2Bullet1,
|
||||
L10n.termsSec2Bullet2,
|
||||
L10n.termsSec2Bullet3,
|
||||
]
|
||||
),
|
||||
TermsSection(
|
||||
title: L10n.termsSec3Title,
|
||||
body: L10n.termsSec3Body,
|
||||
bullets: [
|
||||
L10n.termsSec3Bullet1,
|
||||
L10n.termsSec3Bullet2,
|
||||
]
|
||||
),
|
||||
]}
|
||||
}
|
||||
Reference in New Issue
Block a user