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