Implement bank transfer app flows
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import Foundation
|
||||
|
||||
// Mirrors the Kotlin MibContactsClient exactly.
|
||||
// Endpoint: POST https://faisamobilex-wv.mib.com.mv/ajaxBeneficiary/main
|
||||
// Auth: Cookie header with xxid, IBSID, mbnonce, mbmodel (same session established by MibLoginFlow).
|
||||
actor MibContactsClient {
|
||||
|
||||
private let wvBase = "https://faisamobilex-wv.mib.com.mv"
|
||||
|
||||
private let urlSession: URLSession = {
|
||||
let cfg = URLSessionConfiguration.ephemeral
|
||||
cfg.timeoutIntervalForRequest = 30
|
||||
cfg.timeoutIntervalForResource = 60
|
||||
cfg.httpCookieAcceptPolicy = .never
|
||||
cfg.httpShouldSetCookies = false
|
||||
return URLSession(configuration: cfg)
|
||||
}()
|
||||
|
||||
private let session: MibSession
|
||||
|
||||
init(session: MibSession) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func fetchContacts(loginTag: String) async throws -> [BankContact] {
|
||||
var all: [BankContact] = []
|
||||
var page = 1
|
||||
let pageSize = 100
|
||||
|
||||
while true {
|
||||
let start = (page - 1) * pageSize + 1
|
||||
let end = page * pageSize
|
||||
|
||||
let formFields: [(String, String)] = [
|
||||
("page", String(page)),
|
||||
("search", ""),
|
||||
("searchCategoryId", "0"),
|
||||
("benefType", "A"),
|
||||
("sortBenef", "name"),
|
||||
("sortDir", "asc"),
|
||||
("start", String(start)),
|
||||
("end", String(end)),
|
||||
("includeCount", "1"),
|
||||
]
|
||||
|
||||
let (contacts, totalCount) = try await fetchPage(formFields: formFields, loginTag: loginTag)
|
||||
all.append(contentsOf: contacts)
|
||||
if all.count >= totalCount || contacts.isEmpty { break }
|
||||
page += 1
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func fetchPage(formFields: [(String, String)], loginTag: String) async throws -> ([BankContact], Int) {
|
||||
let url = URL(string: "\(wvBase)/ajaxBeneficiary/main")!
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")
|
||||
request.setValue(
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
|
||||
forHTTPHeaderField: "User-Agent"
|
||||
)
|
||||
request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
|
||||
request.setValue("*/*", forHTTPHeaderField: "Accept")
|
||||
request.setValue(wvBase, forHTTPHeaderField: "Origin")
|
||||
request.setValue("\(wvBase)/beneficiary?dashurl=1", forHTTPHeaderField: "Referer")
|
||||
|
||||
var allowed = CharacterSet.alphanumerics
|
||||
allowed.insert(charactersIn: "-._~")
|
||||
let body = formFields.map { k, v in
|
||||
"\(k.addingPercentEncoding(withAllowedCharacters: allowed) ?? k)=\(v.addingPercentEncoding(withAllowedCharacters: allowed) ?? v)"
|
||||
}.joined(separator: "&")
|
||||
request.httpBody = Data(body.utf8)
|
||||
|
||||
let (data, response) = try await urlSession.data(for: request)
|
||||
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||
if code == 419 { throw MibError.sessionExpired }
|
||||
if code >= 500 { throw MibError.networkError("HTTP \(code)") }
|
||||
|
||||
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw MibError.invalidResponse
|
||||
}
|
||||
|
||||
let totalCount = obj["total_count"] as? Int
|
||||
?? Int(obj["total_count"] as? String ?? "") ?? 0
|
||||
|
||||
guard let arr = obj["data"] as? [[String: Any]] else {
|
||||
return ([], totalCount)
|
||||
}
|
||||
|
||||
let contacts = arr.compactMap { d -> BankContact? in
|
||||
let account = d["benefAccount"] as? String ?? ""
|
||||
guard !account.isEmpty else { return nil }
|
||||
|
||||
let benefNo = d["benefNo"] as? String ?? account
|
||||
// Android: benefType "I"=Internal(MIB), "L"=Local(IPS), "S"=Swift
|
||||
let rawType = d["benefType"] as? String ?? "I"
|
||||
let benefType: String
|
||||
switch rawType {
|
||||
case "I": benefType = "MIB"
|
||||
case "L": benefType = "LOCAL"
|
||||
case "S": benefType = "SWIFT"
|
||||
default: benefType = rawType
|
||||
}
|
||||
|
||||
return BankContact(
|
||||
id: "MIB_\(benefNo)_\(loginTag)",
|
||||
benefNo: benefNo,
|
||||
benefName: d["benefName"] as? String ?? "",
|
||||
benefNickName: (d["benefNickName"] as? String).flatMap { $0.isEmpty ? nil : $0 },
|
||||
benefAccount: account,
|
||||
benefType: benefType,
|
||||
bankColor: d["bankColor"] as? String,
|
||||
benefBankName: d["benefBankName"] as? String,
|
||||
bankCode: d["bankCode"] as? String,
|
||||
benefStatus: d["benefStatus"] as? String ?? "Active",
|
||||
transferCyDesc: d["transferCyDesc"] as? String ?? "MVR",
|
||||
customerImgHash: (d["customerImgHash"] as? String).flatMap { $0.isEmpty || $0 == "null" ? nil : $0 },
|
||||
benefCategoryId: d["benefCategoryID"] as? String,
|
||||
profileId: nil,
|
||||
source: "MIB"
|
||||
)
|
||||
}
|
||||
|
||||
return (contacts, totalCount)
|
||||
}
|
||||
|
||||
private var cookieHeader: String {
|
||||
"mbmodel=IOS-1.0; xxid=\(session.xxid); IBSID=\(session.xxid); mbnonce=\(session.nonceGenerator); time-tracker=597"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user