Files

138 lines
5.5 KiB
Swift

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"
}
}