Initial commit
This commit is contained in:
670
Emoney Info/HomeView.swift
Normal file
670
Emoney Info/HomeView.swift
Normal file
@ -0,0 +1,670 @@
|
||||
import UIKit
|
||||
import GoogleMobileAds
|
||||
import Toast_Swift
|
||||
|
||||
final class HomeViewController: UIViewController {
|
||||
|
||||
// MARK: - Public state
|
||||
|
||||
var balanceText: String = "Rp 0" {
|
||||
didSet { balanceLabel.text = balanceText }
|
||||
}
|
||||
var cardTypeText: String = "E-Money Card" {
|
||||
didSet { cardTypeLabel.text = cardTypeText }
|
||||
}
|
||||
var lastTransaction: LastTransactionItem? {
|
||||
didSet { configureLastTransaction() }
|
||||
}
|
||||
|
||||
// MARK: - Callbacks
|
||||
|
||||
var onScanTapped: (() -> Void)?
|
||||
var onViewHistoryTapped: (() -> Void)?
|
||||
var onSettingsTapped: (() -> Void)?
|
||||
|
||||
// Diisi setelah scan berhasil, dipakai oleh SceneDelegate untuk history
|
||||
var latestRiwayatList: [RiwayatCard] = [] {
|
||||
didSet { updateViewHistoryButtonState() }
|
||||
}
|
||||
var latestCardNumber: String = "" // raw card number for PDF export
|
||||
|
||||
// MARK: - UI Elements
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentView = UIView()
|
||||
|
||||
// Header
|
||||
private let appNameLabel = UILabel()
|
||||
private let settingsButton = UIButton(type: .system)
|
||||
|
||||
// Balance
|
||||
private let availableLabel = UILabel()
|
||||
private let balanceLabel = UILabel()
|
||||
|
||||
// Card
|
||||
private let cardView = UIView()
|
||||
private let cardGradient = CAGradientLayer()
|
||||
private let nfcIconView = UIImageView()
|
||||
private let tapCardLabel = UILabel()
|
||||
private let cardNumberLabel = UILabel()
|
||||
private let copyButton = UIButton(type: .system)
|
||||
private let cardTypeLabel = UILabel()
|
||||
|
||||
// Raw card number stored after scan for re-formatting on setting change
|
||||
private var rawCardNumber: String = ""
|
||||
|
||||
// Instruction
|
||||
private let tapHereLabel = UILabel()
|
||||
private let tapHintLabel = UILabel()
|
||||
|
||||
// Scan button
|
||||
private let scanButton = UIButton(type: .system)
|
||||
|
||||
// Ads
|
||||
private let promoCard = UIView()
|
||||
private var bannerView = GADBannerView()
|
||||
|
||||
// Dynamic layout: toggle based on ad load state
|
||||
private var lastTxTopNoAd: NSLayoutConstraint!
|
||||
private var lastTxTopWithAd: NSLayoutConstraint!
|
||||
|
||||
// Last transaction
|
||||
private let lastTxHeader = UILabel()
|
||||
private let lastTxCard = UIView()
|
||||
private let txIconView = UIView()
|
||||
private let txIconImage = UIImageView()
|
||||
private let txTitleLabel = UILabel()
|
||||
private let txDateLabel = UILabel()
|
||||
private let txAmountLabel = UILabel()
|
||||
private let txStatusLabel = UILabel()
|
||||
|
||||
// Footer link
|
||||
private let viewHistoryButton = UIButton(type: .system)
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = Theme.Color.background
|
||||
setupScrollView()
|
||||
setupHeader()
|
||||
setupBalanceSection()
|
||||
setupCard()
|
||||
setupInstruction()
|
||||
setupScanButton()
|
||||
setupPromo()
|
||||
setupLastTransaction()
|
||||
setupViewHistoryButton()
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(onSettingChanged),
|
||||
name: Notification.Name("refreshScreen"),
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
NotificationCenter.default.removeObserver(self, name: Notification.Name("refreshScreen"), object: nil)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
cardGradient.frame = cardView.bounds
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupScrollView() {
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentView)
|
||||
}
|
||||
|
||||
private func setupHeader() {
|
||||
appNameLabel.text = "Emoney Info"
|
||||
appNameLabel.font = Theme.Font.title(weight: .bold)
|
||||
appNameLabel.textColor = Theme.Color.textPrimary
|
||||
|
||||
let gearImage = UIImage(systemName: "gearshape", withConfiguration:
|
||||
UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))
|
||||
settingsButton.setImage(gearImage, for: .normal)
|
||||
settingsButton.tintColor = Theme.Color.textPrimary
|
||||
settingsButton.addTarget(self, action: #selector(settingsTapped), for: .touchUpInside)
|
||||
|
||||
[appNameLabel, settingsButton].forEach {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview($0)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupBalanceSection() {
|
||||
availableLabel.text = L10n.availableBalance
|
||||
availableLabel.font = Theme.Font.caption(weight: .semibold)
|
||||
availableLabel.textColor = Theme.Color.textSecondary
|
||||
availableLabel.letterSpacing(1.5)
|
||||
|
||||
balanceLabel.text = "Rp 0"
|
||||
balanceLabel.font = .systemFont(ofSize: 36, weight: .bold)
|
||||
balanceLabel.textColor = Theme.Color.textPrimary
|
||||
|
||||
[availableLabel, balanceLabel].forEach {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview($0)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupCard() {
|
||||
cardView.layer.cornerRadius = 24
|
||||
cardView.clipsToBounds = true
|
||||
|
||||
cardGradient.colors = [
|
||||
UIColor(red: 0.48, green: 0.83, blue: 0.82, alpha: 1).cgColor,
|
||||
UIColor(red: 0.36, green: 0.49, blue: 0.48, alpha: 1).cgColor
|
||||
]
|
||||
cardGradient.startPoint = CGPoint(x: 0, y: 0)
|
||||
cardGradient.endPoint = CGPoint(x: 1, y: 1)
|
||||
cardView.layer.insertSublayer(cardGradient, at: 0)
|
||||
|
||||
let nfcConfig = UIImage.SymbolConfiguration(pointSize: 48, weight: .medium)
|
||||
nfcIconView.image = UIImage(systemName: "wave.3.right.circle.fill", withConfiguration: nfcConfig)
|
||||
nfcIconView.tintColor = .white.withAlphaComponent(0.9)
|
||||
nfcIconView.contentMode = .scaleAspectFit
|
||||
|
||||
tapCardLabel.text = L10n.cardTapInstruction
|
||||
tapCardLabel.font = Theme.Font.caption(weight: .bold)
|
||||
tapCardLabel.textColor = .white.withAlphaComponent(0.85)
|
||||
tapCardLabel.letterSpacing(2)
|
||||
|
||||
cardTypeLabel.text = cardTypeText
|
||||
cardTypeLabel.font = Theme.Font.caption(weight: .regular)
|
||||
cardTypeLabel.textColor = .white.withAlphaComponent(0.7)
|
||||
|
||||
cardNumberLabel.font = .systemFont(ofSize: 11, weight: .medium)
|
||||
cardNumberLabel.textColor = .white.withAlphaComponent(0.85)
|
||||
cardNumberLabel.isHidden = true
|
||||
|
||||
let copyConfig = UIImage.SymbolConfiguration(pointSize: 11, weight: .medium)
|
||||
copyButton.setImage(UIImage(systemName: "doc.on.doc", withConfiguration: copyConfig), for: .normal)
|
||||
copyButton.tintColor = .white.withAlphaComponent(0.75)
|
||||
copyButton.isHidden = true
|
||||
copyButton.addTarget(self, action: #selector(copyCardNumber), for: .touchUpInside)
|
||||
|
||||
[cardView, nfcIconView, tapCardLabel, cardNumberLabel, copyButton, cardTypeLabel].forEach {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
contentView.addSubview(cardView)
|
||||
cardView.addSubview(nfcIconView)
|
||||
cardView.addSubview(tapCardLabel)
|
||||
cardView.addSubview(cardNumberLabel)
|
||||
cardView.addSubview(copyButton)
|
||||
cardView.addSubview(cardTypeLabel)
|
||||
}
|
||||
|
||||
private func setupInstruction() {
|
||||
tapHereLabel.isHidden = true
|
||||
|
||||
tapHintLabel.text = L10n.tapCardHint
|
||||
tapHintLabel.font = Theme.Font.body(weight: .regular)
|
||||
tapHintLabel.textColor = Theme.Color.textSecondary
|
||||
tapHintLabel.textAlignment = .center
|
||||
tapHintLabel.numberOfLines = 2
|
||||
|
||||
[tapHereLabel, tapHintLabel].forEach {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview($0)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupScanButton() {
|
||||
scanButton.setTitle(L10n.checkBalance, for: .normal)
|
||||
scanButton.titleLabel?.font = Theme.Font.body(weight: .semibold)
|
||||
scanButton.setTitleColor(.white, for: .normal)
|
||||
scanButton.backgroundColor = Theme.Color.secondary
|
||||
scanButton.layer.cornerRadius = 14
|
||||
scanButton.addTarget(self, action: #selector(scanTapped), for: .touchUpInside)
|
||||
scanButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(scanButton)
|
||||
}
|
||||
|
||||
private func setupPromo() {
|
||||
// TODO: Replace ad unit ID before release
|
||||
let adUnitID = "ca-app-pub-3389368171983845/5892672309"
|
||||
|
||||
let adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(
|
||||
UIScreen.main.bounds.width - 48 // matches horizontal padding 24pt each side
|
||||
)
|
||||
bannerView = GADBannerView(adSize: adSize)
|
||||
bannerView.adUnitID = adUnitID
|
||||
bannerView.rootViewController = self
|
||||
bannerView.delegate = self
|
||||
bannerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
promoCard.backgroundColor = .clear
|
||||
promoCard.translatesAutoresizingMaskIntoConstraints = false
|
||||
promoCard.clipsToBounds = true
|
||||
promoCard.isHidden = true // shown only when ad loads successfully
|
||||
promoCard.addSubview(bannerView)
|
||||
contentView.addSubview(promoCard)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
bannerView.topAnchor.constraint(equalTo: promoCard.topAnchor),
|
||||
bannerView.leadingAnchor.constraint(equalTo: promoCard.leadingAnchor),
|
||||
bannerView.trailingAnchor.constraint(equalTo: promoCard.trailingAnchor),
|
||||
bannerView.bottomAnchor.constraint(equalTo: promoCard.bottomAnchor),
|
||||
bannerView.heightAnchor.constraint(equalToConstant: CGFloat(adSize.size.height))
|
||||
])
|
||||
|
||||
// Ad is loaded externally via loadBannerAd() after GADMobileAds SDK has started.
|
||||
}
|
||||
|
||||
func loadBannerAd() {
|
||||
bannerView.load(GADRequest())
|
||||
}
|
||||
|
||||
private func setupLastTransaction() {
|
||||
lastTxHeader.text = L10n.lastTransaction
|
||||
lastTxHeader.font = Theme.Font.subtitle(weight: .bold)
|
||||
lastTxHeader.textColor = Theme.Color.textPrimary
|
||||
lastTxHeader.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(lastTxHeader)
|
||||
|
||||
lastTxCard.backgroundColor = Theme.Color.card
|
||||
lastTxCard.layer.cornerRadius = 16
|
||||
lastTxCard.layer.shadowColor = UIColor.black.cgColor
|
||||
lastTxCard.layer.shadowOpacity = 0.06
|
||||
lastTxCard.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
lastTxCard.layer.shadowRadius = 8
|
||||
lastTxCard.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(lastTxCard)
|
||||
|
||||
// Transaction icon
|
||||
txIconView.backgroundColor = Theme.Color.background
|
||||
txIconView.layer.cornerRadius = 12
|
||||
|
||||
txIconImage.tintColor = Theme.Color.secondary
|
||||
txIconImage.contentMode = .scaleAspectFit
|
||||
txIconImage.translatesAutoresizingMaskIntoConstraints = false
|
||||
txIconView.addSubview(txIconImage)
|
||||
|
||||
txTitleLabel.font = Theme.Font.body(weight: .semibold)
|
||||
txTitleLabel.textColor = Theme.Color.textPrimary
|
||||
|
||||
txDateLabel.font = Theme.Font.caption(weight: .regular)
|
||||
txDateLabel.textColor = Theme.Color.textSecondary
|
||||
|
||||
txAmountLabel.font = Theme.Font.body(weight: .bold)
|
||||
txAmountLabel.textColor = UIColor.systemRed
|
||||
txAmountLabel.textAlignment = .right
|
||||
|
||||
txStatusLabel.font = Theme.Font.caption(weight: .semibold)
|
||||
txStatusLabel.textColor = Theme.Color.success
|
||||
txStatusLabel.textAlignment = .right
|
||||
|
||||
let leftStack = UIStackView(arrangedSubviews: [txTitleLabel, txDateLabel])
|
||||
leftStack.axis = .vertical
|
||||
leftStack.spacing = 2
|
||||
|
||||
let rightStack = UIStackView(arrangedSubviews: [txAmountLabel, txStatusLabel])
|
||||
rightStack.axis = .vertical
|
||||
rightStack.spacing = 2
|
||||
rightStack.alignment = .trailing
|
||||
|
||||
let hStack = UIStackView(arrangedSubviews: [txIconView, leftStack, rightStack])
|
||||
hStack.axis = .horizontal
|
||||
hStack.spacing = 12
|
||||
hStack.alignment = .center
|
||||
|
||||
[txIconView, txIconImage, hStack, leftStack, rightStack].forEach {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
lastTxCard.addSubview(hStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
txIconImage.centerXAnchor.constraint(equalTo: txIconView.centerXAnchor),
|
||||
txIconImage.centerYAnchor.constraint(equalTo: txIconView.centerYAnchor),
|
||||
txIconImage.widthAnchor.constraint(equalToConstant: 20),
|
||||
txIconImage.heightAnchor.constraint(equalToConstant: 20),
|
||||
txIconView.widthAnchor.constraint(equalToConstant: 44),
|
||||
txIconView.heightAnchor.constraint(equalToConstant: 44),
|
||||
|
||||
hStack.topAnchor.constraint(equalTo: lastTxCard.topAnchor, constant: 16),
|
||||
hStack.leadingAnchor.constraint(equalTo: lastTxCard.leadingAnchor, constant: 16),
|
||||
hStack.trailingAnchor.constraint(equalTo: lastTxCard.trailingAnchor, constant: -16),
|
||||
hStack.bottomAnchor.constraint(equalTo: lastTxCard.bottomAnchor, constant: -16),
|
||||
|
||||
rightStack.widthAnchor.constraint(greaterThanOrEqualToConstant: 80)
|
||||
])
|
||||
|
||||
configureLastTransaction()
|
||||
}
|
||||
|
||||
private func setupViewHistoryButton() {
|
||||
viewHistoryButton.setTitle("\(L10n.viewFullHistory) →", for: .normal)
|
||||
viewHistoryButton.titleLabel?.font = Theme.Font.body(weight: .semibold)
|
||||
viewHistoryButton.addTarget(self, action: #selector(viewHistoryTapped), for: .touchUpInside)
|
||||
viewHistoryButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(viewHistoryButton)
|
||||
updateViewHistoryButtonState()
|
||||
}
|
||||
|
||||
private func updateViewHistoryButtonState() {
|
||||
let hasData = !latestRiwayatList.isEmpty
|
||||
viewHistoryButton.isEnabled = hasData
|
||||
viewHistoryButton.setTitleColor(hasData ? Theme.Color.secondary : Theme.Color.textSecondary, for: .normal)
|
||||
viewHistoryButton.alpha = hasData ? 1.0 : 0.4
|
||||
}
|
||||
|
||||
// MARK: - Constraints
|
||||
|
||||
private func setupConstraints() {
|
||||
NSLayoutConstraint.activate([
|
||||
|
||||
// ScrollView
|
||||
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
// ContentView
|
||||
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),
|
||||
|
||||
// Header
|
||||
appNameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
|
||||
appNameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
|
||||
settingsButton.centerYAnchor.constraint(equalTo: appNameLabel.centerYAnchor),
|
||||
settingsButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
settingsButton.widthAnchor.constraint(equalToConstant: 32),
|
||||
settingsButton.heightAnchor.constraint(equalToConstant: 32),
|
||||
|
||||
// Balance
|
||||
availableLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: 28),
|
||||
availableLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
|
||||
balanceLabel.topAnchor.constraint(equalTo: availableLabel.bottomAnchor, constant: 4),
|
||||
balanceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
|
||||
// Card
|
||||
cardView.topAnchor.constraint(equalTo: balanceLabel.bottomAnchor, constant: 20),
|
||||
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
cardView.heightAnchor.constraint(equalToConstant: 160),
|
||||
|
||||
nfcIconView.centerXAnchor.constraint(equalTo: cardView.centerXAnchor),
|
||||
nfcIconView.centerYAnchor.constraint(equalTo: cardView.centerYAnchor, constant: -10),
|
||||
nfcIconView.widthAnchor.constraint(equalToConstant: 60),
|
||||
nfcIconView.heightAnchor.constraint(equalToConstant: 60),
|
||||
|
||||
tapCardLabel.topAnchor.constraint(equalTo: nfcIconView.bottomAnchor, constant: 8),
|
||||
tapCardLabel.centerXAnchor.constraint(equalTo: cardView.centerXAnchor),
|
||||
|
||||
// card number + copy — bottom left
|
||||
cardNumberLabel.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -14),
|
||||
cardNumberLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
|
||||
|
||||
copyButton.centerYAnchor.constraint(equalTo: cardNumberLabel.centerYAnchor),
|
||||
copyButton.leadingAnchor.constraint(equalTo: cardNumberLabel.trailingAnchor, constant: 6),
|
||||
copyButton.widthAnchor.constraint(equalToConstant: 20),
|
||||
copyButton.heightAnchor.constraint(equalToConstant: 20),
|
||||
|
||||
// card type — bottom right
|
||||
cardTypeLabel.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -14),
|
||||
cardTypeLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16),
|
||||
|
||||
// Instruction
|
||||
tapHereLabel.topAnchor.constraint(equalTo: cardView.bottomAnchor, constant: 20),
|
||||
tapHereLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
|
||||
tapHintLabel.topAnchor.constraint(equalTo: tapHereLabel.bottomAnchor, constant: 6),
|
||||
tapHintLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
tapHintLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40),
|
||||
tapHintLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40),
|
||||
|
||||
// Scan Button
|
||||
scanButton.topAnchor.constraint(equalTo: tapHintLabel.bottomAnchor, constant: 20),
|
||||
scanButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
scanButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
scanButton.heightAnchor.constraint(equalToConstant: 52),
|
||||
|
||||
// Promo
|
||||
promoCard.topAnchor.constraint(equalTo: scanButton.bottomAnchor, constant: 28),
|
||||
promoCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
promoCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
|
||||
// Last Transaction leading (top is toggled dynamically)
|
||||
lastTxHeader.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
|
||||
lastTxCard.topAnchor.constraint(equalTo: lastTxHeader.bottomAnchor, constant: 12),
|
||||
lastTxCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
lastTxCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
|
||||
// View History
|
||||
viewHistoryButton.topAnchor.constraint(equalTo: lastTxCard.bottomAnchor, constant: 12),
|
||||
viewHistoryButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
viewHistoryButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -140)
|
||||
])
|
||||
|
||||
// Dynamic top constraints for Last Transaction — toggled by ad load state
|
||||
lastTxTopNoAd = lastTxHeader.topAnchor.constraint(equalTo: scanButton.bottomAnchor, constant: 28)
|
||||
lastTxTopWithAd = lastTxHeader.topAnchor.constraint(equalTo: promoCard.bottomAnchor, constant: 28)
|
||||
lastTxTopNoAd.isActive = true // default: no ad
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func scanTapped() {
|
||||
latestRiwayatList = []
|
||||
latestCardNumber = ""
|
||||
lastTransaction = nil
|
||||
onScanTapped?()
|
||||
}
|
||||
|
||||
@objc private func viewHistoryTapped() {
|
||||
onViewHistoryTapped?()
|
||||
}
|
||||
|
||||
@objc private func settingsTapped() {
|
||||
onSettingsTapped?()
|
||||
}
|
||||
|
||||
@objc private func copyCardNumber() {
|
||||
guard !rawCardNumber.isEmpty else { return }
|
||||
UIPasteboard.general.string = rawCardNumber
|
||||
|
||||
var style = ToastStyle()
|
||||
style.backgroundColor = Theme.Color.primary
|
||||
style.messageColor = .white
|
||||
style.messageFont = Theme.Font.body(weight: .semibold)
|
||||
style.cornerRadius = 12
|
||||
|
||||
view.makeToast(L10n.copiedToClipboard, duration: 2.0, position: .top, style: style)
|
||||
}
|
||||
|
||||
@objc private func onSettingChanged() {
|
||||
guard !rawCardNumber.isEmpty else { return }
|
||||
updateCardNumberDisplay()
|
||||
}
|
||||
|
||||
// MARK: - Card number display
|
||||
|
||||
private func updateCardNumberDisplay() {
|
||||
// "Show Card Number on Home" toggle maps to key "masked"
|
||||
// isOn = true → show full number
|
||||
// isOn = false → mask first 12 digits
|
||||
let showFull = UserDefaults.standard.bool(forKey: "masked")
|
||||
cardNumberLabel.text = showFull ? rawCardNumber.formatCardNumber() : rawCardNumber.maskFirst12()
|
||||
cardNumberLabel.isHidden = false
|
||||
copyButton.isHidden = false
|
||||
}
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
private func configureLastTransaction() {
|
||||
guard let tx = lastTransaction else {
|
||||
txTitleLabel.text = "–"
|
||||
txDateLabel.text = "–"
|
||||
txAmountLabel.text = "–"
|
||||
txStatusLabel.text = "–"
|
||||
txIconImage.image = UIImage(systemName: "creditcard.fill")
|
||||
return
|
||||
}
|
||||
|
||||
let hasPlace = !tx.place.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
|
||||
// Title: gunakan place jika ada, fallback ke status topup/payment
|
||||
txTitleLabel.text = hasPlace ? tx.place : (tx.isCredit ? L10n.topup : L10n.payment)
|
||||
txTitleLabel.numberOfLines = 2
|
||||
|
||||
// Icon: tram hanya untuk KMT, selain itu pakai arrow.down/creditcard
|
||||
let iconName: String
|
||||
if tx.isKMT {
|
||||
iconName = "tram.fill"
|
||||
} else {
|
||||
iconName = tx.isCredit ? "arrow.down.circle.fill" : "creditcard.fill"
|
||||
}
|
||||
txIconImage.image = UIImage(systemName: iconName)
|
||||
|
||||
txDateLabel.text = tx.date
|
||||
txAmountLabel.text = tx.amount
|
||||
txStatusLabel.text = tx.status.uppercased()
|
||||
txStatusLabel.textColor = tx.isCredit ? Theme.Color.success : UIColor.systemRed
|
||||
txAmountLabel.textColor = tx.isCredit ? Theme.Color.success : UIColor.systemRed
|
||||
if (!hasPlace){
|
||||
txStatusLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data Model
|
||||
|
||||
struct LastTransactionItem {
|
||||
let place: String // locationName, bisa kosong
|
||||
let date: String
|
||||
let amount: String
|
||||
let status: String
|
||||
let isCredit: Bool
|
||||
let isKMT: Bool
|
||||
}
|
||||
|
||||
// MARK: - GADBannerViewDelegate
|
||||
|
||||
extension HomeViewController: GADBannerViewDelegate {
|
||||
func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
|
||||
lastTxTopNoAd.isActive = false
|
||||
lastTxTopWithAd.isActive = true
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.promoCard.isHidden = false
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) {
|
||||
lastTxTopWithAd.isActive = false
|
||||
lastTxTopNoAd.isActive = true
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.promoCard.isHidden = true
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ApduCallback
|
||||
|
||||
import CoreNFC
|
||||
|
||||
extension HomeViewController: ApduCallback {
|
||||
func felicaConnected(unifiedNfcApi: UnifiedNfcApi, tag:NFCFeliCaTag) {
|
||||
debugLog("felica nih.")
|
||||
unifiedNfcApi.checkFelicaCard(tag: tag)
|
||||
}
|
||||
|
||||
func connected(unifiedNfcApi: UnifiedNfcApi) {
|
||||
debugLog("normal nih.")
|
||||
unifiedNfcApi.checkCard()
|
||||
}
|
||||
|
||||
func complete(emoney: Emoney) {
|
||||
DispatchQueue.main.async {
|
||||
self.balanceText = Self.idrCurrencyFormatter.string(for: emoney.getBalance()) ?? "Rp 0"
|
||||
self.cardTypeText = emoney.getCardLabel()
|
||||
|
||||
self.rawCardNumber = emoney.getCardNumber()
|
||||
self.latestCardNumber = emoney.getCardNumber()
|
||||
self.updateCardNumberDisplay()
|
||||
|
||||
if (emoney.isTampilRiwayat()){
|
||||
let riwayat = emoney.getRiwayatList()
|
||||
if let first = riwayat.first {
|
||||
let dateFmt = DateFormatter()
|
||||
dateFmt.dateFormat = "dd MMM yyyy, HH:mm"
|
||||
let dateStr = first.getTransationTime().map { dateFmt.string(from: $0) } ?? "–"
|
||||
let isCredit = first.getProsesTipe() == 0
|
||||
let sign = isCredit ? "+" : "-"
|
||||
let place = first.getLocationName()?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let isKMT = emoney.getCardLabel().uppercased().contains("KMT")
|
||||
self.lastTransaction = LastTransactionItem(
|
||||
place: place,
|
||||
date: dateStr,
|
||||
amount: "\(sign)\(Self.idrCurrencyFormatter.string(for: first.getAmount()) ?? "Rp 0")",
|
||||
status: isCredit ? L10n.topup : L10n.payment,
|
||||
isCredit: isCredit,
|
||||
isKMT: isKMT
|
||||
)
|
||||
}
|
||||
|
||||
// Simpan list untuk history
|
||||
self.latestRiwayatList = riwayat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func failed(error: NSError) {}
|
||||
}
|
||||
|
||||
private extension HomeViewController {
|
||||
static let idrCurrencyFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale(identifier: "id_ID")
|
||||
formatter.numberStyle = .currency
|
||||
formatter.currencyCode = "IDR"
|
||||
formatter.currencySymbol = "Rp "
|
||||
formatter.maximumFractionDigits = 0
|
||||
formatter.minimumFractionDigits = 0
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - UILabel letter spacing helper
|
||||
|
||||
private extension UILabel {
|
||||
func letterSpacing(_ spacing: CGFloat) {
|
||||
guard let text = text else { return }
|
||||
let attributed = NSMutableAttributedString(string: text)
|
||||
attributed.addAttribute(.kern, value: spacing, range: NSRange(location: 0, length: text.count))
|
||||
attributedText = attributed
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String card masking
|
||||
|
||||
private extension String {
|
||||
/// Replaces the first 12 digits with * and shows the last 4, formatted as **** **** **** XXXX
|
||||
func maskFirst12() -> String {
|
||||
let digits = self.filter { $0.isNumber }
|
||||
guard digits.count == 16 else { return self }
|
||||
let last4 = String(digits.suffix(4))
|
||||
return "**** **** **** \(last4)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user