138 lines
5.5 KiB
Swift
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"
|
|
}
|
|
}
|