Initial commit
366
Emoney Info/AboutViewController.swift
Normal file
@ -0,0 +1,366 @@
|
||||
// AboutViewController.swift
|
||||
// Emoney Info
|
||||
|
||||
import UIKit
|
||||
|
||||
class AboutViewController: 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 button + nav title
|
||||
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 navTitleLabel = UILabel()
|
||||
navTitleLabel.text = L10n.aboutAppTitle
|
||||
navTitleLabel.font = Theme.Font.caption(weight: .semibold)
|
||||
navTitleLabel.textColor = Theme.Color.textSecondary
|
||||
navTitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: App icon
|
||||
let iconView = UIImageView(image: UIImage(named: "AppLogo"))
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.layer.cornerRadius = 24
|
||||
iconView.clipsToBounds = true
|
||||
iconView.layer.shadowColor = UIColor.black.cgColor
|
||||
iconView.layer.shadowOpacity = 0.12
|
||||
iconView.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
iconView.layer.shadowRadius = 12
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: App name + version
|
||||
let appNameLabel = UILabel()
|
||||
appNameLabel.text = "Emoney Info"
|
||||
appNameLabel.font = .systemFont(ofSize: 28, weight: .bold)
|
||||
appNameLabel.textColor = Theme.Color.textPrimary
|
||||
appNameLabel.textAlignment = .center
|
||||
appNameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
||||
let versionLabel = UILabel()
|
||||
let versionText = "VERSI \(appVersion)"
|
||||
let versionAttr = NSAttributedString(string: versionText, attributes: [
|
||||
.kern: 1.5,
|
||||
.font: Theme.Font.caption(weight: .semibold),
|
||||
.foregroundColor: Theme.Color.textSecondary,
|
||||
])
|
||||
versionLabel.attributedText = versionAttr
|
||||
versionLabel.textAlignment = .center
|
||||
versionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Description
|
||||
let descLabel = UILabel()
|
||||
descLabel.text = L10n.aboutAppDescription
|
||||
descLabel.font = Theme.Font.body(weight: .regular)
|
||||
descLabel.textColor = Theme.Color.textSecondary
|
||||
descLabel.numberOfLines = 0
|
||||
descLabel.textAlignment = .center
|
||||
descLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Feature chips
|
||||
let chipsStack = UIStackView(arrangedSubviews: [
|
||||
makeFeatureChip(L10n.aboutChipNfc),
|
||||
makeFeatureChip(L10n.aboutChipRealtime),
|
||||
makeFeatureChip(L10n.aboutChipMulti),
|
||||
])
|
||||
chipsStack.axis = .horizontal
|
||||
chipsStack.spacing = 10
|
||||
chipsStack.alignment = .center
|
||||
chipsStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Legal rows card
|
||||
let legalCard = makeCard()
|
||||
|
||||
let termsRow = makeLegalRow(icon: "doc.text", title: L10n.aboutTerms)
|
||||
termsRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(termsTapped)))
|
||||
termsRow.isUserInteractionEnabled = true
|
||||
|
||||
let separator = UIView()
|
||||
separator.backgroundColor = Theme.Color.background
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
separator.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
||||
|
||||
let privacyRow = makeLegalRow(icon: "lock.shield", title: L10n.aboutPrivacy)
|
||||
privacyRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(privacyTapped)))
|
||||
privacyRow.isUserInteractionEnabled = true
|
||||
|
||||
let legalStack = UIStackView(arrangedSubviews: [termsRow, separator, privacyRow])
|
||||
legalStack.axis = .vertical
|
||||
legalStack.spacing = 0
|
||||
legalStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
legalCard.addSubview(legalStack)
|
||||
|
||||
// MARK: Connect card (teal gradient)
|
||||
let connectCard = UIView()
|
||||
connectCard.layer.cornerRadius = 20
|
||||
connectCard.clipsToBounds = true
|
||||
connectCard.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let gradientLayer = CAGradientLayer()
|
||||
gradientLayer.colors = [
|
||||
Theme.Color.primary.cgColor,
|
||||
Theme.Color.secondary.cgColor,
|
||||
]
|
||||
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
|
||||
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
|
||||
connectCard.layer.insertSublayer(gradientLayer, at: 0)
|
||||
|
||||
let connectIcon = UIImageView(image: UIImage(systemName: "wave.3.right.circle.fill",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 32, weight: .medium)))
|
||||
connectIcon.tintColor = UIColor.white.withAlphaComponent(0.35)
|
||||
connectIcon.contentMode = .scaleAspectFit
|
||||
connectIcon.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let connectTitle = UILabel()
|
||||
connectTitle.text = L10n.aboutConnectTitle
|
||||
connectTitle.font = Theme.Font.subtitle(weight: .bold)
|
||||
connectTitle.textColor = .white
|
||||
connectTitle.numberOfLines = 0
|
||||
connectTitle.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let connectDesc = UILabel()
|
||||
connectDesc.text = L10n.aboutConnectDesc
|
||||
connectDesc.font = Theme.Font.caption(weight: .regular)
|
||||
connectDesc.textColor = UIColor.white.withAlphaComponent(0.85)
|
||||
connectDesc.numberOfLines = 0
|
||||
connectDesc.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
[connectIcon, connectTitle, connectDesc].forEach { connectCard.addSubview($0) }
|
||||
|
||||
// MARK: Copyright
|
||||
let copyrightLabel = UILabel()
|
||||
copyrightLabel.text = L10n.footerCopyright
|
||||
copyrightLabel.font = Theme.Font.caption(weight: .regular)
|
||||
copyrightLabel.textColor = Theme.Color.textSecondary
|
||||
copyrightLabel.textAlignment = .center
|
||||
copyrightLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Add to contentView
|
||||
[backButton, navTitleLabel, iconView, appNameLabel, versionLabel,
|
||||
descLabel, chipsStack, legalCard, connectCard, copyrightLabel]
|
||||
.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),
|
||||
|
||||
navTitleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
|
||||
navTitleLabel.leadingAnchor.constraint(equalTo: backButton.trailingAnchor, constant: 8),
|
||||
|
||||
// Icon
|
||||
iconView.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 28),
|
||||
iconView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
iconView.widthAnchor.constraint(equalToConstant: 96),
|
||||
iconView.heightAnchor.constraint(equalToConstant: 96),
|
||||
|
||||
// Name + version
|
||||
appNameLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 16),
|
||||
appNameLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
|
||||
versionLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: 6),
|
||||
versionLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
|
||||
// Desc
|
||||
descLabel.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: 16),
|
||||
descLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32),
|
||||
descLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -32),
|
||||
|
||||
// Chips
|
||||
chipsStack.topAnchor.constraint(equalTo: descLabel.bottomAnchor, constant: 20),
|
||||
chipsStack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
|
||||
// Legal card
|
||||
legalCard.topAnchor.constraint(equalTo: chipsStack.bottomAnchor, constant: 28),
|
||||
legalCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
legalCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
|
||||
legalStack.topAnchor.constraint(equalTo: legalCard.topAnchor, constant: 4),
|
||||
legalStack.leadingAnchor.constraint(equalTo: legalCard.leadingAnchor),
|
||||
legalStack.trailingAnchor.constraint(equalTo: legalCard.trailingAnchor),
|
||||
legalStack.bottomAnchor.constraint(equalTo: legalCard.bottomAnchor, constant: -4),
|
||||
|
||||
// Connect card
|
||||
connectCard.topAnchor.constraint(equalTo: legalCard.bottomAnchor, constant: 20),
|
||||
connectCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
connectCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
|
||||
connectIcon.topAnchor.constraint(equalTo: connectCard.topAnchor, constant: 20),
|
||||
connectIcon.trailingAnchor.constraint(equalTo: connectCard.trailingAnchor, constant: -20),
|
||||
connectIcon.widthAnchor.constraint(equalToConstant: 44),
|
||||
connectIcon.heightAnchor.constraint(equalToConstant: 44),
|
||||
|
||||
connectTitle.topAnchor.constraint(equalTo: connectCard.topAnchor, constant: 24),
|
||||
connectTitle.leadingAnchor.constraint(equalTo: connectCard.leadingAnchor, constant: 20),
|
||||
connectTitle.trailingAnchor.constraint(equalTo: connectIcon.leadingAnchor, constant: -12),
|
||||
|
||||
connectDesc.topAnchor.constraint(equalTo: connectTitle.bottomAnchor, constant: 10),
|
||||
connectDesc.leadingAnchor.constraint(equalTo: connectCard.leadingAnchor, constant: 20),
|
||||
connectDesc.trailingAnchor.constraint(equalTo: connectCard.trailingAnchor, constant: -20),
|
||||
connectDesc.bottomAnchor.constraint(equalTo: connectCard.bottomAnchor, constant: -24),
|
||||
|
||||
// Copyright
|
||||
copyrightLabel.topAnchor.constraint(equalTo: connectCard.bottomAnchor, constant: 24),
|
||||
copyrightLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
copyrightLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -32),
|
||||
])
|
||||
|
||||
// Gradient frame — set after layout
|
||||
DispatchQueue.main.async {
|
||||
gradientLayer.frame = connectCard.bounds
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func makeCard() -> UIView {
|
||||
let v = UIView()
|
||||
v.backgroundColor = Theme.Color.card
|
||||
v.layer.cornerRadius = 16
|
||||
v.layer.shadowColor = UIColor.black.cgColor
|
||||
v.layer.shadowOpacity = 0.06
|
||||
v.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
v.layer.shadowRadius = 8
|
||||
v.translatesAutoresizingMaskIntoConstraints = false
|
||||
return v
|
||||
}
|
||||
|
||||
private func makeFeatureChip(_ title: String) -> UIView {
|
||||
let container = UIView()
|
||||
container.backgroundColor = Theme.Color.primary.withAlphaComponent(0.12)
|
||||
container.layer.cornerRadius = 12
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = Theme.Font.caption(weight: .semibold)
|
||||
label.textColor = Theme.Color.secondary
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(label)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
label.topAnchor.constraint(equalTo: container.topAnchor, constant: 6),
|
||||
label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
|
||||
label.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -12),
|
||||
label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -6),
|
||||
])
|
||||
return container
|
||||
}
|
||||
|
||||
private func makeLegalRow(icon: String, title: String) -> UIView {
|
||||
let row = UIView()
|
||||
row.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let iconContainer = UIView()
|
||||
iconContainer.backgroundColor = Theme.Color.background
|
||||
iconContainer.layer.cornerRadius = 10
|
||||
iconContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let iconView = UIImageView(image: UIImage(systemName: icon,
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .medium)))
|
||||
iconView.tintColor = Theme.Color.secondary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
iconContainer.addSubview(iconView)
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
titleLabel.font = Theme.Font.body(weight: .medium)
|
||||
titleLabel.textColor = Theme.Color.textPrimary
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let chevronImg = UIImage(systemName: "chevron.right",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))
|
||||
let chevron = UIImageView(image: chevronImg)
|
||||
chevron.tintColor = Theme.Color.textSecondary
|
||||
chevron.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
[iconContainer, titleLabel, chevron].forEach { row.addSubview($0) }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
iconContainer.widthAnchor.constraint(equalToConstant: 36),
|
||||
iconContainer.heightAnchor.constraint(equalToConstant: 36),
|
||||
iconView.centerXAnchor.constraint(equalTo: iconContainer.centerXAnchor),
|
||||
iconView.centerYAnchor.constraint(equalTo: iconContainer.centerYAnchor),
|
||||
iconView.widthAnchor.constraint(equalToConstant: 18),
|
||||
iconView.heightAnchor.constraint(equalToConstant: 18),
|
||||
|
||||
iconContainer.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 16),
|
||||
iconContainer.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
|
||||
titleLabel.leadingAnchor.constraint(equalTo: iconContainer.trailingAnchor, constant: 14),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8),
|
||||
|
||||
chevron.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -16),
|
||||
chevron.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
|
||||
row.heightAnchor.constraint(equalToConstant: 60),
|
||||
])
|
||||
return row
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func backTapped() {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func termsTapped() {
|
||||
let vc = TermsViewController()
|
||||
navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
|
||||
@objc private func privacyTapped() {
|
||||
let vc = PrivacyPolicyViewController()
|
||||
navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
}
|
||||
24
Emoney Info/AdsCell.xib
Executable file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="181" id="KGk-i7-Jjw" customClass="AdsCell" customModule="Emoney_Info" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="468" height="181"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="468" height="181"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<point key="canvasLocation" x="164.8854961832061" y="35.563380281690144"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
33
Emoney Info/AppDelegate.swift
Executable file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 22/07/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
11
Emoney Info/Assets.xcassets/AccentColor.colorset/Contents.json
Executable file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
14
Emoney Info/Assets.xcassets/AppIcon.appiconset/Contents.json
Executable file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logos.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Assets.xcassets/AppIcon.appiconset/logos.png
Executable file
|
After Width: | Height: | Size: 16 KiB |
21
Emoney Info/Assets.xcassets/AppLogo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logos.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Assets.xcassets/AppLogo.imageset/logos.png
vendored
Executable file
|
After Width: | Height: | Size: 16 KiB |
6
Emoney Info/Assets.xcassets/Contents.json
Executable file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Emoney Info/Assets.xcassets/button.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "background.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Assets.xcassets/button.imageset/background.png
vendored
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
Emoney Info/Assets.xcassets/card.imageset/32.png
vendored
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
Emoney Info/Assets.xcassets/card.imageset/64.png
vendored
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
Emoney Info/Assets.xcassets/card.imageset/96.png
vendored
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
23
Emoney Info/Assets.xcassets/card.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "64.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "96.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
89
Emoney Info/Assets.xcassets/info.imageset/Contents.json
vendored
Executable file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "info.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "info 2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "info 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "info@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "info@2x 2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "info@2x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "info@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "info@3x 2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "info@3x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Assets.xcassets/info.imageset/info 1.png
vendored
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
Emoney Info/Assets.xcassets/info.imageset/info 2.png
vendored
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Emoney Info/Assets.xcassets/info.imageset/info.png
vendored
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Emoney Info/Assets.xcassets/info.imageset/info@2x 1.png
vendored
Executable file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
Emoney Info/Assets.xcassets/info.imageset/info@2x 2.png
vendored
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Emoney Info/Assets.xcassets/info.imageset/info@2x.png
vendored
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Emoney Info/Assets.xcassets/info.imageset/info@3x 1.png
vendored
Executable file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
Emoney Info/Assets.xcassets/info.imageset/info@3x 2.png
vendored
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Emoney Info/Assets.xcassets/info.imageset/info@3x.png
vendored
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Emoney Info/Assets.xcassets/kmt.imageset/32.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
Emoney Info/Assets.xcassets/kmt.imageset/64.png
vendored
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
Emoney Info/Assets.xcassets/kmt.imageset/96.png
vendored
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
23
Emoney Info/Assets.xcassets/kmt.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "64.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "96.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
89
Emoney Info/Assets.xcassets/payment.imageset/Contents.json
vendored
Executable file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "payment_black 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "payment_black.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "payment_white.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "payment_black@2x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "payment_black@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "payment_white@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "payment_black@3x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "payment_black@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "payment_white@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_black 1.png
vendored
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_black.png
vendored
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_black@2x 1.png
vendored
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_black@2x.png
vendored
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_black@3x 1.png
vendored
Executable file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_black@3x.png
vendored
Executable file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_white.png
vendored
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_white@2x.png
vendored
Executable file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
Emoney Info/Assets.xcassets/payment.imageset/payment_white@3x.png
vendored
Executable file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
Emoney Info/Assets.xcassets/prefs.imageset/32.png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Emoney Info/Assets.xcassets/prefs.imageset/64.png
vendored
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
Emoney Info/Assets.xcassets/prefs.imageset/96.png
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
23
Emoney Info/Assets.xcassets/prefs.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "64.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "96.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
89
Emoney Info/Assets.xcassets/setting.imageset/Contents.json
vendored
Executable file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "gear.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "gear 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "gear 2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "gear@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "gear@2x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "gear@2x 2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "gear@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "gear@3x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "gear@3x 2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear 1.png
vendored
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear 2.png
vendored
Executable file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear.png
vendored
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear@2x 1.png
vendored
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear@2x 2.png
vendored
Executable file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear@2x.png
vendored
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear@3x 1.png
vendored
Executable file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear@3x 2.png
vendored
Executable file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
Emoney Info/Assets.xcassets/setting.imageset/gear@3x.png
vendored
Executable file
|
After Width: | Height: | Size: 3.8 KiB |
21
Emoney Info/Assets.xcassets/simcard.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "simcard.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Assets.xcassets/simcard.imageset/simcard.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
89
Emoney Info/Assets.xcassets/topup.imageset/Contents.json
vendored
Executable file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "creditcard_black 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "creditcard_black.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "creditcard.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "creditcard_black@2x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "creditcard_black@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "creditcard@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "creditcard_black@3x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "creditcard_black@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "creditcard@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard.png
vendored
Executable file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard@2x.png
vendored
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard@3x.png
vendored
Executable file
|
After Width: | Height: | Size: 804 B |
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard_black 1.png
vendored
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard_black.png
vendored
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard_black@2x 1.png
vendored
Executable file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard_black@2x.png
vendored
Executable file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard_black@3x 1.png
vendored
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
Emoney Info/Assets.xcassets/topup.imageset/creditcard_black@3x.png
vendored
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
18
Emoney Info/BRIEF.md
Normal file
@ -0,0 +1,18 @@
|
||||
# emoneyInfo - UI Brief
|
||||
Reference in Design folder
|
||||
## App Description
|
||||
E-Money wallet app untuk cek saldo kartu e-money via NFC.
|
||||
## Design System
|
||||
- Primary color: Green (#7AD4D1)
|
||||
- Success/Active: Green (#5D7D7B)
|
||||
- Background: Light gray (#F3F3F8)
|
||||
- Cards: White with rounded corners (16pt)
|
||||
- Font: System font (San Francisco)
|
||||
## Screens
|
||||
1. Home - Saldo, NFC tap, promo, last transaction
|
||||
2. History - List semua transaksi
|
||||
3. Settings - Account settings, preferences
|
||||
## Tech Stack
|
||||
- SwiftUI
|
||||
- Core NFC (existing)
|
||||
- iOS 16+
|
||||
63
Emoney Info/Base.lproj/LaunchScreen.storyboard
Executable file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Read Card Balance" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f2o-LW-8aE">
|
||||
<rect key="frame" x="105.66666666666669" y="296.66666666666669" width="182" height="28"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="thin" pointSize="23"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="eG8-3f-M4u">
|
||||
<rect key="frame" x="76.666666666666686" y="374.66666666666669" width="240" height="128"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="240" id="ZNH-31-9ZB"/>
|
||||
<constraint firstAttribute="height" constant="128" id="hxE-7A-H81"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Emoney Info" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Qay-HP-vMh">
|
||||
<rect key="frame" x="142.66666666666666" y="522.66666666666663" width="108" height="25.333333333333371"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="thin" pointSize="21"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="eG8-3f-M4u" firstAttribute="centerY" secondItem="6Tk-OE-BBY" secondAttribute="centerY" id="dfD-yn-ABp"/>
|
||||
<constraint firstItem="eG8-3f-M4u" firstAttribute="top" secondItem="f2o-LW-8aE" secondAttribute="bottom" constant="50" id="gfi-Qk-O6g"/>
|
||||
<constraint firstItem="Qay-HP-vMh" firstAttribute="top" secondItem="eG8-3f-M4u" secondAttribute="bottom" constant="20" id="nwU-fJ-2bF"/>
|
||||
<constraint firstItem="eG8-3f-M4u" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="sXh-i2-W2n"/>
|
||||
<constraint firstItem="Qay-HP-vMh" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="xVm-AF-t8N"/>
|
||||
<constraint firstItem="f2o-LW-8aE" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="yLk-em-nQ2"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="logo.png" width="616.5" height="395.25"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
408
Emoney Info/Classes/api/BcaFlazzApi.swift
Executable file
@ -0,0 +1,408 @@
|
||||
//
|
||||
// BcaFlazzApi.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 27/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreNFC
|
||||
|
||||
public class BcaFlazzApi : UnifiedNfcApi {
|
||||
var emoney : Emoney = Emoney()
|
||||
var riwayatList: [RiwayatCard] = []
|
||||
var start = 0
|
||||
var finishV2 = 5
|
||||
var finish2V202 = 256
|
||||
var finishV1 = 16
|
||||
var mapv1 : String = ""
|
||||
var mapv2 : String = ""
|
||||
|
||||
public override init() {}
|
||||
|
||||
public func checkFlazzCard(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU01, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.emoney.setCardLabel("BCA Flazz")
|
||||
self.getCardNumber()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getCardNumber(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU02, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
let raw = String(data: response.getData(), encoding: .isoLatin1)
|
||||
let start = raw!.firstIndex(of: ";")?.utf16Offset(in: raw!)
|
||||
let end = raw!.firstIndex(of: "=")?.utf16Offset(in: raw!)
|
||||
self.emoney.setCardNumber(String((raw?.subString(from: (start! + 1), to: end!))!))
|
||||
self.getBalance()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getBalance(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU03, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
let raw = response.getData().hexEncodedString()
|
||||
let balance = raw.subString(from: 2, to: 8)
|
||||
self.emoney.setBalance(balance.hex2decimal())
|
||||
self.checkLog()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func checkLog(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU04, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
debugLog("log v2")
|
||||
// self.updateScreen()
|
||||
self.getLogV2step01(index: self.start)
|
||||
} else {
|
||||
debugLog("log v1")
|
||||
// self.updateScreen()
|
||||
self.getLogV1step01(index: self.start)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogV2step01(index : Int){
|
||||
debugLog("log getLogV2step01")
|
||||
//00 B0 85 00 78
|
||||
let st = index*60
|
||||
let hex = String(st, radix: 16)
|
||||
let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x85, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 120)
|
||||
apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
debugLog("log data", response.getData().hexEncodedString())
|
||||
|
||||
|
||||
self.mapv1.append(response.getData().hexEncodedString())
|
||||
self.start+=1
|
||||
if (self.start < (self.finishV2)){
|
||||
self.getLogV2step01(index: self.start)
|
||||
} else {
|
||||
//mapping first
|
||||
self.parseLogV201()
|
||||
self.start = 0
|
||||
self.getLogV2step02(index: self.start)
|
||||
}
|
||||
} else {
|
||||
self.parseLogV201()
|
||||
self.start = 0
|
||||
self.getLogV2step02(index: self.start)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogV1step01(index : Int){
|
||||
debugLog("log getLogV1step01")
|
||||
//00 b0 84 00 3c
|
||||
let st = index*15
|
||||
let hex = String(st, radix: 16)
|
||||
let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x84, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 60)
|
||||
apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.mapv1.append(response.getData().hexEncodedString())
|
||||
self.start+=1
|
||||
if (self.start < (self.finishV1)){
|
||||
self.getLogV1step01(index: self.start)
|
||||
} else {
|
||||
self.start = 0
|
||||
self.getLogV1step02(index: self.start)
|
||||
}
|
||||
} else {
|
||||
self.start = 0
|
||||
self.getLogV1step02(index: self.start)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogV1step02(index : Int){
|
||||
debugLog("log getLogV1step02")
|
||||
//00b085003c
|
||||
let st = index*15
|
||||
let hex = String(st, radix: 16)
|
||||
let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x85, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 60)
|
||||
apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.mapv1.append(response.getData().hexEncodedString())
|
||||
self.start+=1
|
||||
if (self.start < (self.finishV1)){
|
||||
self.getLogV1step02(index: self.start)
|
||||
} else {
|
||||
self.parseLogV101()
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
|
||||
if (self.riwayatList.count > 0){
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
}
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
} else {
|
||||
self.parseLogV101()
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
|
||||
if (self.riwayatList.count > 0){
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
}
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogV2step02(index : Int){
|
||||
debugLog("log getLogV2step02")
|
||||
//00b08400f0
|
||||
let st = index*60
|
||||
let hex = String(st, radix: 16)
|
||||
let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x84, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 240)
|
||||
apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.mapv1.append(response.getData().hexEncodedString())
|
||||
self.start+=1
|
||||
if (self.start < (self.finishV2)){
|
||||
self.getLogV2step02(index: self.start)
|
||||
} else {
|
||||
self.start = 0
|
||||
self.getLogV2step03()
|
||||
}
|
||||
} else {
|
||||
self.start = 0
|
||||
self.getLogV2step03()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogV2step03(){
|
||||
debugLog("log getLogV2step03")
|
||||
//00 84 00 00 08
|
||||
let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0x84, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(), expectedResponseLength : 8)
|
||||
apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.getLogV2step04(data: response.getData())
|
||||
} else {
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogV2step04(data: Data){
|
||||
debugLog("log getLogV2step04")
|
||||
//90 32 03 00 0A 0801 0000000000000000 29
|
||||
let send = "0801" + data.hexEncodedString()
|
||||
let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x32, p1Parameter : 0x03, p2Parameter : 0x00, data : Data(_: send.hex2byte()), expectedResponseLength : 41)
|
||||
apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.getLogV2step05(index: self.start)
|
||||
} else {
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogV2step05(index : Int){
|
||||
debugLog("log getLogV2step05")
|
||||
//00 B0 89 00 40
|
||||
let st = index
|
||||
let hex = String(st, radix: 16)
|
||||
let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x89, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 64)
|
||||
apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.mapv2.append(response.getData().hexEncodedString())
|
||||
self.start+=1
|
||||
if (self.start < (self.finish2V202)){
|
||||
self.getLogV2step05(index: self.start)
|
||||
} else {
|
||||
self.start = 0
|
||||
self.getLogV2step06(index: self.start)
|
||||
}
|
||||
} else {
|
||||
self.start = 0
|
||||
self.getLogV2step06(index: self.start)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogV2step06(index : Int){
|
||||
debugLog("log getLogV2step06")
|
||||
//90 32 03 00 01 00 20
|
||||
let st = index
|
||||
let hex = String(st, radix: 16)
|
||||
let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x32, p1Parameter : 0x03, p2Parameter : 0x00, data : Data(_: hex.hex2byte()), expectedResponseLength : 32)
|
||||
apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.mapv2.append(response.getData().hexEncodedString())
|
||||
self.start+=1
|
||||
if (self.start < (self.finish2V202)){
|
||||
self.getLogV2step06(index: self.start)
|
||||
} else {
|
||||
//mapping the data
|
||||
self.parseLogV202()
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
|
||||
if (self.riwayatList.count > 0){
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
}
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
} else {
|
||||
//mapping the data
|
||||
self.parseLogV202()
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
|
||||
if (self.riwayatList.count > 0){
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
}
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func parseLogV101(){
|
||||
debugLog("log parseLogV101")
|
||||
let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if (logs.count % 120 != 0){
|
||||
return
|
||||
}
|
||||
let total = logs.count/120
|
||||
for i in 0..<total {
|
||||
let start = i*120
|
||||
let end = start + 120
|
||||
let data = logs.subString(from: start, to: end)
|
||||
let riwayat = RiwayatCard()
|
||||
let location = data.subString(from: 60, to: 76).hex2byte()
|
||||
let locationId = String(data: location, encoding: .utf8)!
|
||||
riwayat.setLocationId(locationId)
|
||||
// riwayat.setLocationName(data.subString(from: 0, to: 16))
|
||||
let amount = data.subString(from: 12, to: 18).hex2decimal()
|
||||
riwayat.setAmount(amount)
|
||||
let transactionTime = data.subString(from: 76, to: 84).hex2decimal()
|
||||
riwayat.setTransactionTime(formatDate(seconds: transactionTime))
|
||||
let type = data.subString(from: 0, to: 4).hex2decimal()
|
||||
if (type == 1024){
|
||||
riwayat.setProsesTipe(1)
|
||||
riwayat.setTitle("payment".localizeString(string: self.langCode!))
|
||||
} else {
|
||||
riwayat.setProsesTipe(0)
|
||||
riwayat.setTitle("topup".localizeString(string: self.langCode!))
|
||||
}
|
||||
|
||||
if (transactionTime > 0){
|
||||
riwayatList.append(riwayat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func parseLogV201(){
|
||||
debugLog("log parseLogV201")
|
||||
let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if (logs.count % 120 != 0){
|
||||
return
|
||||
}
|
||||
let total = logs.count/120
|
||||
for i in 0..<total {
|
||||
let start = i*120
|
||||
let end = start + 120
|
||||
let data = logs.subString(from: start, to: end)
|
||||
let riwayat = RiwayatCard()
|
||||
let location = data.subString(from: 60, to: 76).hex2byte()
|
||||
let locationId = String(data: location, encoding: .utf8)!
|
||||
riwayat.setLocationId(locationId)
|
||||
// riwayat.setLocationName(data.subString(from: 0, to: 16))
|
||||
let amount = data.subString(from: 12, to: 18).hex2decimal()
|
||||
riwayat.setAmount(amount)
|
||||
let transactionTime = data.subString(from: 76, to: 84).hex2decimal()
|
||||
riwayat.setTransactionTime(formatDate(seconds: transactionTime))
|
||||
let type = data.subString(from: 0, to: 4).hex2decimal()
|
||||
if (type == 1024){
|
||||
riwayat.setProsesTipe(1)
|
||||
riwayat.setTitle("payment".localizeString(string: self.langCode!))
|
||||
} else {
|
||||
riwayat.setProsesTipe(0)
|
||||
riwayat.setTitle("topup".localizeString(string: self.langCode!))
|
||||
}
|
||||
|
||||
if (transactionTime > 0){
|
||||
riwayatList.append(riwayat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func parseLogV202(){
|
||||
debugLog("log parseLogV202")
|
||||
let logs = self.mapv2.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if (logs.count % 64 != 0){
|
||||
return
|
||||
}
|
||||
let total = logs.count/64
|
||||
for i in 0..<total {
|
||||
let start = i*64
|
||||
let end = start + 64
|
||||
let data = logs.subString(from: start, to: end)
|
||||
let riwayat = RiwayatCard()
|
||||
// let location = data.subString(from: 28, to: 44).hex2byte()
|
||||
// let locationId = String(data: location, encoding: .utf8)!
|
||||
// riwayat.setLocationId(locationId)
|
||||
let transactionTime = data.subString(from: 8, to: 16).hex2decimal()
|
||||
riwayat.setTransactionTime(formatDate(seconds: transactionTime))
|
||||
let type = data.subString(from: 0, to: 2).hex2decimal()
|
||||
let amount = data.subString(from: 2, to: 8).hex2decimal()
|
||||
if (type == 4){
|
||||
riwayat.setProsesTipe(1)
|
||||
riwayat.setAmount(16777216 - amount)
|
||||
riwayat.setTitle("payment".localizeString(string: self.langCode!))
|
||||
} else {
|
||||
riwayat.setProsesTipe(0)
|
||||
riwayat.setAmount(amount)
|
||||
riwayat.setTitle("topup".localizeString(string: self.langCode!))
|
||||
}
|
||||
if (transactionTime > 0){
|
||||
riwayatList.append(riwayat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDate(seconds : Int) -> Date{
|
||||
// Specify date components
|
||||
var dateComponents = DateComponents()
|
||||
dateComponents.year = 1980
|
||||
dateComponents.month = 1
|
||||
dateComponents.day = 1
|
||||
dateComponents.timeZone = TimeZone(identifier: "Asia/Jakarta")!
|
||||
dateComponents.hour = 0
|
||||
dateComponents.minute = 0
|
||||
dateComponents.second = seconds
|
||||
// Create date from components
|
||||
let userCalendar = Calendar.current // user calendar
|
||||
let someDateTime = userCalendar.date(from: dateComponents)
|
||||
return someDateTime!
|
||||
}
|
||||
|
||||
private func updateScreen(){
|
||||
if (self.apduRunner.callback != nil){
|
||||
self.apduRunner.callback?.complete(emoney: self.emoney)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
247
Emoney Info/Classes/api/BrizziApi.swift
Executable file
@ -0,0 +1,247 @@
|
||||
//
|
||||
// BrizziApi.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 27/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreNFC
|
||||
|
||||
public class BrizziApi : UnifiedNfcApi {
|
||||
var emoney : Emoney = Emoney()
|
||||
var riwayatList: [RiwayatCard] = []
|
||||
var uid : String?
|
||||
var rawLog : String = ""
|
||||
|
||||
public override init() {}
|
||||
|
||||
public func getUid(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_UID01, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0xAF){
|
||||
self.getUid02()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getUid02(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_UID02, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0xAF){
|
||||
self.getUid02()
|
||||
} else {
|
||||
self.uid = response.getData().hexEncodedString().subString(from: 0, to: 14)
|
||||
self.getCardNumber()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getCardNumber(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_APDU01, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0x00){
|
||||
self.emoney.setCardLabel("Brizzi")
|
||||
self.emoney.setCardNumber(response.getData().hexEncodedString().subString(from: 6, to: 22))
|
||||
self.process01()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func process01(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_APDU02, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0x00){
|
||||
self.process02()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func process02(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_APDU03, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0x00) || (response.sw1 == 0x91 && response.sw2 == 0xAF){
|
||||
self.process03(data: response.getData())
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func process03(data : Data){
|
||||
let brizziSamHelper = BrizziSamHelper()
|
||||
brizziSamHelper.keyCard = data.hexEncodedString()
|
||||
let random = "8DC0DC40FE1DC582CF7099E2AACFBC10".hex2byte()
|
||||
let command = self.emoney.getCardNumber() + self.uid! + "FF"
|
||||
let decrypted = BrizziSamHelper.decryptDeSeDe(random)?.hexEncodedString()
|
||||
|
||||
let decryptedFinal = decrypted?.subString(from: 0, to: 32)
|
||||
let encrypted = BrizziSamHelper.encryptDeSeDe(command, decryptedFinal!, "0000000000000000")?.hexEncodedString()
|
||||
|
||||
brizziSamHelper.encryptedKey = encrypted?.subString(from: 0, to: 32)
|
||||
|
||||
|
||||
let randomHex = "3C37029CA595FE4E7E62FCB2F7909B2C".hex2byte()
|
||||
let randomHexDecrypted = BrizziSamHelper.decryptDeSeDe(randomHex)
|
||||
|
||||
let randomHexFinal = randomHexDecrypted?.hexEncodedString().subString(from: 0, to: 32)
|
||||
let randomHexEncrypted = BrizziSamHelper.encryptDeSeDe(brizziSamHelper.encryptedKey!, randomHexFinal!, brizziSamHelper.authKey)
|
||||
brizziSamHelper.random = (randomHexEncrypted?.hexEncodedString())!.subString(from: 0, to: 32)
|
||||
|
||||
|
||||
let samChallenge = brizziSamHelper.generateSamRandom().subString(from: 0, to: 32)
|
||||
let BRI_APDU04 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0xAF, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_: samChallenge.hex2byte()), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
apduRunner.exchangeApdu(apduCommand: BRI_APDU04, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0x00) || (response.sw1 == 0x91 && response.sw2 == 0xAF){
|
||||
self.process04()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func process04(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_APDU05, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0x00) || (response.sw1 == 0x91 && response.sw2 == 0xAF){
|
||||
self.emoney.setBalance(self.getRealBalance(reverseHexa: response.getData().hexEncodedString().subString(from: 0, to: 8)))
|
||||
self.getLog()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLog(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_LOG01, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0x00) || (response.sw1 == 0x91 && response.sw2 == 0xAF){
|
||||
self.rawLog.append(response.getData().hexEncodedString())
|
||||
self.getMoreLog()
|
||||
} else {
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getMoreLog(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_LOG02, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0xAF){
|
||||
self.rawLog.append(response.getData().hexEncodedString())
|
||||
self.getMoreLog()
|
||||
} else {
|
||||
self.rawLog.append(response.getData().hexEncodedString())
|
||||
|
||||
if (self.parseLog()){
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
}
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func parseLog() -> Bool {
|
||||
let logs = self.rawLog.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if (logs.count % 64 != 0){
|
||||
return false
|
||||
}
|
||||
let total = logs.count/64
|
||||
for i in 0..<total {
|
||||
let start = i*64
|
||||
let end = start + 64
|
||||
let data = logs.subString(from: start, to: end)
|
||||
let riwayat = RiwayatCard()
|
||||
// riwayat.setLocationId(data.subString(from: 16, to: 32))
|
||||
// riwayat.setLocationName(data.subString(from: 0, to: 16))
|
||||
riwayat.setTitle(self.getCode(trxCode: data.subString(from: 44, to: 46)))
|
||||
riwayat.setProsesTipe(self.getTipe(trxCode: data.subString(from: 44, to: 46)))
|
||||
riwayat.setAmount(self.getRealBalance(reverseHexa: data.subString(from: 46, to: 52)))
|
||||
|
||||
let time = self.getTransactionTime(formatDate: data.subString(from: 32, to: 38), formatTime: data.subString(from: 38, to: 44))
|
||||
riwayat.setTransactionTime(time!)
|
||||
|
||||
riwayatList.append(riwayat)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getTransactionTime(formatDate : String, formatTime : String) -> Date?{
|
||||
let dateFormatter2 = DateFormatter()
|
||||
dateFormatter2.dateFormat = "HHmmss"
|
||||
let date12 = dateFormatter2.date(from: formatTime)!
|
||||
|
||||
dateFormatter2.dateFormat = "hh:mm a"
|
||||
let date22 = dateFormatter2.string(from: date12)
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
|
||||
dateFormatter.dateFormat = "ddMMyy hh:mm a"
|
||||
return dateFormatter.date(from:(formatDate + " " + date22))
|
||||
}
|
||||
|
||||
private func getCode(trxCode: String) -> String {
|
||||
let trxUp = trxCode.uppercased()
|
||||
if trxUp.contains("5F") {
|
||||
return "reactivation".localizeString(string: self.langCode!)
|
||||
} else if trxUp.contains("EB") {
|
||||
return "payment".localizeString(string: self.langCode!)
|
||||
} else if trxUp.contains("EC") {
|
||||
return "topup".localizeString(string: self.langCode!)
|
||||
} else if trxUp.contains("ED") {
|
||||
return "void".localizeString(string: self.langCode!)
|
||||
} else if trxUp.contains("EF") {
|
||||
return "updateBalance".localizeString(string: self.langCode!)
|
||||
} else {
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
|
||||
private func getTipe(trxCode: String) -> Int {
|
||||
let trxUp = trxCode.uppercased()
|
||||
if trxUp.contains("5F") {
|
||||
return 2
|
||||
} else if trxUp.contains("EB") {
|
||||
return 1
|
||||
} else if trxUp.contains("EC") {
|
||||
return 0
|
||||
} else if trxUp.contains("ED") {
|
||||
return 0
|
||||
} else if trxUp.contains("EF") {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func getRealBalance(reverseHexa: String?) -> Int {
|
||||
guard let reverseHexa = reverseHexa?.trimmingCharacters(in: .whitespacesAndNewlines), !reverseHexa.isEmpty else {
|
||||
return 0
|
||||
}
|
||||
|
||||
if reverseHexa.count % 2 != 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var sb = ""
|
||||
for l in stride(from: reverseHexa.count / 2, through: 1, by: -1) {
|
||||
let index1 = reverseHexa.index(reverseHexa.startIndex, offsetBy: l * 2 - 2)
|
||||
let index2 = reverseHexa.index(reverseHexa.startIndex, offsetBy: l * 2 - 1)
|
||||
sb.append(reverseHexa[index1])
|
||||
sb.append(reverseHexa[index2])
|
||||
}
|
||||
return sb.hex2decimal()
|
||||
}
|
||||
|
||||
private func updateScreen(){
|
||||
if (self.apduRunner.callback != nil){
|
||||
self.apduRunner.callback?.complete(emoney: self.emoney)
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Emoney Info/Classes/api/JackCardApi.swift
Executable file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// JackCardApi.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 27/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class JackCardApi : UnifiedNfcApi {
|
||||
var emoney : Emoney = Emoney()
|
||||
var riwayatList: [RiwayatCard] = []
|
||||
|
||||
public override init() {}
|
||||
|
||||
public func getBalance(resp : Data){
|
||||
self.emoney.setCardLabel("Jackcard")
|
||||
self.emoney.setCardNumber(resp.hexEncodedString().subString(from: 16, to: 32))
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.DKI_APDU01, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.emoney.setBalance(response.getData().hexEncodedString().hex2decimal())
|
||||
self.emoney.setTampilRiwayat(false)
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateScreen(){
|
||||
if (self.apduRunner.callback != nil){
|
||||
self.apduRunner.callback?.complete(emoney: self.emoney)
|
||||
}
|
||||
}
|
||||
}
|
||||
263
Emoney Info/Classes/api/MandiriEmoneyApi.swift
Executable file
@ -0,0 +1,263 @@
|
||||
//
|
||||
// MandiriEmoneyApi.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 27/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreNFC
|
||||
|
||||
public class MandiriEmoneyApi : UnifiedNfcApi {
|
||||
var emoney : Emoney = Emoney()
|
||||
var riwayatList: [RiwayatCard] = []
|
||||
var cardType : Int?
|
||||
var mapv1 : String = ""
|
||||
var start = 0
|
||||
var finish = 256
|
||||
var finish2 = 10
|
||||
|
||||
public override init() {}
|
||||
|
||||
public func getCardNumber(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.MANDIRI_APDU01, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.emoney.setCardLabel("Mandiri e-Money")
|
||||
self.emoney.setCardNumber(response.getData().hexEncodedString().subString(from: 0, to: 16))
|
||||
self.cardType = response.getData().hexEncodedString().subString(from: 36, to: 38).hex2decimal()
|
||||
debugLog(self.cardType!)
|
||||
self.getBalance()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getBalance(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.MANDIRI_APDU02, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
debugLog(response.getData().hexEncodedString())
|
||||
let balance = response.getData().hexEncodedString().subString(from: 0, to: 8)
|
||||
self.emoney.setBalance(self.getRealBalance(reverseHexa: balance))
|
||||
// self.updateScreen()
|
||||
if (self.cardType! == 131){
|
||||
self.getLogStep01(index: self.start)
|
||||
} else {
|
||||
self.getLogStep02(index: self.start)
|
||||
}
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getLogStep01(index : Int){
|
||||
//00 d1 00 00 00
|
||||
let hex = String(index, radix: 16)
|
||||
let MANDIRI_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xD1, p1Parameter : hex.hex2byte().bytes.first!, p2Parameter : 0x00, data : Data(), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
apduRunner.exchangeApdu(apduCommand: MANDIRI_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.mapv1.append(response.getData().hexEncodedString())
|
||||
self.start+=1
|
||||
if (self.start < (self.finish)){
|
||||
self.getLogStep01(index: self.start)
|
||||
} else {
|
||||
self.parseNewLog()
|
||||
self.start = 0
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
} else {
|
||||
self.parseNewLog()
|
||||
self.start = 0
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseNewLog(){
|
||||
let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if (logs.count % 48 != 0){
|
||||
return
|
||||
}
|
||||
let total = logs.count/48
|
||||
for i in 0..<total {
|
||||
let start = i*48
|
||||
let end = start + 48
|
||||
let data = logs.subString(from: start, to: end)
|
||||
let riwayat = RiwayatCard()
|
||||
let time = self.getTransactionTime(formatDate: data.subString(from: 0, to: 6), formatTime: data.subString(from: 6, to: 12))
|
||||
riwayat.setTransactionTime(time!)
|
||||
let processType = Int(data.subString(from: 28, to: 32))
|
||||
if (processType == 100){
|
||||
riwayat.setProsesTipe(0)
|
||||
riwayat.setTitle("Top Up")
|
||||
} else {
|
||||
riwayat.setProsesTipe(1)
|
||||
riwayat.setTitle("Payment")
|
||||
}
|
||||
riwayat.setLocationId(data.subString(from: 12, to: 20))
|
||||
let amount = data.subString(from: 32, to: 40)
|
||||
riwayat.setAmount(getRealBalance(reverseHexa: amount))
|
||||
riwayatList.append(riwayat)
|
||||
}
|
||||
}
|
||||
|
||||
private func getLogStep02(index : Int){
|
||||
//00 b2 00 00 1e
|
||||
let hex = String(index, radix: 16)
|
||||
let MANDIRI_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB2, p1Parameter : hex.hex2byte().bytes.first!, p2Parameter : 0x00, data : Data(), expectedResponseLength : 30)
|
||||
apduRunner.exchangeApdu(apduCommand: MANDIRI_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.mapv1.append(response.getData().hexEncodedString())
|
||||
self.start+=1
|
||||
if (self.start < (self.finish2)){
|
||||
self.getLogStep02(index: self.start)
|
||||
} else {
|
||||
self.parseOldLog()
|
||||
self.start = 0
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
} else {
|
||||
self.parseOldLog()
|
||||
self.start = 0
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseOldLog(){
|
||||
let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if (logs.count % 60 != 0){
|
||||
return
|
||||
}
|
||||
let total = logs.count/60
|
||||
for i in 0..<total {
|
||||
let start = i*60
|
||||
let end = start + 60
|
||||
let data = logs.subString(from: start, to: end)
|
||||
let riwayat = riwayatCard(data.hex2byte().bytes)
|
||||
riwayatList.append(riwayat!)
|
||||
}
|
||||
}
|
||||
|
||||
func getTransactionTime(formatDate : String, formatTime : String) -> Date?{
|
||||
let dateFormatter2 = DateFormatter()
|
||||
dateFormatter2.dateFormat = "HHmmss"
|
||||
let date12 = dateFormatter2.date(from: formatTime)!
|
||||
|
||||
dateFormatter2.dateFormat = "hh:mm a"
|
||||
let date22 = dateFormatter2.string(from: date12)
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
|
||||
dateFormatter.dateFormat = "ddMMyy hh:mm a"
|
||||
return dateFormatter.date(from:(formatDate + " " + date22))
|
||||
}
|
||||
|
||||
func getRealBalance(reverseHexa: String?) -> Int {
|
||||
guard let reverseHexa = reverseHexa?.trimmingCharacters(in: .whitespacesAndNewlines), !reverseHexa.isEmpty else {
|
||||
return 0
|
||||
}
|
||||
|
||||
if reverseHexa.count % 2 != 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var sb = ""
|
||||
for l in stride(from: reverseHexa.count / 2, through: 1, by: -1) {
|
||||
let index1 = reverseHexa.index(reverseHexa.startIndex, offsetBy: l * 2 - 2)
|
||||
let index2 = reverseHexa.index(reverseHexa.startIndex, offsetBy: l * 2 - 1)
|
||||
sb.append(reverseHexa[index1])
|
||||
sb.append(reverseHexa[index2])
|
||||
}
|
||||
return sb.hex2decimal()
|
||||
}
|
||||
|
||||
private func updateScreen(){
|
||||
if (self.apduRunner.callback != nil){
|
||||
self.apduRunner.callback?.complete(emoney: self.emoney)
|
||||
}
|
||||
}
|
||||
|
||||
func riwayatCard(_ bArr: [UInt8]) -> RiwayatCard? {
|
||||
var str: String
|
||||
let riwayatCard = RiwayatCard()
|
||||
var wrap = Data(bArr)
|
||||
var bArr2 = [UInt8](repeating: 0, count: 6)
|
||||
var bArr3 = [UInt8](repeating: 0, count: 16)
|
||||
|
||||
wrap.copyBytes(to: &bArr2, count: 6)
|
||||
wrap.removeFirst(10)
|
||||
|
||||
let len = wrap.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
|
||||
wrap.removeFirst(4)
|
||||
let amount = wrap.withUnsafeBytes { $0.load(as: Int32.self).littleEndian }
|
||||
wrap.removeFirst(4)
|
||||
let desk = wrap.withUnsafeBytes { $0.load(as: Int32.self).littleEndian }
|
||||
wrap.removeFirst(4)
|
||||
|
||||
do {
|
||||
wrap.copyBytes(to: &bArr3, count: 16)
|
||||
debugLog("bArr3: \(bArr3.map { String(format: "%02X", $0) }.joined())")
|
||||
} catch {
|
||||
debugLog("Error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
var type = 0
|
||||
if len == 288 {
|
||||
str = "payment".localizeString(string: self.langCode!)
|
||||
type = 1
|
||||
} else if len == 256 || len == 336 {
|
||||
type = 3
|
||||
str = "topup".localizeString(string: self.langCode!)
|
||||
} else {
|
||||
type = -1
|
||||
str = "unknown".localizeString(string: self.langCode!)
|
||||
}
|
||||
|
||||
|
||||
var str2 = ""
|
||||
for y in 0..<6 {
|
||||
str2 += String(format: "%02X", bArr2[y])
|
||||
}
|
||||
|
||||
let transactionTime = self.getTransactionTime(formatDate: str.subString(from: 0, to: 6), formatTime: str.subString(from: 6, to: 12))
|
||||
riwayatCard.setTransactionTime(transactionTime!)
|
||||
riwayatCard.setAmount(Int(amount))
|
||||
|
||||
// if str.caseInsensitiveCompare("payment") == .orderedSame {
|
||||
// type = 1
|
||||
// } else if str.caseInsensitiveCompare("topup") != .orderedSame {
|
||||
// type = 3
|
||||
// }
|
||||
|
||||
riwayatCard.setProsesTipe(type)
|
||||
riwayatCard.setTitle(str)
|
||||
|
||||
if type == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return riwayatCard
|
||||
}
|
||||
}
|
||||
58
Emoney Info/Classes/api/MegaCashApi.swift
Executable file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// MegaCashApi.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 27/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class MegaCashApi : UnifiedNfcApi {
|
||||
var emoney : Emoney = Emoney()
|
||||
var riwayatList: [RiwayatCard] = []
|
||||
|
||||
public override init() {}
|
||||
|
||||
public func getBalance(resp : Data){
|
||||
self.emoney.setCardLabel("MegaCash")
|
||||
let value = resp.hexEncodedString()
|
||||
self.emoney.setCardNumber(value.subString(from: 4, to: value.count))
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.MEGA_APDU02, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
let balance = response.getData().hexEncodedString()
|
||||
self.emoney.setBalance(self.getRealBalance(reverseHexa: balance))
|
||||
self.emoney.setTampilRiwayat(false)
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func getRealBalance(reverseHexa: String?) -> Int {
|
||||
guard let reverseHexa = reverseHexa?.trimmingCharacters(in: .whitespacesAndNewlines), !reverseHexa.isEmpty else {
|
||||
return 0
|
||||
}
|
||||
|
||||
if reverseHexa.count % 2 != 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var sb = ""
|
||||
for l in stride(from: reverseHexa.count / 2, through: 1, by: -1) {
|
||||
let index1 = reverseHexa.index(reverseHexa.startIndex, offsetBy: l * 2 - 2)
|
||||
let index2 = reverseHexa.index(reverseHexa.startIndex, offsetBy: l * 2 - 1)
|
||||
sb.append(reverseHexa[index1])
|
||||
sb.append(reverseHexa[index2])
|
||||
}
|
||||
return sb.hex2decimal()
|
||||
}
|
||||
|
||||
private func updateScreen(){
|
||||
if (self.apduRunner.callback != nil){
|
||||
self.apduRunner.callback?.complete(emoney: self.emoney)
|
||||
}
|
||||
}
|
||||
}
|
||||
178
Emoney Info/Classes/api/TapCashApi.swift
Executable file
@ -0,0 +1,178 @@
|
||||
//
|
||||
// TapCashApi.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 26/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreNFC
|
||||
|
||||
public class TapCashApi : UnifiedNfcApi {
|
||||
var emoney : Emoney = Emoney()
|
||||
var tapCashData : TapCashData = TapCashData()
|
||||
var riwayatList: [RiwayatCard] = []
|
||||
var start = 0
|
||||
var totalLog = 0
|
||||
|
||||
public override init() {}
|
||||
|
||||
public func checkBalance(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.TAPCASH_APDU01, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.emoney.setCardLabel("BNI TapCash")
|
||||
self.tapCashData.setPurseData(response.getData().bytes)
|
||||
let balance = self.tapCashData.getPurseBalance()?.hexString().hex2decimal()
|
||||
self.emoney.setBalance(balance!)
|
||||
self.emoney.setCardNumber(self.tapCashData.getCAN()!.hexString())
|
||||
self.totalLog = (self.tapCashData.getTotalRecords()?.hexString().hex2decimal())!
|
||||
if (self.totalLog > 10){
|
||||
self.totalLog = 10
|
||||
}
|
||||
debugLog("total log " + String(self.totalLog))
|
||||
self.getHistory(index: 0)
|
||||
} else {
|
||||
self.emoney.setTampilRiwayat(false)
|
||||
self.updateScreen()
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func getHistory(index : Int){
|
||||
let st = String(index).leftPad(with: "0", length: 2)
|
||||
//90 32 03 00 01 00 10
|
||||
let TAPCASH_LOG = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x32, p1Parameter : 0x03, p2Parameter : 0x00, data : Data(_ : st.stringToBytes()!), expectedResponseLength : 16)
|
||||
debugLog(TAPCASH_LOG.toHexString())
|
||||
apduRunner.exchangeApdu(apduCommand: TAPCASH_LOG, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
self.addRiwayatTransaksi(data: response.getData().bytes)
|
||||
}
|
||||
self.start+=1
|
||||
if (self.start < (self.totalLog)){
|
||||
self.getHistory(index: self.start)
|
||||
} else {
|
||||
self.updateScreen()
|
||||
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
|
||||
|
||||
if (self.riwayatList.count > 0){
|
||||
self.emoney.setRiwayatList(self.riwayatList)
|
||||
self.emoney.setTampilRiwayat(true)
|
||||
}
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateScreen(){
|
||||
if (self.apduRunner.callback != nil){
|
||||
self.apduRunner.callback?.complete(emoney: self.emoney)
|
||||
}
|
||||
}
|
||||
|
||||
private func addRiwayatTransaksi(data: [UInt8]) {
|
||||
let trxType = Array(data[0..<1])
|
||||
let trxAmount = Array(data[1..<4])
|
||||
let trxDateTimes = Array(data[4..<8])
|
||||
//let trxUserData = Array(data[8..<16])
|
||||
|
||||
let title = trxType.hexString()
|
||||
var amount: Int
|
||||
if title.lowercased() == "01" || title.lowercased() == "05" || title.lowercased() == "07" || title.lowercased() == "10" || title.lowercased() == "20" {
|
||||
amount = trxAmount.hexString().secondComplementsAmount()
|
||||
} else {
|
||||
amount = trxType.hexString().hex2decimal()
|
||||
}
|
||||
|
||||
let riwayatCard = RiwayatCard()
|
||||
riwayatCard.setTitle(getStatementTitle(header: trxType.hexString()))
|
||||
riwayatCard.setProsesTipe(getTranscationType(header: trxType.hexString()))
|
||||
riwayatCard.setAmount(amount)
|
||||
let transactionTime = self.getTransactionTime(julian: trxDateTimes.hexString())
|
||||
riwayatCard.setTransactionTime(transactionTime)
|
||||
riwayatList.append(riwayatCard)
|
||||
}
|
||||
|
||||
private func getStatementTitle(header: String) -> String {
|
||||
var title = ""
|
||||
switch header.uppercased() {
|
||||
case "01":
|
||||
title = "payment".localizeString(string: self.langCode!)
|
||||
case "02":
|
||||
title = "Black List Card"
|
||||
case "03":
|
||||
title = "topup".localizeString(string: self.langCode!)
|
||||
case "04":
|
||||
title = "topup".localizeString(string: self.langCode!)
|
||||
case "05":
|
||||
title = "statementFee".localizeString(string: self.langCode!)
|
||||
case "06":
|
||||
title = "updateBalance".localizeString(string: self.langCode!)
|
||||
case "07":
|
||||
title = "gracePeriod".localizeString(string: self.langCode!)
|
||||
case "10":
|
||||
title = "refund".localizeString(string: self.langCode!)
|
||||
case "20":
|
||||
title = "refund".localizeString(string: self.langCode!)
|
||||
case "22":
|
||||
title = "close".localizeString(string: self.langCode!)
|
||||
case "F0":
|
||||
title = "atu".localizeString(string: self.langCode!)
|
||||
default:
|
||||
break
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
private func getTranscationType(header: String) -> Int {
|
||||
switch header.uppercased() {
|
||||
case "01":
|
||||
return 1
|
||||
case "02":
|
||||
return 2
|
||||
case "03":
|
||||
return 0
|
||||
case "04":
|
||||
return 0
|
||||
case "05":
|
||||
return 1
|
||||
case "06":
|
||||
return 6
|
||||
case "07":
|
||||
return 7
|
||||
case "10":
|
||||
return 10
|
||||
case "20":
|
||||
return 20
|
||||
case "22":
|
||||
return 22
|
||||
default:
|
||||
break
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getTransactionTime(julian: String) -> Date {
|
||||
let dec = julian.hex2decimal()
|
||||
|
||||
let cal = Calendar.current
|
||||
// set to 1st January 1995
|
||||
var dateComponents = DateComponents()
|
||||
dateComponents.year = 1995
|
||||
dateComponents.month = 1
|
||||
dateComponents.day = 1
|
||||
dateComponents.hour = 0
|
||||
dateComponents.minute = 0
|
||||
dateComponents.second = 0
|
||||
|
||||
if let date = cal.date(from: dateComponents) {
|
||||
let newDate = date.addingTimeInterval(Double(dec))
|
||||
return newDate
|
||||
}
|
||||
|
||||
return Date()
|
||||
}
|
||||
|
||||
}
|
||||
463
Emoney Info/Classes/api/UnifiedNfcApi.swift
Executable file
@ -0,0 +1,463 @@
|
||||
import Foundation
|
||||
import CoreNFC
|
||||
|
||||
|
||||
extension CFArray {
|
||||
func toSwiftArray<T>() -> [T] {
|
||||
let array = Array<AnyObject>(_immutableCocoaArray: self)
|
||||
return array.compactMap { $0 as? T }
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary where Key == String, Value == Any {
|
||||
var account: String? {
|
||||
guard let account = self[kSecAttrAccount as String] as? String else {
|
||||
return nil
|
||||
}
|
||||
return account
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public class UnifiedNfcApi {
|
||||
var stationMap: [Int: Station] = [:]
|
||||
|
||||
func parseData() {
|
||||
stationMap = [
|
||||
0: Station(id: 0, name: "PARKIR RESKA", subName: "PARKIR RESKA", latitude: "0", longitude: "0"),
|
||||
1: Station(id: 1, name: "Tanah Abang", subName: "Tanah Abang", latitude: "-6.18574476", longitude: "106.8108382"),
|
||||
67: Station(id: 67, name: "C-Access", subName: "C-Access", latitude: "0", longitude: "0"),
|
||||
257: Station(id: 257, name: "Bogor", subName: "Bogor", latitude: "-6.59561005", longitude: "106.7904379"),
|
||||
258: Station(id: 258, name: "Cilebut", subName: "Cilebut", latitude: "-6.53050343", longitude: "106.8005885"),
|
||||
259: Station(id: 259, name: "Bojonggede", subName: "Bojonggede", latitude: "-6.49326562", longitude: "106.7949173"),
|
||||
260: Station(id: 260, name: "Citayam", subName: "Citayam", latitude: "-6.44879141", longitude: "106.8024588"),
|
||||
261: Station(id: 261, name: "Depok", subName: "Depok", latitude: "-6.40493394", longitude: "106.8172447"),
|
||||
262: Station(id: 262, name: "Depok Baru", subName: "Depok Baru", latitude: "-6.39113047", longitude: "106.821707"),
|
||||
263: Station(id: 263, name: "Pondok Cina", subName: "Pondok Cina", latitude: "-6.36905168", longitude: "106.8322114"),
|
||||
264: Station(id: 264, name: "Univ. Indonesia", subName: "Univ. Indonesia", latitude: "-6.36075528", longitude: "106.8317544"),
|
||||
265: Station(id: 265, name: "Univ. Pancasila", subName: "Univ. Pancasila", latitude: "-6.33894476", longitude: "106.8344241"),
|
||||
272: Station(id: 272, name: "Lenteng Agung", subName: "Lenteng Agung", latitude: "-6.33065157", longitude: "106.8349938"),
|
||||
273: Station(id: 273, name: "Tanjung Barat", subName: "Tanjung Barat", latitude: "-6.30780817", longitude: "106.8388513"),
|
||||
274: Station(id: 274, name: "Pasar Minggu", subName: "Pasar Minggu", latitude: "-6.28440597", longitude: "106.8445384"),
|
||||
275: Station(id: 275, name: "Pasar Minggu Baru", subName: "Pasar Minggu Baru", latitude: "-6.26278132", longitude: "106.8518598"),
|
||||
276: Station(id: 276, name: "Duren Kalibata", subName: "Duren Kalibata", latitude: "-6.25534623", longitude: "106.8550195"),
|
||||
277: Station(id: 277, name: "Cawang", subName: "Cawang", latitude: "-6.24266069", longitude: "106.8588196"),
|
||||
278: Station(id: 278, name: "Tebet", subName: "Tebet", latitude: "-6.22606896", longitude: "106.8583004"),
|
||||
279: Station(id: 279, name: "Manggarai", subName: "Manggarai", latitude: "-6.20992352", longitude: "106.8502129"),
|
||||
280: Station(id: 280, name: "Cikini", subName: "Cikini", latitude: "-6.19856352", longitude: "106.8412599"),
|
||||
281: Station(id: 281, name: "Gondangdia", subName: "Gondangdia", latitude: "-6.18594019", longitude: "106.8325942"),
|
||||
288: Station(id: 288, name: "Juanda", subName: "Juanda", latitude: "-6.16672229", longitude: "106.8304674"),
|
||||
289: Station(id: 289, name: "Sawah Besar", subName: "Sawah Besar", latitude: "-6.16063965", longitude: "106.8276397"),
|
||||
290: Station(id: 290, name: "Mangga Besar", subName: "Mangga Besar", latitude: "-6.14979667", longitude: "106.8269796"),
|
||||
291: Station(id: 291, name: "Jayakarta", subName: "Jayakarta", latitude: "-6.14134112", longitude: "106.8230834"),
|
||||
292: Station(id: 292, name: "Jakarta Kota", subName: "Jakarta Kota", latitude: "-6.13761335", longitude: "106.8146308"),
|
||||
293: Station(id: 293, name: "Bekasi", subName: "Bekasi", latitude: "-6.23614485", longitude: "106.9994173"),
|
||||
294: Station(id: 294, name: "Kranji", subName: "Kranji", latitude: "-6.22433352", longitude: "106.9793992"),
|
||||
295: Station(id: 295, name: "Cakung", subName: "Cakung", latitude: "-6.21929974", longitude: "106.9521357"),
|
||||
296: Station(id: 296, name: "Klender Baru", subName: "Klender Baru", latitude: "-6.21743543", longitude: "106.9396893"),
|
||||
297: Station(id: 297, name: "Buaran", subName: "Buaran", latitude: "-6.21615092", longitude: "106.9283069"),
|
||||
304: Station(id: 304, name: "Klender", subName: "Klender", latitude: "-6.21335877", longitude: "106.8998889"),
|
||||
305: Station(id: 305, name: "Jatinegara", subName: "Jatinegara", latitude: "-6.21513342", longitude: "106.8703259"),
|
||||
313: Station(id: 313, name: "Tangerang", subName: "Tangerang", latitude: "-6.17679787", longitude: "106.63272688"),
|
||||
327: Station(id: 327, name: "Karet", subName: "Karet", latitude: "-6.2008165", longitude: "106.8159002"),
|
||||
328: Station(id: 328, name: "Sudirman", subName: "Sudirman", latitude: "-6.202438", longitude: "106.8234505"),
|
||||
329: Station(id: 329, name: "Tanah Abang", subName: "Tanah Abang", latitude: "-6.18574476", longitude: "106.8108382"),
|
||||
336: Station(id: 336, name: "Palmerah", subName: "Palmerah", latitude: "-6.20740425", longitude: "106.7974463"),
|
||||
337: Station(id: 337, name: "Kebayoran", subName: "Kebayoran", latitude: "-6.23718958", longitude: "106.782542"),
|
||||
338: Station(id: 338, name: "Pondok Ranji", subName: "Pondok Ranji", latitude: "-6.27633762", longitude: "106.7449376"),
|
||||
339: Station(id: 339, name: "Jurang Mangu", subName: "Jurang Mangu", latitude: "-6.28876225", longitude: "106.7291141"),
|
||||
340: Station(id: 340, name: "Sudimara", subName: "Sudimara", latitude: "-6.29694285", longitude: "106.7127952"),
|
||||
341: Station(id: 341, name: "Rawabuntu", subName: "Rawabuntu", latitude: "-6.31500105", longitude: "106.6761968"),
|
||||
342: Station(id: 342, name: "Serpong", subName: "Serpong", latitude: "-6.32004857", longitude: "106.6655717"),
|
||||
343: Station(id: 343, name: "Cisauk", subName: "Cisauk", latitude: "-6.3249995", longitude: "106.6407467"),
|
||||
344: Station(id: 344, name: "Cicayur", subName: "Cicayur", latitude: "-6.32951436", longitude: "106.6189624"),
|
||||
345: Station(id: 345, name: "Parung Panjang", subName: "Parung Panjang", latitude: "-6.34420808", longitude: "106.5698061"),
|
||||
352: Station(id: 352, name: "Cilejit", subName: "Cilejit", latitude: "-6.35434367", longitude: "106.5097328"),
|
||||
353: Station(id: 353, name: "Daru", subName: "Daru", latitude: "-6.33800742", longitude: "106.4923913"),
|
||||
354: Station(id: 354, name: "Tenjo", subName: "Tenjo", latitude: "-6.32725713", longitude: "106.4613542"),
|
||||
355: Station(id: 355, name: "Tigaraksa", subName: "Tigaraksa", latitude: "-6.32846118", longitude: "106.4347451"),
|
||||
356: Station(id: 356, name: "Maja", subName: "Maja", latitude: "-6.33230387", longitude: "106.3965692"),
|
||||
357: Station(id: 357, name: "Citeras", subName: "Citeras", latitude: "-6.33492764", longitude: "106.3327125"),
|
||||
358: Station(id: 358, name: "Rangkasbitung", subName: "Rangkasbitung", latitude: "-6.3526711", longitude: "106.251502"),
|
||||
374: Station(id: 374, name: "Bekasi Timur", subName: "Bekasitimur", latitude: "-6.246845", longitude: "107.0181248"),
|
||||
376: Station(id: 376, name: "Cikarang", subName: "Cikarang", latitude: "-6.2553926", longitude: "107.1451293")
|
||||
]
|
||||
}
|
||||
var langCode : String?
|
||||
var apduRunner = ApduRunner()
|
||||
|
||||
public init() {
|
||||
langCode = Locale.current.languageCode
|
||||
parseData()
|
||||
}
|
||||
|
||||
func setCallback(apduCallback : ApduCallback){
|
||||
apduRunner.setApduCallback(callback: apduCallback)
|
||||
apduRunner.setUnifiedNfcApi(nfcApi: self)
|
||||
}
|
||||
|
||||
func setApduRunner(apRunner : ApduRunner){
|
||||
self.apduRunner = apRunner
|
||||
}
|
||||
|
||||
public func checkIfNfcSupported() -> Bool {
|
||||
return NFCTagReaderSession.readingAvailable
|
||||
}
|
||||
|
||||
public func searchCard(){
|
||||
apduRunner.startScan()
|
||||
}
|
||||
|
||||
public func checkCard(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRIZZI_INIT_APDU, completionHandler: {response in
|
||||
if (response.sw1 == 0x91 && response.sw2 == 0x00){
|
||||
debugLog("brizzi card")
|
||||
let brizzi = BrizziApi()
|
||||
brizzi.setApduRunner(apRunner: self.apduRunner)
|
||||
brizzi.getUid()
|
||||
} else {
|
||||
self.checkNext01()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func checkFelicaCard(tag: NFCFeliCaTag){
|
||||
readFelicaCard(tag: tag)
|
||||
}
|
||||
|
||||
func readFelicaCard(tag: NFCFeliCaTag){
|
||||
let kmt = Emoney()
|
||||
|
||||
let serviceCode = Data([0x0B, 0x30])
|
||||
let blockList = Data([0x80, 0x00])
|
||||
|
||||
tag.readWithoutEncryption(
|
||||
serviceCodeList: [serviceCode],
|
||||
blockList: [blockList]
|
||||
) { (status1, status2, blockData, error) in
|
||||
|
||||
if let error = error {
|
||||
self.apduRunner.nfcApi?.stopCheckCard(message: "Gagal membaca: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
// Cek status keberhasilan dari kartu (0x00 0x00 berarti sukses)
|
||||
guard status1 == 0x00 && status2 == 0x00 else {
|
||||
self.apduRunner.nfcApi?.stopCheckCard(message: "Error Status: \(status1) \(status2)")
|
||||
return
|
||||
}
|
||||
for (index, data) in blockData.enumerated() {
|
||||
debugLog("Data Blok \(index): \(data.map { String(format: "%02X", $0) }.joined())")
|
||||
if let cardNumberString = String(data: data, encoding: .utf8) {
|
||||
kmt.setCardLabel("KMT")
|
||||
kmt.setCardNumber(cardNumberString)
|
||||
self.readFelicaBalance(tag: tag, kmt: kmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readFelicaBalance(tag: NFCFeliCaTag, kmt: Emoney){
|
||||
let serviceCode = Data([0x17, 0x10])
|
||||
let blockList = Data([0x80, 0x00])
|
||||
|
||||
tag.readWithoutEncryption(
|
||||
serviceCodeList: [serviceCode],
|
||||
blockList: [blockList]
|
||||
) { (status1, status2, blockData, error) in
|
||||
if let error = error {
|
||||
self.apduRunner.nfcApi?.stopCheckCard(message: "Gagal membaca: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
if status1 == 0x00 && status2 == 0x00 {
|
||||
let cardBalance = [UInt8](blockData[0])
|
||||
var y: Int = 0
|
||||
for x in 0..<4 {
|
||||
y += Int(cardBalance[x]) << (x * 8)
|
||||
}
|
||||
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale(identifier: "id_ID")
|
||||
formatter.numberStyle = .decimal
|
||||
debugLog("balance")
|
||||
kmt.setBalance(y)
|
||||
if let balance = formatter.string(from: NSNumber(value: y)) {
|
||||
debugLog("Saldo: \(balance)") // Hasil contoh: "67.305.985"
|
||||
}
|
||||
self.readFelicaCardHistory(tag: tag, kmt: kmt)
|
||||
// kmt.setTampilRiwayat(false)
|
||||
// self.apduRunner.callback?.complete(emoney: kmt)
|
||||
// self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
// self.apduRunner.invalidateSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readFelicaCardHistory(tag: NFCFeliCaTag, kmt: Emoney){
|
||||
let serviceCode = Data([0x0F, 0x20])
|
||||
|
||||
// 2. Buat daftar 15 blok (Blok 0 sampai 14) secara otomatis
|
||||
var blockList = [Data]()
|
||||
for i in 0..<15 {
|
||||
blockList.append(Data([0x80, UInt8(i)]))
|
||||
}
|
||||
|
||||
// 3. Panggil fungsi pembacaan
|
||||
tag.readWithoutEncryption(
|
||||
serviceCodeList: [serviceCode],
|
||||
blockList: blockList
|
||||
) { (status1, status2, blockData, error) in
|
||||
var riwayatList: [RiwayatCard] = []
|
||||
if let error = error {
|
||||
debugLog("Error: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
if status1 == 0x00 && status2 == 0x00 {
|
||||
debugLog("Berhasil membaca 15 blok!")
|
||||
// blockData akan berisi array of Data, masing-masing 16 byte
|
||||
for (index, data) in blockData.enumerated() {
|
||||
let riwayat = RiwayatCard()
|
||||
var normal = true
|
||||
let subId = data.subdata(in: 8..<10)
|
||||
|
||||
let uid = self.convert(bytes: [UInt8](subId))
|
||||
debugLog("station: \(uid)")
|
||||
|
||||
debugLog("Blok \(index): \(data.map { String(format: "%02X", $0) }.joined())")
|
||||
if (uid == 0){
|
||||
normal = false
|
||||
}
|
||||
if (data.count > 10){
|
||||
let type = data[10]
|
||||
debugLog(type)
|
||||
switch type {
|
||||
case 0x01:
|
||||
riwayat.setProsesTipe(1)
|
||||
riwayat.setTitle("payment".localizeString(string: self.langCode!))
|
||||
debugLog("Pembayaran")
|
||||
case 0x00, 0x03:
|
||||
riwayat.setProsesTipe(0)
|
||||
riwayat.setTitle("topup".localizeString(string: self.langCode!))
|
||||
debugLog("Topup")
|
||||
default:
|
||||
riwayat.setProsesTipe(1)
|
||||
riwayat.setTitle("payment".localizeString(string: self.langCode!))
|
||||
debugLog("Other")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if let station = self.stationMap[uid]{
|
||||
debugLog("station", station.name)
|
||||
riwayat.setLocationName(station.name.uppercased(with: .autoupdatingCurrent))
|
||||
}
|
||||
// let station = self.stationMap[uid]
|
||||
// print("station", station!)
|
||||
// riwayat.setPlace(self.stationMap[uid]!.name)
|
||||
if (normal){
|
||||
|
||||
let subData = data.subdata(in: 0..<4)
|
||||
|
||||
let date = self.getDate(data: [UInt8](subData))
|
||||
riwayat.setTransactionTime(date!)
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "id_ID") // Format Indonesia
|
||||
formatter.dateFormat = "dd MMMM yyyy, HH:mm"
|
||||
|
||||
let dateString = formatter.string(from: date!)
|
||||
debugLog("Hasil Konversi: \(dateString)")
|
||||
|
||||
let amn = data.subdata(in: 4..<8)
|
||||
let amount = amn.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
|
||||
|
||||
//print("Amount: \(amount)")
|
||||
|
||||
// if (data.count > 10){
|
||||
// let type = data[10]
|
||||
// print(type)
|
||||
// switch type {
|
||||
// case 0x01:
|
||||
// print("Pembayaran")
|
||||
// case 0x00:
|
||||
// print("Topup")
|
||||
// default:
|
||||
// print("Other")
|
||||
// }
|
||||
//
|
||||
// let subId = data.subdata(in: 8..<10)
|
||||
//
|
||||
// let uid = self.convert(bytes: [UInt8](subId))
|
||||
// print("station: \(uid)")
|
||||
// }
|
||||
riwayat.setAmount(Int(amount))
|
||||
let nformatter = NumberFormatter()
|
||||
nformatter.locale = Locale(identifier: "id_ID")
|
||||
nformatter.numberStyle = .decimal
|
||||
if let balance = nformatter.string(from: NSNumber(value: amount)) {
|
||||
debugLog("amount: \(balance)") // Hasil contoh: "67.305.985"
|
||||
}
|
||||
debugLog("")
|
||||
} else {
|
||||
debugLog("RESKA PARKIR")
|
||||
|
||||
let stringData = data.map { String(format: "%02X", $0) }.joined()
|
||||
|
||||
let inputFormatter = DateFormatter()
|
||||
inputFormatter.dateFormat = "ddMMyyyyHHmmssSS"
|
||||
|
||||
let finalData = stringData.prefix(16)
|
||||
// 2. Konversi String ke objek Date
|
||||
if let date = inputFormatter.date(from: String(finalData)) {
|
||||
|
||||
// 3. Inisialisasi Formatter untuk mengubah ke format tujuan
|
||||
let outputFormatter = DateFormatter()
|
||||
outputFormatter.dateFormat = "dd MMM yyyy HH:mm"
|
||||
|
||||
let result = outputFormatter.string(from: date)
|
||||
riwayat.setTransactionTime(date)
|
||||
debugLog(result) // Hasil: 29-01-2026 16:00:44
|
||||
} else {
|
||||
debugLog("Format string tidak cocok")
|
||||
}
|
||||
let amn = data.subdata(in: 8..<12)
|
||||
let amount = amn.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
|
||||
riwayat.setAmount(Int(amount))
|
||||
//print("Amount: \(amount)")
|
||||
|
||||
|
||||
let nformatter = NumberFormatter()
|
||||
nformatter.locale = Locale(identifier: "id_ID")
|
||||
nformatter.numberStyle = .decimal
|
||||
if let balance = nformatter.string(from: NSNumber(value: amount)) {
|
||||
debugLog("amount: \(balance)") // Hasil contoh: "67.305.985"
|
||||
}
|
||||
}
|
||||
debugLog("")
|
||||
riwayatList.append(riwayat)
|
||||
}
|
||||
kmt.setRiwayatList(riwayatList)
|
||||
kmt.setTampilRiwayat(true)
|
||||
self.apduRunner.callback?.complete(emoney: kmt)
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
} else {
|
||||
debugLog("Gagal. Status: \(status1), \(status2)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDate(data: [UInt8]) -> Date? {
|
||||
// 1. Tentukan TimeZone Jakarta
|
||||
let timeZone = TimeZone(identifier: "Asia/Jakarta")!
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = timeZone
|
||||
|
||||
// 2. Set tanggal dasar (1 Januari 2000, 07:00:00)
|
||||
var components = DateComponents()
|
||||
components.year = 2000
|
||||
components.month = 1
|
||||
components.day = 1
|
||||
components.hour = 7
|
||||
components.minute = 0
|
||||
components.second = 0
|
||||
|
||||
guard let baseDate = calendar.date(from: components) else { return nil }
|
||||
|
||||
// 3. Ambil selisih detik dari data byte
|
||||
let secondsToAdd = convert(bytes: data)
|
||||
|
||||
// 4. Tambahkan detik ke baseDate
|
||||
let finalDate = calendar.date(byAdding: .second, value: secondsToAdd, to: baseDate)
|
||||
|
||||
return finalDate
|
||||
}
|
||||
|
||||
func convert(bytes: [UInt8]) -> Int {
|
||||
switch bytes.count {
|
||||
case 0:
|
||||
fatalError("Data kosong")
|
||||
case 1:
|
||||
return Int(bytes[0])
|
||||
case 2:
|
||||
// Big-endian: (byte[0] << 8) | byte[1]
|
||||
return (Int(bytes[0]) << 8) | Int(bytes[1])
|
||||
case 3:
|
||||
// (byte[0] << 16) | (byte[1] << 8) | byte[2]
|
||||
return (Int(bytes[0]) << 16) | (Int(bytes[1]) << 8) | Int(bytes[2])
|
||||
default:
|
||||
// Padanan ByteBuffer.wrap(bArr).getInt() (Big-endian)
|
||||
return Int(bytes.withUnsafeBytes { $0.load(as: Int32.self).bigEndian })
|
||||
}
|
||||
}
|
||||
|
||||
public func stopCheckCard(message : String){
|
||||
apduRunner.sessionEx?.invalidate(errorMessage: message)
|
||||
}
|
||||
|
||||
private func checkNext01(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.FLAZZ_INIT_APDU, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
debugLog("flazz card")
|
||||
let flazz = BcaFlazzApi()
|
||||
flazz.setApduRunner(apRunner: self.apduRunner)
|
||||
flazz.checkFlazzCard()
|
||||
} else {
|
||||
self.checkNext02()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func checkNext02(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.TAPCASH_INIT_APDU, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
debugLog("tapcash card")
|
||||
let tapCash = TapCashApi()
|
||||
tapCash.setApduRunner(apRunner: self.apduRunner)
|
||||
tapCash.checkBalance()
|
||||
} else {
|
||||
self.checkNext03()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func checkNext03(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.EMONEY_INIT_APDU, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
debugLog("emoney card")
|
||||
let emoney = MandiriEmoneyApi()
|
||||
emoney.setApduRunner(apRunner: self.apduRunner)
|
||||
emoney.getCardNumber()
|
||||
} else {
|
||||
self.checkNext04()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func checkNext04(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.JACKCARD_INIT_APDU, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
debugLog("jack card")
|
||||
let jack = JackCardApi()
|
||||
jack.setApduRunner(apRunner: self.apduRunner)
|
||||
jack.getBalance(resp: response.getData())
|
||||
} else {
|
||||
self.checkNext05()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func checkNext05(){
|
||||
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.MEGA_APDU01, completionHandler: {response in
|
||||
if (response.sw1 == 0x90 && response.sw2 == 0x00){
|
||||
debugLog("megacash")
|
||||
let mega = MegaCashApi()
|
||||
mega.setApduRunner(apRunner: self.apduRunner)
|
||||
mega.getBalance(resp: response.getData())
|
||||
} else {
|
||||
self.apduRunner.invalidateSession(msg: "Card not supported")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
16
Emoney Info/Classes/api/callback/ApduCallback.swift
Executable file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// ApduCallback.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 24/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreNFC
|
||||
|
||||
protocol ApduCallback {
|
||||
func connected(unifiedNfcApi : UnifiedNfcApi)
|
||||
func felicaConnected(unifiedNfcApi : UnifiedNfcApi, tag : NFCFeliCaTag)
|
||||
func complete(emoney: Emoney)
|
||||
func failed(error: NSError)
|
||||
}
|
||||
38
Emoney Info/Classes/api/nfc/ApduResponse.swift
Executable file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// ApduResponse.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 24/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ApduResponse {
|
||||
var data : Data?
|
||||
var sw1 : UInt8?
|
||||
var sw2 : UInt8?
|
||||
|
||||
func setData(_data : Data){
|
||||
self.data = _data
|
||||
}
|
||||
|
||||
func setSw1(_sw1 : UInt8){
|
||||
self.sw1 = _sw1
|
||||
}
|
||||
|
||||
func setSw2(_sw2 : UInt8){
|
||||
self.sw2 = _sw2
|
||||
}
|
||||
|
||||
func getSw1() -> UInt8{
|
||||
return sw1!
|
||||
}
|
||||
|
||||
func getSw2() -> UInt8{
|
||||
return sw2!
|
||||
}
|
||||
|
||||
func getData() -> Data{
|
||||
return data!
|
||||
}
|
||||
}
|
||||
126
Emoney Info/Classes/api/nfc/ApduRunner.swift
Executable file
@ -0,0 +1,126 @@
|
||||
import Foundation
|
||||
import CoreNFC
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension NFCISO7816APDU {
|
||||
func toHexString() -> String {
|
||||
let dataFieldInHex = (self.data ?? Data(_ : [])).hexEncodedString()
|
||||
return String(format:"%02X %02X %02X %02X", self.instructionClass, self.instructionCode, self.p1Parameter, self.p2Parameter) + " " + dataFieldInHex + String(format:"%02X", self.expectedResponseLength) + " "
|
||||
}
|
||||
}
|
||||
|
||||
typealias CompletionHandler = (_ response:ApduResponse) -> Void
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public class ApduRunner: NSObject, NFCTagReaderSessionDelegate {
|
||||
public static let NFC_TAG_CONNECTED_EVENT:String = "nfcTagConnected"
|
||||
|
||||
var sessionEx: NFCTagReaderSession?
|
||||
var callback: ApduCallback?
|
||||
var nfcApi : UnifiedNfcApi?
|
||||
|
||||
func setApduCallback(callback : ApduCallback) {
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
func setUnifiedNfcApi(nfcApi : UnifiedNfcApi) {
|
||||
self.nfcApi = nfcApi
|
||||
}
|
||||
|
||||
func startScan() {
|
||||
let langCode = Locale.current.languageCode
|
||||
self.sessionEx = NFCTagReaderSession(pollingOption: [.iso14443, .iso18092], delegate: self)
|
||||
self.sessionEx?.alertMessage = "scanMessage".localizeString(string: langCode!)
|
||||
self.sessionEx?.begin()
|
||||
}
|
||||
|
||||
|
||||
public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
|
||||
guard self.callback != nil else {
|
||||
debugLog("NfcCallback is empty.")
|
||||
return
|
||||
}
|
||||
guard self.sessionEx != nil else {
|
||||
return
|
||||
}
|
||||
guard tags.count > 0 else {
|
||||
debugLog("Nfc Tag???.")
|
||||
return
|
||||
}
|
||||
if case NFCTag.iso7816(_) = tags.first! {
|
||||
sessionEx?.connect(to: tags.first!) { [self] (error: Error?) in
|
||||
if let err = error {
|
||||
debugLog("Error connecting to Nfc Tag" + err.localizedDescription)
|
||||
return
|
||||
}
|
||||
debugLog("Nfc Tag is connected.")
|
||||
if (self.callback != nil){
|
||||
self.callback!.connected(unifiedNfcApi: self.nfcApi!)
|
||||
}
|
||||
}
|
||||
} else if case .feliCa(let feliCaTag) = tags.first! {
|
||||
sessionEx?.connect(to: tags.first!) { [self] (error: Error?) in
|
||||
if let err = error {
|
||||
debugLog("Error connecting to Nfc Tag" + err.localizedDescription)
|
||||
return
|
||||
}
|
||||
// felicaTag = feliCaTag
|
||||
debugLog("Felica is connected.")
|
||||
if (self.callback != nil){
|
||||
debugLog("Felica is connected 2.")
|
||||
self.callback!.felicaConnected(unifiedNfcApi: self.nfcApi!, tag: feliCaTag)
|
||||
}
|
||||
// self.sendFelicaCommand(tag: feliCaTag, session: sessionEx!)
|
||||
|
||||
// let idm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined()
|
||||
// let hexString = "0F06" + idm + "010B30018000"
|
||||
// print("hex: \(hexString)")
|
||||
// if let apduData = hexString.hexToData() {
|
||||
// feliCaTag.sendFeliCaCommand(commandPacket: apduData)
|
||||
// }
|
||||
|
||||
// if (self.callback != nil){
|
||||
// self.callback!.connected(felicaNfcApi: self.felicaNfcApi!)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func exchangeApdu(apduCommand: NFCISO7816APDU, completionHandler: @escaping CompletionHandler) {
|
||||
if case let NFCTag.iso7816(nfcTag) = self.sessionEx!.connectedTag! {
|
||||
nfcTag.sendCommand(apdu: apduCommand) { (response: Data, sw1: UInt8, sw2: UInt8, error: Error?)
|
||||
in
|
||||
let resp = ApduResponse()
|
||||
debugLog("SW1-SW2: " + String(format: "%02X, %02X", sw1, sw2))
|
||||
resp.setSw1(_sw1: sw1)
|
||||
resp.setSw2(_sw2: sw2)
|
||||
resp.setData(_data: response)
|
||||
completionHandler(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func invalidateSession() {
|
||||
sessionEx?.invalidate()
|
||||
}
|
||||
|
||||
func invalidateSession(msg : String) {
|
||||
sessionEx?.invalidate(errorMessage: msg)
|
||||
}
|
||||
|
||||
public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
|
||||
debugLog("Nfc session is active")
|
||||
}
|
||||
|
||||
public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
|
||||
debugLog("Error happend: " + error.localizedDescription)
|
||||
NotificationCenter.default.post(name: Notification.Name("stopTimer"), object: nil)
|
||||
if ((sessionEx?.isReady) != nil){
|
||||
self.invalidateSession(msg: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
66
Emoney Info/Classes/api/nfc/Emoney.swift
Executable file
@ -0,0 +1,66 @@
|
||||
//
|
||||
// Emoney.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 26/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Emoney {
|
||||
private var balance: Int = 0
|
||||
private var cardNumber: String?
|
||||
private var cardType: String?
|
||||
private var riwayatList: [RiwayatCard]?
|
||||
private var tampilRiwayat: Bool = false
|
||||
private var cardLabel: String?
|
||||
|
||||
func getBalance() -> Int {
|
||||
return self.balance
|
||||
}
|
||||
|
||||
func getCardNumber() -> String {
|
||||
return self.cardNumber!
|
||||
}
|
||||
|
||||
func getCardType() -> String {
|
||||
return self.cardType!
|
||||
}
|
||||
|
||||
func getRiwayatList() -> [RiwayatCard] {
|
||||
return self.riwayatList!
|
||||
}
|
||||
|
||||
func isTampilRiwayat() -> Bool {
|
||||
return self.tampilRiwayat
|
||||
}
|
||||
|
||||
func setBalance(_ j: Int) {
|
||||
self.balance = j
|
||||
}
|
||||
|
||||
func setCardNumber(_ str: String) {
|
||||
self.cardNumber = str
|
||||
}
|
||||
|
||||
func setCardType(_ str: String) {
|
||||
self.cardType = str
|
||||
}
|
||||
|
||||
func setRiwayatList(_ list: [RiwayatCard]) {
|
||||
self.riwayatList = list
|
||||
}
|
||||
|
||||
func setTampilRiwayat(_ z: Bool) {
|
||||
self.tampilRiwayat = z
|
||||
}
|
||||
|
||||
func setCardLabel(_ str: String) {
|
||||
self.cardLabel = str
|
||||
}
|
||||
|
||||
func getCardLabel() -> String {
|
||||
return self.cardLabel!
|
||||
}
|
||||
|
||||
}
|
||||
124
Emoney Info/Classes/api/nfc/RiwayatCard.swift
Executable file
@ -0,0 +1,124 @@
|
||||
//
|
||||
// RiwayatCard.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 25/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class RiwayatCard
|
||||
//: Comparable
|
||||
{
|
||||
// static func < (lhs: RiwayatCard, rhs: RiwayatCard) -> Bool {
|
||||
// return lhs.valueToCompare > rhs.valueToCompare
|
||||
// }
|
||||
//
|
||||
// static func == (lhs: RiwayatCard, rhs: RiwayatCard) -> Bool {
|
||||
// return false
|
||||
// }
|
||||
|
||||
private var amount: Int = 0
|
||||
private var desk: String?
|
||||
private var jam: String?
|
||||
private var locationId: String?
|
||||
private var locationName: String?
|
||||
private var prosesTipe: Int = 0
|
||||
private var tanggal: String?
|
||||
private var title: String?
|
||||
private var transactionTime : Date?
|
||||
private var ads : Bool = false
|
||||
|
||||
// private var valueToCompare: Date
|
||||
|
||||
// init(valueToCompare: Date) {
|
||||
// self.valueToCompare = valueToCompare
|
||||
// }
|
||||
|
||||
func getAmount() -> Int {
|
||||
return self.amount
|
||||
}
|
||||
|
||||
func setAmount(_ amount: Int) {
|
||||
self.amount = amount
|
||||
}
|
||||
|
||||
func getDesk() -> String? {
|
||||
return self.desk
|
||||
}
|
||||
|
||||
func getJam() -> String? {
|
||||
return self.jam
|
||||
}
|
||||
|
||||
func setJam(_ str: String) {
|
||||
self.jam = str
|
||||
}
|
||||
|
||||
func getLocationId() -> String? {
|
||||
return self.locationId
|
||||
}
|
||||
|
||||
func setLocationId(_ str: String) {
|
||||
self.locationId = str
|
||||
}
|
||||
|
||||
func getLocationName() -> String? {
|
||||
return self.locationName
|
||||
}
|
||||
|
||||
func setLocationName(_ str: String) {
|
||||
self.locationName = str
|
||||
}
|
||||
|
||||
func getProsesTipe() -> Int {
|
||||
return self.prosesTipe
|
||||
}
|
||||
|
||||
func setProsesTipe(_ proc: Int) {
|
||||
self.prosesTipe = proc
|
||||
}
|
||||
|
||||
func getTanggal() -> String? {
|
||||
return self.tanggal
|
||||
}
|
||||
|
||||
func setTanggal(_ str: String) {
|
||||
self.tanggal = str
|
||||
}
|
||||
|
||||
func getTitle() -> String? {
|
||||
return self.title
|
||||
}
|
||||
|
||||
func setTitle(_ str: String) {
|
||||
self.title = str
|
||||
}
|
||||
|
||||
func setTransactionTime(_ date : Date){
|
||||
self.transactionTime = date
|
||||
}
|
||||
|
||||
func getTransationTime() -> Date?{
|
||||
return self.transactionTime
|
||||
}
|
||||
|
||||
func setAds(_ ads : Bool){
|
||||
self.ads = ads
|
||||
}
|
||||
|
||||
func isAds() -> Bool{
|
||||
return self.ads
|
||||
}
|
||||
|
||||
|
||||
// func setValueToCompare(_ valueToCompare: Date) {
|
||||
// self.valueToCompare = valueToCompare
|
||||
// }
|
||||
//
|
||||
// func getValueToCompare() -> Date {
|
||||
// return self.valueToCompare
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
109
Emoney Info/Classes/api/utils/BrizziSamHelper.swift
Executable file
@ -0,0 +1,109 @@
|
||||
//
|
||||
// BrizziSamHelper.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 27/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
class BrizziSamHelper {
|
||||
public var encryptedKey: String?
|
||||
public var authKey = "0000030080000000"
|
||||
public var keyCard: String?
|
||||
public var random = ""
|
||||
|
||||
static func encryptDeSeDe(_ str: String, _ str2: String, _ str3: String) -> Data? {
|
||||
var key = str2
|
||||
if key.count != 48 {
|
||||
if key.count == 32 {
|
||||
key += key.prefix(16)
|
||||
} else if key.count == 16 {
|
||||
key += key + key
|
||||
} else {
|
||||
key = "00000000000000000000000000000000"
|
||||
}
|
||||
}
|
||||
let keyData = key.hex2byte()
|
||||
let ivData = str3.hex2byte()
|
||||
|
||||
return crypt(input: str.hex2byte(), keyData: keyData, ivData: ivData, operation: CCOperation(kCCEncrypt))
|
||||
}
|
||||
|
||||
static func decryptDeSeDe(_ datas: Data) -> Data? {
|
||||
let keyData = ("C152153D5807784C721A433B5B59636D" + "C152153D5807784C").hex2byte()
|
||||
let ivData = ("0000000000000000").hex2byte()
|
||||
|
||||
return crypt(input: datas, keyData: keyData, ivData: ivData, operation: CCOperation(kCCDecrypt))
|
||||
}
|
||||
|
||||
static func mix(_ bArr: [UInt8], _ bArr2: [UInt8]) -> [UInt8] {
|
||||
guard !bArr2.isEmpty else {
|
||||
fatalError("empty security key")
|
||||
}
|
||||
|
||||
var bArr3 = [UInt8](repeating: 0, count: bArr.count)
|
||||
var i = 0
|
||||
for y in 0..<bArr.count {
|
||||
bArr3[y] = bArr[y] ^ bArr2[i]
|
||||
i += 1
|
||||
if i >= bArr2.count {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
return bArr3
|
||||
}
|
||||
|
||||
static func decrypt(_ data: String, _ key: String) -> Data? {
|
||||
let keyData = key.hex2byte()
|
||||
|
||||
return crypt(input: data.hex2byte(), keyData: keyData, ivData: nil, operation: CCOperation(kCCDecrypt))
|
||||
}
|
||||
|
||||
static func encrypt(_ str: String, _ key: String) -> Data? {
|
||||
let substring = String(key.prefix(16))
|
||||
guard let decryptedData = decrypt(str, substring) else {
|
||||
return ("").hex2byte()
|
||||
}
|
||||
|
||||
let a9 = decryptedData.hexEncodedString()
|
||||
let keyData = String(key.dropFirst(16).prefix(16)).hex2byte()
|
||||
|
||||
guard let encryptedData = crypt(input: a9.hex2byte(), keyData: keyData, ivData: nil, operation: CCOperation(kCCEncrypt)) else {
|
||||
return ("").hex2byte()
|
||||
}
|
||||
|
||||
guard let finalDecryptedData = decrypt(encryptedData.hexEncodedString(), substring) else {
|
||||
return ("").hex2byte()
|
||||
}
|
||||
|
||||
return finalDecryptedData
|
||||
}
|
||||
|
||||
func generateSamRandom() -> String {
|
||||
let sam = BrizziSamHelper.mix(((BrizziSamHelper.encrypt(self.keyCard!, self.random)!).hexEncodedString()).hex2byte().bytes, ("0000000000000000").hex2byte().bytes).hexString().subString(from: 0, to: 16)
|
||||
let sams = sam[sam.index(sam.startIndex, offsetBy: 2)..<sam.index(sam.startIndex, offsetBy: 16)] + sam[sam.startIndex..<sam.index(sam.startIndex, offsetBy: 2)]
|
||||
let result = (BrizziSamHelper.encrypt((BrizziSamHelper.mix(("1122334455667788").hex2byte().bytes, (self.keyCard!).hex2byte().bytes).hexString()), self.random)!).hexEncodedString().subString(from: 0, to: 16)
|
||||
|
||||
return result + (BrizziSamHelper.encrypt((BrizziSamHelper.mix(String(sams).hex2byte().bytes, result.hex2byte().bytes)).hexString(), self.random)!).hexEncodedString()
|
||||
}
|
||||
|
||||
private static func crypt(input: Data, keyData: Data, ivData: Data?, operation: CCOperation) -> Data? {
|
||||
var outLength = Int(0)
|
||||
var outBytes = [UInt8](repeating: 0, count: input.count + kCCBlockSize3DES)
|
||||
var status: CCCryptorStatus
|
||||
|
||||
if let ivData = ivData {
|
||||
status = CCCrypt(operation, CCAlgorithm(kCCAlgorithm3DES), CCOptions(kCCOptionPKCS7Padding), keyData.bytes, kCCKeySize3DES, ivData.bytes, input.bytes, input.count, &outBytes, outBytes.count, &outLength)
|
||||
} else {
|
||||
status = CCCrypt(operation, CCAlgorithm(kCCAlgorithmDES), CCOptions(kCCOptionPKCS7Padding), keyData.bytes, kCCKeySizeDES, nil, input.bytes, input.count, &outBytes, outBytes.count, &outLength)
|
||||
}
|
||||
|
||||
guard status == kCCSuccess else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Data(bytes: outBytes, count: outLength)
|
||||
}
|
||||
}
|
||||
41
Emoney Info/Classes/api/utils/ByteArrayAndHexHelper.swift
Executable file
@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public class ByteArrayAndHexHelper {
|
||||
|
||||
public static func digitalStrIntoAsciiUInt8Array(digitalStr : String) -> [UInt8]{
|
||||
var bytes = [UInt8]()
|
||||
for s in digitalStr {
|
||||
if let byte = UInt8(String(s)) {
|
||||
bytes.append(0x30 + byte)
|
||||
}
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
public static func hexStrToUInt8Array(hexStr: String) -> [UInt8] {
|
||||
var startIndex = hexStr.startIndex
|
||||
return (0..<hexStr.count/2).compactMap { _ in
|
||||
let endIndex = hexStr.index(after: startIndex)
|
||||
defer { startIndex = hexStr.index(after: endIndex) }
|
||||
return UInt8(hexStr[startIndex...endIndex], radix: 16)
|
||||
}
|
||||
}
|
||||
|
||||
public static func hex(from string: String) -> Data {
|
||||
.init(stride(from: 0, to: string.count, by: 2).map {
|
||||
string[string.index(string.startIndex, offsetBy: $0) ... string.index(string.startIndex, offsetBy: $0 + 1)]
|
||||
}.map {
|
||||
UInt8($0, radix: 16)!
|
||||
})
|
||||
}
|
||||
|
||||
public static func makeShort(src: [UInt8], srcOff : Int) -> Int {
|
||||
// if (srcOff < 0 || src.length < (srcOff + 2))
|
||||
// throw new IllegalArgumentException("Bad args!");
|
||||
let b0 = Int(src[srcOff] & 0xFF);
|
||||
let b1 = Int(src[srcOff + 1] & 0xFF);
|
||||
return (b0 << 8) + b1
|
||||
}
|
||||
|
||||
}
|
||||
14
Emoney Info/Classes/extensions/Array.swift
Executable file
@ -0,0 +1,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Array {
|
||||
|
||||
subscript (range r: Range<Int>) -> Array {
|
||||
return Array(self[r])
|
||||
}
|
||||
|
||||
|
||||
subscript (range r: ClosedRange<Int>) -> Array {
|
||||
return Array(self[r])
|
||||
}
|
||||
}
|
||||
35
Emoney Info/Classes/extensions/Data.swift
Executable file
@ -0,0 +1,35 @@
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
extension Data {
|
||||
public var bytes: [UInt8] {
|
||||
return [UInt8](self)
|
||||
}
|
||||
|
||||
func hexEncodedString() -> String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
func makeDigitalString() -> String {
|
||||
//todo: check numeric
|
||||
var s: String = ""
|
||||
for i in 0...bytes.count-1 {
|
||||
if (bytes[i] >= 0 && bytes[i] <= 9) {
|
||||
s += String(bytes[i])
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension UISwitch {
|
||||
|
||||
func setOnValueChangeListener(onValueChanged :@escaping () -> Void){
|
||||
self.addAction(UIAction(){ action in
|
||||
|
||||
onValueChanged()
|
||||
|
||||
}, for: .valueChanged)
|
||||
}
|
||||
}
|
||||
261
Emoney Info/Classes/extensions/String.swift
Executable file
@ -0,0 +1,261 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
extension StringProtocol {
|
||||
func substring<S: StringProtocol>(from start: S, options: String.CompareOptions = []) -> SubSequence? {
|
||||
guard let lower = range(of: start, options: options)?.upperBound
|
||||
else { return nil }
|
||||
return self[lower...]
|
||||
}
|
||||
func substring<S: StringProtocol, T: StringProtocol>(from start: S, to end: T, options: String.CompareOptions = []) -> SubSequence? {
|
||||
guard let lower = range(of: start, options: options)?.upperBound,
|
||||
let upper = self[lower...].range(of: end, options: options)?.lowerBound
|
||||
else { return nil }
|
||||
return self[lower..<upper]
|
||||
}
|
||||
|
||||
func index(from: Int) -> Index {
|
||||
return self.index(startIndex, offsetBy: from)
|
||||
}
|
||||
|
||||
func substring(with r: Range<Int>) -> String {
|
||||
let startIndex = index(from: r.lowerBound)
|
||||
let endIndex = index(from: r.upperBound)
|
||||
return String(self[startIndex..<endIndex])
|
||||
}
|
||||
|
||||
func hex2decimal() -> Int {
|
||||
Int(self, radix: 16)!
|
||||
}
|
||||
|
||||
func hex2bin() -> String {
|
||||
return String(Int(self, radix: 16)!, radix: 2)
|
||||
}
|
||||
|
||||
func bin2decimal() -> Int {
|
||||
return Int(self, radix: 2)!
|
||||
}
|
||||
|
||||
func bin2hex() -> String {
|
||||
return String(Int(self, radix: 2)!, radix: 16)
|
||||
}
|
||||
|
||||
func secondComplementsAmount() -> Int{
|
||||
let bin = self.hex2bin()
|
||||
let characters = Array(bin)
|
||||
var firstComplement = ""
|
||||
for c in characters {
|
||||
if (c == "0"){
|
||||
firstComplement.append("1")
|
||||
} else {
|
||||
firstComplement.append("0")
|
||||
}
|
||||
}
|
||||
let flipped = Array(firstComplement)
|
||||
var isFinish = false
|
||||
var secondComplement = ""
|
||||
for index in stride(from: firstComplement.count - 1, through: 0, by: -1) {
|
||||
let c = flipped[index]
|
||||
if (!isFinish){
|
||||
if (c == "0"){
|
||||
secondComplement.append("1")
|
||||
isFinish = true
|
||||
} else {
|
||||
secondComplement.append("0")
|
||||
}
|
||||
} else {
|
||||
secondComplement.append(c)
|
||||
}
|
||||
|
||||
}
|
||||
let second = Array(secondComplement)
|
||||
var flipback = ""
|
||||
for index in stride(from: secondComplement.count - 1, through: 0, by: -1) {
|
||||
let c = second[index]
|
||||
flipback.append(c)
|
||||
}
|
||||
let binhex = flipback.bin2hex()
|
||||
return binhex.hex2decimal()
|
||||
}
|
||||
|
||||
func formatCardNumber() -> String {
|
||||
let chars = Array(self)
|
||||
var cardNumber = ""
|
||||
for i in 0..<chars.count {
|
||||
cardNumber.append(chars[i])
|
||||
if (i == 3) {
|
||||
cardNumber.append(" ")
|
||||
} else if (i == 7){
|
||||
cardNumber.append(" ")
|
||||
} else if (i == 11){
|
||||
cardNumber.append(" ")
|
||||
}
|
||||
}
|
||||
return cardNumber
|
||||
}
|
||||
|
||||
func formatMaskedCardNumber() -> String {
|
||||
let chars = Array(self)
|
||||
var cardNumber = ""
|
||||
for i in 0..<chars.count {
|
||||
if (i == 3) {
|
||||
cardNumber.append(chars[i])
|
||||
cardNumber.append(" ")
|
||||
} else if (i == 7){
|
||||
cardNumber.append(chars[i])
|
||||
cardNumber.append(" ")
|
||||
} else if (i == 8){
|
||||
cardNumber.append("*")
|
||||
} else if (i == 9){
|
||||
cardNumber.append("*")
|
||||
} else if (i == 10){
|
||||
cardNumber.append("*")
|
||||
} else if (i == 11){
|
||||
cardNumber.append("*")
|
||||
cardNumber.append(" ")
|
||||
} else {
|
||||
cardNumber.append(chars[i])
|
||||
}
|
||||
}
|
||||
return cardNumber
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Int {
|
||||
func decimal2bin() -> String {
|
||||
return String(self, radix: 2)
|
||||
}
|
||||
|
||||
func decimal2hex() -> String {
|
||||
return String(self, radix: 16)
|
||||
}
|
||||
var bin: String {
|
||||
String(self, radix: 2).leftPad(with: "0", length: 8)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt8 {
|
||||
var bin: String {
|
||||
String(self, radix: 2).leftPad(with: "0", length: 8)
|
||||
}
|
||||
}
|
||||
|
||||
extension [UInt8] {
|
||||
func hexString() -> String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
// static func hexString(_ data: Data) -> String {
|
||||
// return data.map { String(format: "%02x", $0) }.joined()
|
||||
// }
|
||||
|
||||
// static func hex2byte(_ str: String) -> Data {
|
||||
// var data = Data()
|
||||
// var tempStr = str
|
||||
// while tempStr.count > 0 {
|
||||
// let c = String(tempStr.prefix(2))
|
||||
// tempStr = String(tempStr.dropFirst(2))
|
||||
// if let num = UInt8(c, radix: 16) {
|
||||
// data.append(num)
|
||||
// }
|
||||
// }
|
||||
// return data
|
||||
// }
|
||||
|
||||
func hex2byte() -> Data {
|
||||
var data = Data()
|
||||
var tempStr = self
|
||||
while tempStr.count > 0 {
|
||||
let c = String(tempStr.prefix(2))
|
||||
tempStr = String(tempStr.dropFirst(2))
|
||||
if let num = UInt8(c, radix: 16) {
|
||||
data.append(num)
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func subString(from: Int, to: Int) -> String {
|
||||
let startIndex = self.index(self.startIndex, offsetBy: from)
|
||||
let endIndex = self.index(self.startIndex, offsetBy: to)
|
||||
return String(self[startIndex..<endIndex])
|
||||
}
|
||||
|
||||
func leftPad(with character: Character, length: UInt) -> String {
|
||||
let maxLength = Int(length) - count
|
||||
guard maxLength > 0 else {
|
||||
return self
|
||||
}
|
||||
return String(repeating: String(character), count: maxLength) + self
|
||||
}
|
||||
|
||||
func stringToBytes() -> [UInt8]? {
|
||||
let length = self.count
|
||||
if length & 1 != 0 {
|
||||
return nil
|
||||
}
|
||||
var bytes = [UInt8]()
|
||||
bytes.reserveCapacity(length/2)
|
||||
var index = self.startIndex
|
||||
for _ in 0..<length/2 {
|
||||
let nextIndex = self.index(index, offsetBy: 2)
|
||||
if let b = UInt8(self[index..<nextIndex], radix: 16) {
|
||||
bytes.append(b)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
index = nextIndex
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
func localizeString(string: String) -> String {
|
||||
|
||||
let path = Bundle.main.path(forResource: string, ofType: "lproj")
|
||||
let bundle = Bundle(path: path!)
|
||||
return NSLocalizedString(self, tableName: nil, bundle: bundle!,
|
||||
value: "", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension String: LocalizedError {
|
||||
static let numbers: Set<Character> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
static let hexSymbols: Set<Character> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F" ]
|
||||
|
||||
public var errorDescription: String? { return self }
|
||||
|
||||
var isNumeric: Bool {
|
||||
guard self.count > 0 else { return false }
|
||||
return Set(self).isSubset(of: String.numbers)
|
||||
}
|
||||
|
||||
var isHex: Bool {
|
||||
guard self.count > 0 && self.count % 2 == 0 else { return false }
|
||||
return Set(self).isSubset(of: String.hexSymbols)
|
||||
}
|
||||
|
||||
subscript(_ range: CountableRange<Int>) -> String {
|
||||
let start = index(startIndex, offsetBy: max(0, range.lowerBound))
|
||||
let end = index(start, offsetBy: min(self.count - range.lowerBound,
|
||||
range.upperBound - range.lowerBound))
|
||||
return String(self[start..<end])
|
||||
}
|
||||
|
||||
subscript(_ range: CountablePartialRangeFrom<Int>) -> String {
|
||||
let start = index(startIndex, offsetBy: max(0, range.lowerBound))
|
||||
return String(self[start...])
|
||||
}
|
||||
|
||||
func deletingPrefix(_ prefix: String) -> String {
|
||||
guard self.hasPrefix(prefix) else { return self }
|
||||
return String(self.dropFirst(prefix.count))
|
||||
}
|
||||
func indexInt(of char: Character) -> Int? {
|
||||
return firstIndex(of: char)?.utf16Offset(in: self)
|
||||
}
|
||||
|
||||
}
|
||||
167
Emoney Info/Classes/smartCard/CardErrorCodes.swift
Executable file
@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2018-2020 TON DEV SOLUTIONS LTD.
|
||||
*
|
||||
* Licensed under the SOFTWARE EVALUATION License (the "License"); you may not use
|
||||
* this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific TON DEV software governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
class CardErrorCodes {
|
||||
|
||||
/* Standard status words that may be returned by any Java card */
|
||||
|
||||
static let SW_SUCCESS :UInt16 = 0x9000;
|
||||
static let SW_WRONG_LENGTH :UInt16 = 0x6700;
|
||||
static let SW_APPLET_SELECT_FAILED :UInt16 = 0x6999;
|
||||
static let SW_RESPONSE_BYTES_REMAINING :UInt16 = 0x6100;
|
||||
static let SW_CLA_NOT_SUPPORTED :UInt16 = 0x6E00;
|
||||
static let SW_COMMAND_CHAINING_NOT_SUPPORTED :UInt16 = 0x6884;
|
||||
static let SW_COMMAND_NOT_ALLOWED :UInt16 = 0x6986;
|
||||
static let SW_CONDITIONS_OF_USE_NOT_SATISFIED :UInt16 = 0x6985;
|
||||
static let SW_CORRECT_EXPECTED_LENGTH :UInt16 = 0x6C00;
|
||||
static let SW_DATA_INVALID :UInt16 = 0x6984;
|
||||
static let SW_NOT_ENOUGH_MEMORY_SPACE_IN_FILE :UInt16 = 0x6A84;
|
||||
static let SW_FILE_INVALID :UInt16 = 0x6983;
|
||||
static let SW_FILE_NOT_FOUND :UInt16 = 0x6A82;
|
||||
static let SW_FUNCTION_NOT_SUPPORTED :UInt16 = 0x6A81;
|
||||
static let SW_INCORRECT_P1_P2 :UInt16 = 0x6A86;
|
||||
static let SW_INS_NOT_SUPPORTED :UInt16 = 0x6D00;
|
||||
/*static let SW_LAST_COMMAND_IN_CHAIN_EXPECTED :UInt16 = 0x6883;*/
|
||||
static let SW_LOGICAL_CHANNEL_NOT_SUPPORTED :UInt16 = 0x6881;
|
||||
static let SW_RECORD_NOT_FOUND :UInt16 = 0x6883;
|
||||
static let SW_SECURE_MESSAGING_NOT_SUPPORTED :UInt16 = 0x6882;
|
||||
static let SW_SECURITY_CONDITION_NOT_SATISFIED :UInt16 = 0x6982;
|
||||
static let SW_COMMAND_ABORTED :UInt16 = 0x6F00;
|
||||
static let SW_WRONG_DATA :UInt16 = 0x6A80;
|
||||
static let SW_WRONG_P1_P2 :UInt16 = 0x6B00;
|
||||
|
||||
|
||||
/* Status words that may be returned by TonWalletApplet */
|
||||
|
||||
// Common errors
|
||||
static let SW_INTERNAL_BUFFER_IS_NULL_OR_TOO_SMALL :UInt16 = 0x4F00;
|
||||
static let SW_PERSONALIZATION_NOT_FINISHED :UInt16 = 0x4F01;
|
||||
static let SW_INCORRECT_OFFSET :UInt16 = 0x4F02;
|
||||
static let SW_INCORRECT_PAYLOAD :UInt16 = 0x4F03;
|
||||
|
||||
// Password authentication errors
|
||||
static let SW_INCORRECT_PASSWORD_FOR_CARD_AUTHENICATION :UInt16 = 0x5F00;
|
||||
static let SW_INCORRECT_PASSWORD_CARD_IS_BLOCKED :UInt16 = 0x5F01;
|
||||
|
||||
// Signature errors
|
||||
static let SW_SET_COIN_TYPE_FAILED :UInt16 = 0x6F01;
|
||||
static let SW_SET_CURVE_FAILED :UInt16 = 0x6F02;
|
||||
static let SW_GET_COIN_PUB_DATA_FAILED :UInt16 = 0x6F03;
|
||||
static let SW_SIGN_DATA_FAILED :UInt16 = 0x6F04;
|
||||
|
||||
// Pin verification errors
|
||||
static let SW_COIN_MANAGER_INCORRECT_PIN :UInt16 = 0x9B01;
|
||||
static let SW_COIN_MANAGER_UPDATE_PIN_ERROR :UInt16 = 0x9B02;
|
||||
// static let SW_PIN_TRIES_EXPIRED :UInt16 = 0x9F08;
|
||||
static let SW_INCORRECT_PIN :UInt16 = 0x6F07;
|
||||
static let SW_PIN_TRIES_EXPIRED :UInt16 = 0x6F08;
|
||||
|
||||
static let SW_LOAD_SEED_ERROR :UInt16 = 0x9F03;
|
||||
|
||||
// Key chain errors
|
||||
static let SW_INCORRECT_KEY_INDEX :UInt16 = 0x7F00;
|
||||
static let SW_INCORRECT_KEY_CHUNK_START_OR_LEN :UInt16 = 0x7F01;
|
||||
static let SW_INCORRECT_KEY_CHUNK_LEN :UInt16 = 0x7F02;
|
||||
static let SW_NOT_ENOUGH_SPACE :UInt16 = 0x7F03;
|
||||
static let SW_KEY_SIZE_UNKNOWN :UInt16 = 0x7F04;
|
||||
static let SW_KEY_LEN_INCORRECT :UInt16 = 0x7F05;
|
||||
static let SW_HMAC_EXISTS :UInt16 = 0x7F06;
|
||||
static let SW_INCORRECT_KEY_INDEX_TO_CHANGE :UInt16 = 0x7F07;
|
||||
static let SW_MAX_KEYS_NUMBER_EXCEEDED :UInt16 = 0x7F08;
|
||||
static let SW_DELETE_KEY_CHUNK_IS_NOT_FINISHED :UInt16 = 0x7F09;
|
||||
|
||||
// Hmac errors
|
||||
static let SW_INCORRECT_SAULT :UInt16 = 0x8F01;
|
||||
static let SW_DATA_INTEGRITY_CORRUPTED :UInt16 = 0x8F02;
|
||||
static let SW_INCORRECT_APDU_HMAC :UInt16 = 0x8F03;
|
||||
static let SW_HMAC_VERIFICATION_TRIES_EXPIRED :UInt16 = 0x8F04;
|
||||
|
||||
// Recovery errors
|
||||
static let SW_RECOVERY_DATA_TOO_LONG :UInt16 = 0x6F09;
|
||||
static let SW_INCORRECT_START_POS_OR_LE :UInt16 = 0x6F0A;
|
||||
static let SW_INTEGRITY_OF_RECOVERY_DATA_CORRUPTED :UInt16 = 0x6F0B;
|
||||
static let SW_RECOVERY_DATA_ALREADY_EXISTS :UInt16 = 0x6F0C;
|
||||
static let SW_RECOVERY_DATA_IS_NOT_SET:UInt16 = 0x6F0D;
|
||||
|
||||
static func convertSw1Sw2IntoOneSw(sw1 : UInt8, sw2 : UInt8) -> Int {
|
||||
Int(256) * Int(sw1) + Int(sw2)
|
||||
}
|
||||
|
||||
static let CARD_ERROR_MSGS = [SW_SUCCESS: "No error.",
|
||||
SW_APPLET_SELECT_FAILED : "Applet select failed.",
|
||||
SW_RESPONSE_BYTES_REMAINING : "Response bytes remaining.",
|
||||
SW_CLA_NOT_SUPPORTED : "CLA value not supported.",
|
||||
SW_COMMAND_CHAINING_NOT_SUPPORTED : "Command chaining not supported.",
|
||||
SW_COMMAND_NOT_ALLOWED : "Command not allowed (no current EF).",
|
||||
SW_CONDITIONS_OF_USE_NOT_SATISFIED : "Conditions of use not satisfied.",
|
||||
SW_CORRECT_EXPECTED_LENGTH : "Correct Expected Length (Le).",
|
||||
SW_DATA_INVALID : "Data invalid.",
|
||||
SW_NOT_ENOUGH_MEMORY_SPACE_IN_FILE : "Not enough memory space in the file.",
|
||||
SW_FILE_INVALID : "File invalid.",
|
||||
SW_FILE_NOT_FOUND : "File not found.",
|
||||
SW_FUNCTION_NOT_SUPPORTED : "Function not supported.",
|
||||
SW_INCORRECT_P1_P2 : "Incorrect parameters (P1,P2).",
|
||||
SW_INS_NOT_SUPPORTED : "INS value not supported.",
|
||||
/* SW_LAST_COMMAND_IN_CHAIN_EXPECTED : "Last command in chain expected.",*/
|
||||
SW_LOGICAL_CHANNEL_NOT_SUPPORTED : "Card does not support the operation on the specified logical channel.",
|
||||
SW_RECORD_NOT_FOUND : "Record not found.",
|
||||
SW_SECURE_MESSAGING_NOT_SUPPORTED : "Card does not support secure messaging.",
|
||||
SW_SECURITY_CONDITION_NOT_SATISFIED : "Security condition not satisfied.",
|
||||
SW_COMMAND_ABORTED : "Command aborted, No precise diagnosis.",
|
||||
SW_WRONG_DATA : "Wrong data.",
|
||||
SW_WRONG_LENGTH : "Wrong length.",
|
||||
SW_WRONG_P1_P2 : "Wrong parameter(s) P1-P2",
|
||||
|
||||
SW_INTERNAL_BUFFER_IS_NULL_OR_TOO_SMALL : "Internal buffer is null or too small.",
|
||||
SW_PERSONALIZATION_NOT_FINISHED : "Personalization is not finished.",
|
||||
SW_INCORRECT_OFFSET : "Internal error: incorrect offset.",
|
||||
SW_INCORRECT_PAYLOAD : "Internal error: incorrect payload value.",
|
||||
SW_INCORRECT_PASSWORD_FOR_CARD_AUTHENICATION : "Incorrect password for card authentication.",
|
||||
SW_INCORRECT_PASSWORD_CARD_IS_BLOCKED : "Incorrect password, card is locked.",
|
||||
SW_SET_COIN_TYPE_FAILED : "Set coin type failed.",
|
||||
SW_SET_CURVE_FAILED : "Set curve failed.",
|
||||
SW_GET_COIN_PUB_DATA_FAILED : "Get coin pub data failed.",
|
||||
SW_SIGN_DATA_FAILED : "Sign data failed.",
|
||||
SW_INCORRECT_PIN : "Incorrect PIN.",
|
||||
SW_COIN_MANAGER_INCORRECT_PIN : "Incorrect PIN.",
|
||||
SW_COIN_MANAGER_UPDATE_PIN_ERROR : "Update PIN error (for CHANGE_PIN) or wallet status not support to export (for GENERATE SEED).",
|
||||
SW_PIN_TRIES_EXPIRED : "PIN tries expired.",
|
||||
SW_LOAD_SEED_ERROR : "Load seed error.",
|
||||
SW_INCORRECT_KEY_INDEX : "Incorrect key index.",
|
||||
SW_INCORRECT_KEY_CHUNK_START_OR_LEN : "Incorrect key chunk start or length.",
|
||||
SW_INCORRECT_KEY_CHUNK_LEN : "Incorrect key chunk length.",
|
||||
SW_NOT_ENOUGH_SPACE : "Not enough space.",
|
||||
SW_KEY_SIZE_UNKNOWN : "Key size unknown.",
|
||||
SW_KEY_LEN_INCORRECT : "Key length incorrect.",
|
||||
SW_HMAC_EXISTS : "Hmac exists already.",
|
||||
SW_INCORRECT_KEY_INDEX_TO_CHANGE: "Incorrect key index to change.",
|
||||
SW_MAX_KEYS_NUMBER_EXCEEDED : "Max number of keys (1023) is exceeded.",
|
||||
SW_DELETE_KEY_CHUNK_IS_NOT_FINISHED : "Delete key chunk is not finished.",
|
||||
SW_INCORRECT_SAULT : "Incorrect sault.",
|
||||
SW_DATA_INTEGRITY_CORRUPTED : "Data integrity corrupted.",
|
||||
SW_INCORRECT_APDU_HMAC : "Incorrect apdu hmac. ",
|
||||
SW_HMAC_VERIFICATION_TRIES_EXPIRED : "Apdu Hmac verification tries expired.",
|
||||
SW_RECOVERY_DATA_TOO_LONG : "Too big length of recovery data.",
|
||||
SW_INCORRECT_START_POS_OR_LE : "Incorrect start or length of recovery data piece in internal buffer.",
|
||||
SW_INTEGRITY_OF_RECOVERY_DATA_CORRUPTED : "Hash of recovery data is incorrect. ",
|
||||
SW_RECOVERY_DATA_ALREADY_EXISTS : "Recovery data already exists.",
|
||||
SW_RECOVERY_DATA_IS_NOT_SET : "Recovery data does not exist"
|
||||
]
|
||||
|
||||
static func getErrorMsg(sw : UInt16) -> String? {
|
||||
return CARD_ERROR_MSGS[sw]
|
||||
}
|
||||
|
||||
}
|
||||
39
Emoney Info/Classes/smartCard/CommonConstants.swift
Executable file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2018-2020 TON DEV SOLUTIONS LTD.
|
||||
*
|
||||
* Licensed under the SOFTWARE EVALUATION License (the "License"); you may not use
|
||||
* this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific TON DEV software governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
func debugLog(
|
||||
_ items: Any...,
|
||||
separator: String = " ",
|
||||
terminator: String = "\n"
|
||||
) {
|
||||
#if DEBUG
|
||||
let output = items.map { String(describing: $0) }.joined(separator: separator)
|
||||
Swift.print(output, terminator: terminator)
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
Here there are some contants related to all APDU commands
|
||||
*/
|
||||
public class CommonConstants {
|
||||
static let CLA_SELECT: UInt8 = 0x00
|
||||
static let INS_SELECT : UInt8 = 0xA4
|
||||
static let SELECT_P1 : UInt8 = 0x04
|
||||
static let SELECT_P2 : UInt8 = 0x00
|
||||
static let LE_NO_RESPONSE_DATA = -1 // Use this LE if you do not wait any response data from card (except of status word)
|
||||
static let LE_GET_ALL_RESPONSE_DATA = 256 // This is standard 0x00 value of LE. Use this LE if you want to take all response bytes from applet that it produced
|
||||
|
||||
|
||||
}
|
||||
72
Emoney Info/Classes/smartCard/EmoneyApduCommands.swift
Executable file
@ -0,0 +1,72 @@
|
||||
//
|
||||
// EmoneyApduCommands.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 23/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreNFC
|
||||
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
class EmoneyApduCommands{
|
||||
static let TAPCASH_INIT_DATA : [UInt8] = [0xA0, 0x00, 0x42, 0x4e, 0x49, 0x10, 0x00, 0x01]
|
||||
static let BRIZZI_INIT_DATA : [UInt8] = [0x01, 0x00, 0x00]
|
||||
static let EMONEY_INIT_DATA : [UInt8] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]
|
||||
static let FLAZZ_INIT_DATA : [UInt8] = [0xA0, 0x00, 0x00, 0x00, 0x18, 0x0F, 0x00, 0x00, 0x01, 0x80, 0x01]
|
||||
static let JACKCARD_INIT_DATA : [UInt8] = [0xA0, 0x00, 0x00, 0x05, 0x71, 0x4e, 0x4a, 0x43]
|
||||
static let MEGA_DATA : [UInt8] = [0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00]
|
||||
static let BRI_DATA_01 : [UInt8] = [0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00]
|
||||
|
||||
static let TAPCASH_INIT_APDU = NFCISO7816APDU(instructionClass : CommonConstants.CLA_SELECT, instructionCode : CommonConstants.INS_SELECT, p1Parameter : CommonConstants.SELECT_P1, p2Parameter : CommonConstants.SELECT_P2, data : Data(_ : TAPCASH_INIT_DATA), expectedResponseLength : CommonConstants.LE_NO_RESPONSE_DATA)
|
||||
|
||||
static let BRIZZI_INIT_APDU = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x5A, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_ : BRIZZI_INIT_DATA), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
|
||||
static let EMONEY_INIT_APDU = NFCISO7816APDU(instructionClass : CommonConstants.CLA_SELECT, instructionCode : CommonConstants.INS_SELECT, p1Parameter : CommonConstants.SELECT_P1, p2Parameter : CommonConstants.SELECT_P2, data : Data(_ : EMONEY_INIT_DATA), expectedResponseLength : CommonConstants.LE_NO_RESPONSE_DATA)
|
||||
|
||||
static let FLAZZ_INIT_APDU = NFCISO7816APDU(instructionClass : CommonConstants.CLA_SELECT, instructionCode : CommonConstants.INS_SELECT, p1Parameter : CommonConstants.SELECT_P1, p2Parameter : CommonConstants.SELECT_P2, data : Data(_ : FLAZZ_INIT_DATA), expectedResponseLength : CommonConstants.LE_NO_RESPONSE_DATA)
|
||||
|
||||
static let JACKCARD_INIT_APDU = NFCISO7816APDU(instructionClass : CommonConstants.CLA_SELECT, instructionCode : CommonConstants.INS_SELECT, p1Parameter : CommonConstants.SELECT_P1, p2Parameter : CommonConstants.SELECT_P2, data : Data(_ : JACKCARD_INIT_DATA), expectedResponseLength : CommonConstants.LE_NO_RESPONSE_DATA)
|
||||
|
||||
static let TAPCASH_APDU01 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x32, p1Parameter : 0x03, p2Parameter : 0x00, data : Data(), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//00 a4 01 00 02 02 00
|
||||
static let BCA_APDU01 = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xA4, p1Parameter : 0x01, p2Parameter : 0x00, data : Data(_ : [0x02, 0x00]), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//00 b0 81 00 8e
|
||||
static let BCA_APDU02 = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xb0, p1Parameter : 0x81, p2Parameter : 0x00, data : Data(), expectedResponseLength : 142)
|
||||
//80 32 00 03 04 00 00 00 00 //00 b0 81 00 00
|
||||
static let BCA_APDU03 = NFCISO7816APDU(instructionClass : 0x80, instructionCode : 0x32, p1Parameter : 0x00, p2Parameter : 0x03, data : Data(_ : [0x00, 0x00, 0x00, 0x00]), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//00 b0 81 00 00
|
||||
static let BCA_APDU04 = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x81, p2Parameter : 0x00, data : Data(), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
|
||||
|
||||
|
||||
//00 b3 00 00 3f
|
||||
static let MANDIRI_APDU01 = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB3, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(), expectedResponseLength : 63)
|
||||
//00 b5 00 00 0a
|
||||
static let MANDIRI_APDU02 = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB5, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(), expectedResponseLength : 10)
|
||||
//90 4c 00 00 04
|
||||
static let DKI_APDU01 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x4C, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(), expectedResponseLength : 4)
|
||||
//90 bd 00 00 07 01 00 00 00 0a 00 00 00
|
||||
static let MEGA_APDU01 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0xBD, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_ : MEGA_DATA), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//90 6c 00 00 01 02 00
|
||||
static let MEGA_APDU02 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x6C, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_ : [0x02]), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//90 60 00 00 00
|
||||
static let BRI_UID01 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x60, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//90 AF 00 00 00
|
||||
static let BRI_UID02 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0xAF, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
|
||||
|
||||
//90 bd 00 00 07 00 00 00 00 17 00 00 00
|
||||
static let BRI_APDU01 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0xBD, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_: BRI_DATA_01), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//90 5A 00 00 03 03 00 00 00
|
||||
static let BRI_APDU02 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x5A, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_: [0x03, 0x00, 0x00]), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//90 0a 00 00 01 00 00
|
||||
static let BRI_APDU03 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x0A, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_: [0x00]), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//90 6C 00 00 01 00 00
|
||||
static let BRI_APDU05 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x6C, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_: [0x00]), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//90 BB 00 00 07 01 00 00 00 00 00 00 00
|
||||
static let BRI_LOG01 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0xBB, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_: [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
//90 AF 00 00 00
|
||||
static let BRI_LOG02 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0xAF, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
|
||||
}
|
||||
174
Emoney Info/Classes/smartCard/TapCashData.swift
Executable file
@ -0,0 +1,174 @@
|
||||
//
|
||||
// TapCashData.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 26/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TapCashData {
|
||||
var purseData: [UInt8]?
|
||||
var purseStatus: [UInt8]?
|
||||
var purseBalance: [UInt8]?
|
||||
var CAN: [UInt8]?
|
||||
var CSN: [UInt8]?
|
||||
var purseExpiry: [UInt8]?
|
||||
var lastCredirTRP: [UInt8]?
|
||||
var lastCreditHeader: [UInt8]?
|
||||
var lastTxnTRP: [UInt8]?
|
||||
var lastTxnRecord: [UInt8]?
|
||||
var BDC: [UInt8]?
|
||||
var keySet: [UInt8]?
|
||||
var maxCardBalance: [UInt8]?
|
||||
var eData: [UInt8]?
|
||||
var crcb: [UInt8]?
|
||||
var lastTransactionSignCert: [UInt8]?
|
||||
var lastCounterData: [UInt8]?
|
||||
var version: [UInt8]?
|
||||
var lastDebitOption: [UInt8]?
|
||||
var totalRecords: [UInt8]?
|
||||
|
||||
func getPurseData() -> [UInt8]? {
|
||||
return purseData
|
||||
}
|
||||
|
||||
func setPurseData(_ purseData: [UInt8]) {
|
||||
self.purseData = purseData
|
||||
unpackPurseData(data: purseData)
|
||||
}
|
||||
|
||||
func getPurseStatus() -> [UInt8]? {
|
||||
return purseStatus
|
||||
}
|
||||
|
||||
func setPurseStatus(_ purseStatus: [UInt8]) {
|
||||
self.purseStatus = purseStatus
|
||||
}
|
||||
|
||||
func getPurseBalance() -> [UInt8]? {
|
||||
return purseBalance
|
||||
}
|
||||
|
||||
func setPurseBalance(_ purseBalance: [UInt8]) {
|
||||
self.purseBalance = purseBalance
|
||||
}
|
||||
|
||||
func getCAN() -> [UInt8]? {
|
||||
return CAN
|
||||
}
|
||||
|
||||
func setCAN(_ CAN: [UInt8]) {
|
||||
self.CAN = CAN
|
||||
}
|
||||
|
||||
func getCSN() -> [UInt8]? {
|
||||
return CSN
|
||||
}
|
||||
|
||||
func setCSN(_ CSN: [UInt8]) {
|
||||
self.CSN = CSN
|
||||
}
|
||||
|
||||
func getPurseExpiry() -> [UInt8]? {
|
||||
return purseExpiry
|
||||
}
|
||||
|
||||
func setPurseExpiry(_ purseExpiry: [UInt8]) {
|
||||
self.purseExpiry = purseExpiry
|
||||
}
|
||||
|
||||
func getLastCredirTRP() -> [UInt8]? {
|
||||
return lastCredirTRP
|
||||
}
|
||||
|
||||
func setLastCredirTRP(_ lastCredirTRP: [UInt8]) {
|
||||
self.lastCredirTRP = lastCredirTRP
|
||||
}
|
||||
|
||||
func getLastCreditHeader() -> [UInt8]? {
|
||||
return lastCreditHeader
|
||||
}
|
||||
|
||||
func setLastCreditHeader(_ lastCreditHeader: [UInt8]) {
|
||||
self.lastCreditHeader = lastCreditHeader
|
||||
}
|
||||
|
||||
func getLastTxnTRP() -> [UInt8]? {
|
||||
return lastTxnTRP
|
||||
}
|
||||
|
||||
func setLastTxnTRP(_ lastTxnTRP: [UInt8]) {
|
||||
self.lastTxnTRP = lastTxnTRP
|
||||
}
|
||||
|
||||
func getLastTxnRecord() -> [UInt8]? {
|
||||
return lastTxnRecord
|
||||
}
|
||||
|
||||
func setLastTxnRecord(_ lastTxnRecord: [UInt8]) {
|
||||
self.lastTxnRecord = lastTxnRecord
|
||||
}
|
||||
|
||||
func getBDC() -> [UInt8]? {
|
||||
return BDC
|
||||
}
|
||||
|
||||
func setBDC(_ BDC: [UInt8]) {
|
||||
self.BDC = BDC
|
||||
}
|
||||
|
||||
func getKeySet() -> [UInt8]? {
|
||||
return keySet
|
||||
}
|
||||
|
||||
func setKeySet(_ keySet: [UInt8]) {
|
||||
self.keySet = keySet
|
||||
}
|
||||
|
||||
func getMaxCardBalance() -> [UInt8]? {
|
||||
return maxCardBalance
|
||||
}
|
||||
|
||||
func setMaxCardBalance(_ maxCardBalance: [UInt8]) {
|
||||
self.maxCardBalance = maxCardBalance
|
||||
}
|
||||
|
||||
func geteData() -> [UInt8]? {
|
||||
return eData
|
||||
}
|
||||
|
||||
func seteData(_ eData: [UInt8]) {
|
||||
self.eData = eData
|
||||
}
|
||||
|
||||
func getCrcb() -> [UInt8]? {
|
||||
return crcb
|
||||
}
|
||||
|
||||
func setCrcb(_ crcb: [UInt8]) {
|
||||
self.crcb = crcb
|
||||
}
|
||||
|
||||
func getTotalRecords() -> [UInt8]? {
|
||||
return totalRecords
|
||||
}
|
||||
|
||||
private func unpackPurseData(data: [UInt8]) {
|
||||
version = Array(data[0..<1])
|
||||
purseStatus = Array(data[1..<2])
|
||||
purseBalance = Array(data[2..<5])
|
||||
CAN = Array(data[8..<16])
|
||||
CSN = Array(data[16..<24])
|
||||
purseExpiry = Array(data[24..<26])
|
||||
lastCredirTRP = Array(data[28..<32])
|
||||
lastCreditHeader = Array(data[32..<40])
|
||||
totalRecords = Array(data[40..<41])
|
||||
lastTxnTRP = Array(data[42..<46])
|
||||
lastTxnRecord = Array(data[46..<62])
|
||||
BDC = Array(data[63..<64])
|
||||
keySet = Array(data[71..<72])
|
||||
maxCardBalance = Array(data[78..<81])
|
||||
lastDebitOption = Array(data[94..<95])
|
||||
}
|
||||
}
|
||||
19
Emoney Info/Classes/utils/GradientView.swift
Executable file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// GradientView.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Irawan on 29/07/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class GradientView: UIView {
|
||||
override class var layerClass: AnyClass {
|
||||
return CAGradientLayer.self
|
||||
}
|
||||
|
||||
var gradientLayer: CAGradientLayer {
|
||||
return self.layer as! CAGradientLayer
|
||||
}
|
||||
}
|
||||
177
Emoney Info/Classes/utils/L10n.swift
Normal file
@ -0,0 +1,177 @@
|
||||
import Foundation
|
||||
|
||||
/// Type-safe localization helper.
|
||||
/// Usage: L10n.checkBalance → returns localized String automatically
|
||||
/// Works in both UIKit (as String) and SwiftUI (via Text(L10n.xxx))
|
||||
enum L10n {
|
||||
|
||||
// MARK: - Home
|
||||
|
||||
static var availableBalance: String { s("availableBalance") }
|
||||
static var cardTapInstruction: String { s("cardTapInstruction") }
|
||||
static var cardTypeDefault: String { s("cardTypeDefault") }
|
||||
static var tapCardHere: String { s("tapCardHere") }
|
||||
static var tapCardHint: String { s("tapCardHint") }
|
||||
static var checkBalance: String { s("checkBalance") }
|
||||
static var lastTransaction: String { s("lastTransaction") }
|
||||
static var viewFullHistory: String { s("viewFullHistory") }
|
||||
static var copiedToClipboard: String { s("copiedToClipboard") }
|
||||
static var transactionDefault: String { s("transactionDefault") }
|
||||
static var transactionStatusSuccess: String { s("transactionStatusSuccess") }
|
||||
static var noCard: String { s("noCard") }
|
||||
|
||||
// MARK: - Card / NFC
|
||||
|
||||
static var cardType: String { s("cardType") }
|
||||
static var cardNumber: String { s("cardNumber") }
|
||||
static var balance: String { s("balance") }
|
||||
static var scanMessage: String { s("scanMessage") }
|
||||
static var readFinish: String { s("readFinish") }
|
||||
static var readFailed: String { s("readFailed") }
|
||||
static var updateBalance: String { s("updateBalance") }
|
||||
static var payment: String { s("payment") }
|
||||
static var topup: String { s("topup") }
|
||||
static var unknown: String { s("unknown") }
|
||||
static var void: String { s("void") }
|
||||
static var reactivation: String { s("reactivation") }
|
||||
static var statementFee: String { s("statementFee") }
|
||||
static var gracePeriod: String { s("gracePeriod") }
|
||||
static var refund: String { s("refund") }
|
||||
static var close: String { s("close") }
|
||||
static var atu: String { s("atu") }
|
||||
|
||||
// MARK: - History
|
||||
|
||||
static var historyTitle: String { s("historyTitle") }
|
||||
static var recentActivity: String { s("recentActivity") }
|
||||
static var filterAllTime: String { s("filterAllTime") }
|
||||
static var filterToday: String { s("filterToday") }
|
||||
static var filterThisMonth: String { s("filterThisMonth") }
|
||||
static var filterThisWeek: String { s("filterThisWeek") }
|
||||
static var noTransactionsFound: String { s("noTransactionsFound") }
|
||||
static var exportPDF: String { s("exportPDF") }
|
||||
static var transactionHistory: String { s("transactionHistory") }
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
static var settingsTitle: String { s("settingsTitle") }
|
||||
static var premiumBadge: String { s("premiumBadge") }
|
||||
static var premiumTitle: String { s("premiumTitle") }
|
||||
static var premiumDesc: String { s("premiumDesc") }
|
||||
static var upgradeNow: String { s("upgradeNow") }
|
||||
static var sectionGeneral: String { s("sectionGeneral") }
|
||||
static var languageTitle: String { s("languageTitle") }
|
||||
static var languageValue: String { s("languageValue") }
|
||||
static var showCardNumberTitle: String { s("showCardNumberTitle") }
|
||||
static var showCardNumberDesc: String { s("showCardNumberDesc") }
|
||||
static var sectionApp: String { s("sectionApp") }
|
||||
static var notificationsTitle: String { s("notificationsTitle") }
|
||||
static var notificationsDesc: String { s("notificationsDesc") }
|
||||
static var helpCenterTitle: String { s("helpCenterTitle") }
|
||||
static var helpCenterDesc: String { s("helpCenterDesc") }
|
||||
static var aboutAppTitle: String { s("aboutAppTitle") }
|
||||
static var aboutAppDesc: String { s("aboutAppDesc") }
|
||||
// MARK: - Terms & Conditions
|
||||
static var termsLastUpdated: String { s("termsLastUpdated") }
|
||||
static var termsTitleRegular: String { s("termsTitleRegular") }
|
||||
static var termsTitleBold: String { s("termsTitleBold") }
|
||||
static var termsSubtitle: String { s("termsSubtitle") }
|
||||
static var termsSec1Title: String { s("termsSec1Title") }
|
||||
static var termsSec1Body: String { s("termsSec1Body") }
|
||||
static var termsSec2Title: String { s("termsSec2Title") }
|
||||
static var termsSec2Body: String { s("termsSec2Body") }
|
||||
static var termsSec2Bullet1: String { s("termsSec2Bullet1") }
|
||||
static var termsSec2Bullet2: String { s("termsSec2Bullet2") }
|
||||
static var termsSec2Bullet3: String { s("termsSec2Bullet3") }
|
||||
static var termsSec3Title: String { s("termsSec3Title") }
|
||||
static var termsSec3Body: String { s("termsSec3Body") }
|
||||
static var termsSec3Bullet1: String { s("termsSec3Bullet1") }
|
||||
static var termsSec3Bullet2: String { s("termsSec3Bullet2") }
|
||||
static var termsContactTitle: String { s("termsContactTitle") }
|
||||
static var termsContactDesc: String { s("termsContactDesc") }
|
||||
static var termsContactButton: String { s("termsContactButton") }
|
||||
|
||||
// MARK: - Privacy Policy
|
||||
static var privacyLastUpdated: String { s("privacyLastUpdated") }
|
||||
static var privacySectionNfcTitle: String { s("privacySectionNfcTitle") }
|
||||
static var privacySectionNfcBody: String { s("privacySectionNfcBody") }
|
||||
static var privacySectionNoStorageTitle: String { s("privacySectionNoStorageTitle") }
|
||||
static var privacySectionNoStorageBody: String { s("privacySectionNoStorageBody") }
|
||||
static var privacySectionReadOnlyTitle: String { s("privacySectionReadOnlyTitle") }
|
||||
static var privacySectionReadOnlyBody: String { s("privacySectionReadOnlyBody") }
|
||||
static var privacyContactTitle: String { s("privacyContactTitle") }
|
||||
static var privacyContactDesc: String { s("privacyContactDesc") }
|
||||
static var privacyContactButton: String { s("privacyContactButton") }
|
||||
|
||||
// MARK: - About
|
||||
static var aboutAppDescription: String { s("aboutAppDescription") }
|
||||
static var aboutChipNfc: String { s("aboutChipNfc") }
|
||||
static var aboutChipRealtime: String { s("aboutChipRealtime") }
|
||||
static var aboutChipMulti: String { s("aboutChipMulti") }
|
||||
static var aboutTerms: String { s("aboutTerms") }
|
||||
static var aboutPrivacy: String { s("aboutPrivacy") }
|
||||
static var aboutConnectTitle: String { s("aboutConnectTitle") }
|
||||
static var aboutConnectDesc: String { s("aboutConnectDesc") }
|
||||
|
||||
static var maskTitle: String { s("maskTitle") }
|
||||
static var maskDesc: String { s("maskDesc") }
|
||||
static var supportCardTitle: String { s("supportCardTitle") }
|
||||
static var supportCardDesc: String { s("supportCardDesc") }
|
||||
static var aboutTitle: String { s("aboutTitle") }
|
||||
static var version: String { s("versi") }
|
||||
static var footerCopyright: String { s("footerCopyright") }
|
||||
static var reportIssue: String { s("reportIssue") }
|
||||
|
||||
// MARK: - FAQ
|
||||
|
||||
static var faqHeaderTitle: String { s("faqHeaderTitle") }
|
||||
static var faqSearchPlaceholder: String { s("faqSearchPlaceholder") }
|
||||
static var faqFilterAll: String { s("faqFilterAll") }
|
||||
static var faqNoResults: String { s("faqNoResults") }
|
||||
static var faqHelpCardTitle: String { s("faqHelpCardTitle") }
|
||||
static var faqHelpCardDesc: String { s("faqHelpCardDesc") }
|
||||
static var faqEmailSupport: String { s("faqEmailSupport") }
|
||||
|
||||
// FAQ Categories
|
||||
static var faqCategoryCard: String { s("faqCategoryCard") }
|
||||
static var faqCategoryTransaction: String { s("faqCategoryTransaction") }
|
||||
static var faqCategoryBalance: String { s("faqCategoryBalance") }
|
||||
static var faqCategoryApp: String { s("faqCategoryApp") }
|
||||
|
||||
// FAQ Questions & Answers — Card
|
||||
static var faqQ_cardCompatible: String { s("faqQ_cardCompatible") }
|
||||
static var faqA_cardCompatible: String { s("faqA_cardCompatible") }
|
||||
static var faqQ_cardNotDetected: String { s("faqQ_cardNotDetected") }
|
||||
static var faqA_cardNotDetected: String { s("faqA_cardNotDetected") }
|
||||
static var faqQ_cardReadFailed: String { s("faqQ_cardReadFailed") }
|
||||
static var faqA_cardReadFailed: String { s("faqA_cardReadFailed") }
|
||||
|
||||
// FAQ Questions & Answers — Transaction
|
||||
static var faqQ_txNotShown: String { s("faqQ_txNotShown") }
|
||||
static var faqA_txNotShown: String { s("faqA_txNotShown") }
|
||||
static var faqQ_txExportPDF: String { s("faqQ_txExportPDF") }
|
||||
static var faqA_txExportPDF: String { s("faqA_txExportPDF") }
|
||||
|
||||
// FAQ Questions & Answers — Balance
|
||||
static var faqQ_balanceWrong: String { s("faqQ_balanceWrong") }
|
||||
static var faqA_balanceWrong: String { s("faqA_balanceWrong") }
|
||||
static var faqQ_balanceTopup: String { s("faqQ_balanceTopup") }
|
||||
static var faqA_balanceTopup: String { s("faqA_balanceTopup") }
|
||||
|
||||
// FAQ Questions & Answers — App
|
||||
static var faqQ_appLanguage: String { s("faqQ_appLanguage") }
|
||||
static var faqA_appLanguage: String { s("faqA_appLanguage") }
|
||||
static var faqQ_appMaskNumber: String { s("faqQ_appMaskNumber") }
|
||||
static var faqA_appMaskNumber: String { s("faqA_appMaskNumber") }
|
||||
|
||||
// MARK: - Tab Bar
|
||||
|
||||
static var tabEmoney: String { s("tabEmoney") }
|
||||
static var tabSettings: String { s("tabSettings") }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private static func s(_ key: String) -> String {
|
||||
NSLocalizedString(key, comment: "")
|
||||
}
|
||||
}
|
||||
15
Emoney Info/Classes/utils/Station.swift
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Station.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Created by Wira Basalamah on 05/04/26.
|
||||
//
|
||||
|
||||
|
||||
struct Station {
|
||||
let id: Int
|
||||
let name: String
|
||||
let subName: String
|
||||
let latitude: String
|
||||
let longitude: String
|
||||
}
|
||||
59
Emoney Info/Classes/utils/Theme.swift
Normal file
@ -0,0 +1,59 @@
|
||||
import UIKit
|
||||
|
||||
enum Theme {
|
||||
|
||||
// MARK: - Colors
|
||||
|
||||
enum Color {
|
||||
static let primary = UIColor(hex: "#7AD4D1")
|
||||
static let secondary = UIColor(hex: "#5D7D7B")
|
||||
static let success = UIColor(hex: "#34C759")
|
||||
static let background = UIColor(hex: "#F3F3F8")
|
||||
static let card = UIColor.white
|
||||
static let textPrimary = UIColor(hex: "#1A1A2E")
|
||||
static let textSecondary = UIColor(hex: "#8E8E93")
|
||||
}
|
||||
|
||||
// MARK: - Font Sizes
|
||||
|
||||
enum FontSize {
|
||||
static let title: CGFloat = 24
|
||||
static let subtitle: CGFloat = 18
|
||||
static let body: CGFloat = 16
|
||||
static let caption: CGFloat = 12
|
||||
}
|
||||
|
||||
// MARK: - Fonts
|
||||
|
||||
enum Font {
|
||||
static func title(weight: UIFont.Weight = .bold) -> UIFont {
|
||||
.systemFont(ofSize: FontSize.title, weight: weight)
|
||||
}
|
||||
static func subtitle(weight: UIFont.Weight = .semibold) -> UIFont {
|
||||
.systemFont(ofSize: FontSize.subtitle, weight: weight)
|
||||
}
|
||||
static func body(weight: UIFont.Weight = .regular) -> UIFont {
|
||||
.systemFont(ofSize: FontSize.body, weight: weight)
|
||||
}
|
||||
static func caption(weight: UIFont.Weight = .regular) -> UIFont {
|
||||
.systemFont(ofSize: FontSize.caption, weight: weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIColor hex initializer
|
||||
|
||||
private extension UIColor {
|
||||
convenience init(hex: String) {
|
||||
var sanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if sanitized.hasPrefix("#") { sanitized.removeFirst() }
|
||||
|
||||
var rgb: UInt64 = 0
|
||||
Scanner(string: sanitized).scanHexInt64(&rgb)
|
||||
|
||||
let r = CGFloat((rgb >> 16) & 0xFF) / 255
|
||||
let g = CGFloat((rgb >> 8) & 0xFF) / 255
|
||||
let b = CGFloat( rgb & 0xFF) / 255
|
||||
self.init(red: r, green: g, blue: b, alpha: 1)
|
||||
}
|
||||
}
|
||||
50
Emoney Info/Classes/utils/ToastHelper.swift
Executable file
@ -0,0 +1,50 @@
|
||||
//
|
||||
// ToastHelper.swift
|
||||
// NewTonNfcCardLib
|
||||
//
|
||||
// Created by Alina Alinovna on 23.10.2020.
|
||||
// Copyright © 2020 Facebook. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class ToastHelper {
|
||||
static func showToast(message : String) {
|
||||
let toastView = UILabel()
|
||||
toastView.backgroundColor = UIColor.black.withAlphaComponent(0.7)
|
||||
toastView.textColor = UIColor.white
|
||||
toastView.textAlignment = .center
|
||||
toastView.font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
toastView.layer.cornerRadius = 25
|
||||
toastView.layer.masksToBounds = true
|
||||
toastView.text = message
|
||||
toastView.numberOfLines = 0
|
||||
toastView.alpha = 0
|
||||
toastView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let window = UIApplication.shared.delegate?.window!
|
||||
window?.addSubview(toastView)
|
||||
|
||||
let horizontalCenterContraint: NSLayoutConstraint = NSLayoutConstraint(item: toastView, attribute: .centerX, relatedBy: .equal, toItem: window, attribute: .centerX, multiplier: 1, constant: 0)
|
||||
|
||||
let widthContraint: NSLayoutConstraint = NSLayoutConstraint(item: toastView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 275)
|
||||
|
||||
let verticalContraint: [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(>=200)-[loginView(==50)]-68-|", options: [.alignAllCenterX, .alignAllCenterY], metrics: nil, views: ["loginView": toastView])
|
||||
|
||||
NSLayoutConstraint.activate([horizontalCenterContraint, widthContraint])
|
||||
NSLayoutConstraint.activate(verticalContraint)
|
||||
|
||||
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseIn, animations: {
|
||||
toastView.alpha = 1
|
||||
}, completion: nil)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double((Int64)(2 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC), execute: {
|
||||
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseIn, animations: {
|
||||
toastView.alpha = 0
|
||||
}, completion: { finished in
|
||||
toastView.removeFromSuperview()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
BIN
Emoney Info/Design/about.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
Emoney Info/Design/faq.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
Emoney Info/Design/history.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
Emoney Info/Design/home.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
Emoney Info/Design/kebijakan.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
Emoney Info/Design/settings.png
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
Emoney Info/Design/syarat.png
Normal file
|
After Width: | Height: | Size: 196 KiB |