Ship full-featured mobile clients with auth, payments, QRs, push notifications, and iOS polish including Figtree typography, app icons, and QR previews. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
import Foundation
|
||||
|
||||
enum APIError: LocalizedError {
|
||||
case invalidURL
|
||||
case http(Int, String?)
|
||||
case decoding(Error)
|
||||
case network(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL: return "Invalid request."
|
||||
case .http(let code, let message):
|
||||
if let message, !message.isEmpty { return message }
|
||||
if code == 401 { return "Your session expired. Please sign in again." }
|
||||
if code == 403 { return "You don't have permission to do that." }
|
||||
return "Request failed (\(code))."
|
||||
case .decoding: return "Could not read the server response."
|
||||
case .network: return "No connection. Check your network and try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class APIClient {
|
||||
static let shared = APIClient()
|
||||
|
||||
private let session: URLSession
|
||||
private let decoder: JSONDecoder
|
||||
private let encoder: JSONEncoder
|
||||
|
||||
private var baseURL: String {
|
||||
Bundle.main.object(forInfoDictionaryKey: "BASE_URL") as? String
|
||||
?? "https://mini.ladill.com/api/v1/"
|
||||
}
|
||||
|
||||
private init() {
|
||||
session = URLSession.shared
|
||||
decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
}
|
||||
|
||||
// MARK: - Auth
|
||||
|
||||
func login(email: String, password: String) async throws -> LoginResult {
|
||||
try await envelopePost("auth/login", body: LoginRequest(email: email, password: password))
|
||||
}
|
||||
|
||||
func register(_ body: RegisterRequest) async throws -> LoginResult {
|
||||
try await envelopePost("auth/register", body: body)
|
||||
}
|
||||
|
||||
func logout() async {
|
||||
_ = try? await voidRequest("auth/logout", method: "POST")
|
||||
}
|
||||
|
||||
func me() async throws -> User {
|
||||
try await envelopeGet("me")
|
||||
}
|
||||
|
||||
// MARK: - Mini
|
||||
|
||||
func overview() async throws -> Overview { try await envelopeGet("mini/overview") }
|
||||
|
||||
func afiaChat(message: String, history: [AfiaChatTurn]) async throws -> AfiaChatResponse {
|
||||
try await request("mini/afia/chat", method: "POST", body: AfiaChatRequest(message: message, history: history))
|
||||
}
|
||||
|
||||
func paymentQrs() async throws -> [PaymentQr] { try await envelopeGet("mini/payment-qrs") }
|
||||
|
||||
func createPaymentQr(_ body: CreatePaymentQrRequest) async throws -> PaymentQr {
|
||||
try await envelopePost("mini/payment-qrs", body: body)
|
||||
}
|
||||
|
||||
func paymentQr(id: Int64) async throws -> PaymentQr {
|
||||
try await envelopeGet("mini/payment-qrs/\(id)")
|
||||
}
|
||||
|
||||
func updatePaymentQr(id: Int64, body: UpdatePaymentQrRequest) async throws -> PaymentQr {
|
||||
try await envelopePatch("mini/payment-qrs/\(id)", body: body)
|
||||
}
|
||||
|
||||
func deletePaymentQr(id: Int64) async throws {
|
||||
try await voidRequest("mini/payment-qrs/\(id)", method: "DELETE")
|
||||
}
|
||||
|
||||
func payments(query: String? = nil) async throws -> [Payment] {
|
||||
var path = "mini/payments"
|
||||
if let query, !query.isEmpty,
|
||||
let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
|
||||
path += "?q=\(encoded)"
|
||||
}
|
||||
return try await envelopeGet(path)
|
||||
}
|
||||
|
||||
func payouts() async throws -> Payouts { try await envelopeGet("mini/payouts") }
|
||||
|
||||
func accountSettings() async throws -> AccountSettings {
|
||||
try await envelopeGet("mini/account/settings")
|
||||
}
|
||||
|
||||
func updateSettings(_ body: UpdateSettingsRequest) async throws -> NotificationPrefs {
|
||||
try await envelopePut("mini/account/settings", body: body)
|
||||
}
|
||||
|
||||
func updateProfile(_ body: UpdateProfileRequest) async throws -> ProfileInfo {
|
||||
try await envelopePut("mini/account/profile", body: body)
|
||||
}
|
||||
|
||||
func changePassword(_ body: ChangePasswordRequest) async throws -> MessageResponse {
|
||||
try await envelopePost("mini/account/change-password", body: body)
|
||||
}
|
||||
|
||||
func wallet() async throws -> WalletInfo { try await envelopeGet("mini/wallet") }
|
||||
|
||||
func walletTopup(amount: Double) async throws -> CheckoutUrl {
|
||||
try await envelopePost("mini/wallet/topup", body: TopupRequest(amount: amount))
|
||||
}
|
||||
|
||||
func banks(type: String) async throws -> [Bank] {
|
||||
try await envelopeGet("mini/wallet/banks?type=\(type)")
|
||||
}
|
||||
|
||||
func payoutAccount() async throws -> PayoutAccount? {
|
||||
let wrapper: PayoutAccountWrapper = try await envelopeGet("mini/wallet/payout-account")
|
||||
return wrapper.payoutAccount
|
||||
}
|
||||
|
||||
func updatePayoutAccount(_ body: UpdatePayoutAccountRequest) async throws -> PayoutAccount? {
|
||||
let wrapper: PayoutAccountWrapper = try await envelopePut("mini/wallet/payout-account", body: body)
|
||||
return wrapper.payoutAccount
|
||||
}
|
||||
|
||||
func withdrawals() async throws -> [Withdrawal] {
|
||||
try await envelopeGet("mini/wallet/withdrawals")
|
||||
}
|
||||
|
||||
func withdraw(amount: Double) async throws -> Withdrawal {
|
||||
try await envelopePost("mini/wallet/withdraw", body: WithdrawRequest(amount: amount))
|
||||
}
|
||||
|
||||
func supportTickets() async throws -> [SupportTicket] {
|
||||
try await envelopeGet("mini/support/tickets")
|
||||
}
|
||||
|
||||
func createSupportTicket(_ body: CreateTicketRequest) async throws -> SupportTicket {
|
||||
try await envelopePost("mini/support/tickets", body: body)
|
||||
}
|
||||
|
||||
func supportTicket(id: Int64) async throws -> SupportTicket {
|
||||
try await envelopeGet("mini/support/tickets/\(id)")
|
||||
}
|
||||
|
||||
func notifications() async throws -> NotificationsListResponse {
|
||||
try await request("mini/notifications", method: "GET")
|
||||
}
|
||||
|
||||
func unreadNotificationCount() async throws -> Int {
|
||||
let envelope: Envelope<UnreadCountEnvelope> = try await request("mini/notifications/unread-count", method: "GET")
|
||||
return envelope.data.unreadCount ?? 0
|
||||
}
|
||||
|
||||
func markNotificationRead(id: String) async throws {
|
||||
try await voidRequest("mini/notifications/\(id)/read", method: "POST")
|
||||
}
|
||||
|
||||
func markAllNotificationsRead() async throws {
|
||||
try await voidRequest("mini/notifications/mark-all-read", method: "POST")
|
||||
}
|
||||
|
||||
func registerPushToken(_ body: PushTokenRequest) async throws {
|
||||
try await voidRequest("mini/push-token", method: "POST", body: body)
|
||||
}
|
||||
|
||||
func unregisterPushToken(token: String) async throws {
|
||||
try await voidRequest("mini/push-token", method: "DELETE", body: PushTokenRequest(token: token))
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func envelopeGet<T: Decodable>(_ path: String) async throws -> T {
|
||||
let envelope: Envelope<T> = try await request(path, method: "GET")
|
||||
return envelope.data
|
||||
}
|
||||
|
||||
private func envelopePost<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
|
||||
let envelope: Envelope<T> = try await request(path, method: "POST", body: body)
|
||||
return envelope.data
|
||||
}
|
||||
|
||||
private func envelopePut<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
|
||||
let envelope: Envelope<T> = try await request(path, method: "PUT", body: body)
|
||||
return envelope.data
|
||||
}
|
||||
|
||||
private func envelopePatch<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
|
||||
let envelope: Envelope<T> = try await request(path, method: "PATCH", body: body)
|
||||
return envelope.data
|
||||
}
|
||||
|
||||
private func voidRequest(_ path: String, method: String) async throws {
|
||||
let _: Data = try await rawRequest(path, method: method, body: Optional<EmptyBody>.none)
|
||||
}
|
||||
|
||||
private func voidRequest<B: Encodable>(_ path: String, method: String, body: B) async throws {
|
||||
let _: Data = try await rawRequest(path, method: method, body: body)
|
||||
}
|
||||
|
||||
private func request<T: Decodable>(_ path: String, method: String) async throws -> T {
|
||||
let data = try await rawRequest(path, method: method, body: Optional<EmptyBody>.none)
|
||||
do {
|
||||
return try decoder.decode(T.self, from: data)
|
||||
} catch let error as DecodingError {
|
||||
throw APIError.decoding(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func request<T: Decodable, B: Encodable>(
|
||||
_ path: String,
|
||||
method: String,
|
||||
body: B
|
||||
) async throws -> T {
|
||||
let data = try await rawRequest(path, method: method, body: body)
|
||||
do {
|
||||
return try decoder.decode(T.self, from: data)
|
||||
} catch let error as DecodingError {
|
||||
throw APIError.decoding(error)
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmptyBody: Encodable {}
|
||||
|
||||
private func rawRequest<B: Encodable>(_ path: String, method: String, body: B?) async throws -> Data {
|
||||
guard let url = URL(string: baseURL + path) else { throw APIError.invalidURL }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
if body != nil {
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
let token = await SessionStore.shared.token
|
||||
if let token, !token.isEmpty {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
if let body {
|
||||
request.httpBody = try encoder.encode(body)
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, response) = try await session.data(for: request)
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
throw APIError.http(0, nil)
|
||||
}
|
||||
if http.statusCode >= 400 {
|
||||
throw APIError.http(http.statusCode, Self.parseErrorMessage(data))
|
||||
}
|
||||
return data
|
||||
} catch let error as APIError {
|
||||
throw error
|
||||
} catch {
|
||||
throw APIError.network(error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseErrorMessage(_ data: Data) -> String? {
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }
|
||||
if let errors = json["errors"] as? [String: Any],
|
||||
let first = errors.values.first as? [String],
|
||||
let message = first.first {
|
||||
return message
|
||||
}
|
||||
return json["message"] as? String
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
import Foundation
|
||||
|
||||
struct Envelope<T: Decodable>: Decodable { let data: T }
|
||||
|
||||
struct PagedEnvelope<T: Decodable>: Decodable {
|
||||
let data: [T]
|
||||
let meta: PageMeta?
|
||||
}
|
||||
|
||||
struct PageMeta: Decodable {
|
||||
let currentPage: Int?
|
||||
let lastPage: Int?
|
||||
let perPage: Int?
|
||||
let total: Int?
|
||||
}
|
||||
|
||||
struct LoginRequest: Encodable {
|
||||
let email: String
|
||||
let password: String
|
||||
let deviceName = "Ladill Mini iOS"
|
||||
}
|
||||
|
||||
struct LoginResult: Decodable {
|
||||
let token: String
|
||||
let user: User
|
||||
}
|
||||
|
||||
struct RegisterRequest: Encodable {
|
||||
let name: String
|
||||
let email: String
|
||||
let password: String
|
||||
let passwordConfirmation: String
|
||||
let company: String?
|
||||
let address: String
|
||||
let city: String
|
||||
let state: String
|
||||
let country: String
|
||||
let zipcode: String
|
||||
let phoneCc: String
|
||||
let phone: String
|
||||
let mobileCc: String?
|
||||
let mobile: String?
|
||||
let terms: Bool
|
||||
let deviceName = "Ladill Mini iOS"
|
||||
}
|
||||
|
||||
struct User: Decodable {
|
||||
let id: Int64
|
||||
let publicId: String?
|
||||
let name: String?
|
||||
let email: String?
|
||||
let avatarUrl: String?
|
||||
let actingAccount: Account?
|
||||
}
|
||||
|
||||
struct Account: Decodable {
|
||||
let id: Int64
|
||||
let publicId: String?
|
||||
let name: String?
|
||||
let email: String?
|
||||
}
|
||||
|
||||
struct Overview: Decodable {
|
||||
let currency: String?
|
||||
let todayTakingsMinor: Int64?
|
||||
let todayCount: Int?
|
||||
let paymentQrCount: Int?
|
||||
let walletBalanceMinor: Int64?
|
||||
let recentPayments: [Payment]?
|
||||
}
|
||||
|
||||
struct Payment: Decodable, Identifiable {
|
||||
let id: Int64
|
||||
let reference: String?
|
||||
let amountMinor: Int64?
|
||||
let merchantAmountMinor: Int64?
|
||||
let platformFeeMinor: Int64?
|
||||
let currency: String?
|
||||
let status: String?
|
||||
let payerName: String?
|
||||
let payerEmail: String?
|
||||
let payerPhone: String?
|
||||
let payerNote: String?
|
||||
let qrCodeId: Int64?
|
||||
let qrLabel: String?
|
||||
let paidAt: String?
|
||||
let createdAt: String?
|
||||
}
|
||||
|
||||
struct PaymentQr: Decodable, Identifiable {
|
||||
let id: Int64
|
||||
let label: String
|
||||
let businessName: String?
|
||||
let branchLabel: String?
|
||||
let currency: String?
|
||||
let shortCode: String?
|
||||
let publicUrl: String?
|
||||
let isActive: Bool?
|
||||
let scansTotal: Int?
|
||||
let previewUrl: String?
|
||||
let createdAt: String?
|
||||
let updatedAt: String?
|
||||
}
|
||||
|
||||
struct CreatePaymentQrRequest: Encodable {
|
||||
let label: String
|
||||
let businessName: String
|
||||
let branchLabel: String?
|
||||
}
|
||||
|
||||
struct UpdatePaymentQrRequest: Encodable {
|
||||
var label: String?
|
||||
var businessName: String?
|
||||
var branchLabel: String?
|
||||
var isActive: Bool?
|
||||
}
|
||||
|
||||
struct Payouts: Decodable {
|
||||
let currency: String?
|
||||
let totalRevenueMinor: Int64?
|
||||
let walletBalanceMinor: Int64?
|
||||
let accountWalletUrl: String?
|
||||
}
|
||||
|
||||
struct AccountSettings: Decodable {
|
||||
let profile: ProfileInfo
|
||||
let notifications: NotificationPrefs
|
||||
}
|
||||
|
||||
struct ProfileInfo: Decodable {
|
||||
let name: String?
|
||||
let email: String?
|
||||
let phone: String?
|
||||
let phoneCc: String?
|
||||
}
|
||||
|
||||
struct NotificationPrefs: Codable {
|
||||
var notifyEmail: String?
|
||||
var productUpdates: Bool?
|
||||
var notifyRegistrations: Bool?
|
||||
var notifyPayouts: Bool?
|
||||
}
|
||||
|
||||
struct UpdateSettingsRequest: Encodable {
|
||||
let notifyEmail: String?
|
||||
let productUpdates: Bool
|
||||
let notifyRegistrations: Bool
|
||||
let notifyPayouts: Bool
|
||||
}
|
||||
|
||||
struct UpdateProfileRequest: Encodable {
|
||||
let name: String
|
||||
let phoneCc: String?
|
||||
let phone: String?
|
||||
}
|
||||
|
||||
struct AvatarResult: Decodable { let avatarUrl: String? }
|
||||
|
||||
struct ChangePasswordRequest: Encodable {
|
||||
let currentPassword: String
|
||||
let password: String
|
||||
let passwordConfirmation: String
|
||||
}
|
||||
|
||||
struct MessageResponse: Decodable { let message: String? }
|
||||
|
||||
struct AfiaChatRequest: Encodable {
|
||||
let message: String
|
||||
let history: [AfiaChatTurn]
|
||||
}
|
||||
|
||||
struct AfiaChatTurn: Encodable {
|
||||
let role: String
|
||||
let text: String
|
||||
}
|
||||
|
||||
struct AfiaChatResponse: Decodable {
|
||||
let reply: String?
|
||||
let message: String?
|
||||
}
|
||||
|
||||
struct WalletInfo: Decodable {
|
||||
let currency: String?
|
||||
let balanceMinor: Int64?
|
||||
let spentMinor: Int64?
|
||||
let creditedMinor: Int64?
|
||||
}
|
||||
|
||||
struct TopupRequest: Encodable { let amount: Double }
|
||||
|
||||
struct CheckoutUrl: Decodable { let checkoutUrl: String }
|
||||
|
||||
struct PayoutAccountWrapper: Decodable { let payoutAccount: PayoutAccount? }
|
||||
|
||||
struct PayoutAccount: Decodable {
|
||||
let accountType: String?
|
||||
let accountName: String?
|
||||
let accountNumber: String?
|
||||
let bankCode: String?
|
||||
let bankName: String?
|
||||
let currency: String?
|
||||
}
|
||||
|
||||
struct UpdatePayoutAccountRequest: Encodable {
|
||||
let accountType: String
|
||||
let accountName: String
|
||||
let accountNumber: String
|
||||
let bankCode: String
|
||||
let bankName: String
|
||||
let currency: String
|
||||
}
|
||||
|
||||
struct Bank: Decodable, Identifiable {
|
||||
var id: String { code }
|
||||
let name: String
|
||||
let code: String
|
||||
}
|
||||
|
||||
struct WithdrawRequest: Encodable { let amount: Double }
|
||||
|
||||
struct Withdrawal: Decodable, Identifiable {
|
||||
let id: Int64
|
||||
let amountGhs: Double?
|
||||
let status: String?
|
||||
let createdAt: String?
|
||||
}
|
||||
|
||||
struct SupportTicket: Decodable, Identifiable {
|
||||
let id: Int64
|
||||
let ticketRef: String?
|
||||
let subject: String?
|
||||
let status: String?
|
||||
let priority: String?
|
||||
let hasReply: Bool?
|
||||
let createdAt: String?
|
||||
let message: String?
|
||||
let reply: String?
|
||||
let repliedAt: String?
|
||||
}
|
||||
|
||||
struct CreateTicketRequest: Encodable {
|
||||
let subject: String
|
||||
let message: String
|
||||
let priority: String
|
||||
}
|
||||
|
||||
struct NotificationsListResponse: Decodable {
|
||||
let data: [AppNotification]
|
||||
let unreadCount: Int?
|
||||
}
|
||||
|
||||
struct AppNotification: Decodable, Identifiable {
|
||||
let id: String
|
||||
let type: String?
|
||||
let title: String
|
||||
let message: String
|
||||
let icon: String?
|
||||
let milestone: String?
|
||||
let url: String?
|
||||
let readAt: String?
|
||||
let createdAt: String?
|
||||
|
||||
var isRead: Bool { readAt != nil }
|
||||
}
|
||||
|
||||
struct UnreadCountEnvelope: Decodable { let unreadCount: Int? }
|
||||
|
||||
struct PushTokenRequest: Encodable {
|
||||
let token: String
|
||||
let platform = "ios"
|
||||
let deviceName = "Ladill Mini iOS"
|
||||
}
|
||||
|
||||
struct Country: Identifiable, Hashable {
|
||||
var id: String { code }
|
||||
let code: String
|
||||
let name: String
|
||||
let dialCode: String
|
||||
let states: [String]
|
||||
}
|
||||
|
||||
enum Countries {
|
||||
static let all: [Country] = [
|
||||
Country(code: "GH", name: "Ghana", dialCode: "233", states: [
|
||||
"Ahafo", "Ashanti", "Bono", "Bono East", "Central", "Eastern", "Greater Accra",
|
||||
"North East", "Northern", "Oti", "Savannah", "Upper East", "Upper West", "Volta",
|
||||
"Western", "Western North",
|
||||
]),
|
||||
Country(code: "NG", name: "Nigeria", dialCode: "234", states: [
|
||||
"Abia", "Abuja FCT", "Adamawa", "Akwa Ibom", "Anambra", "Bauchi", "Bayelsa", "Benue",
|
||||
"Borno", "Cross River", "Delta", "Ebonyi", "Edo", "Ekiti", "Enugu", "Gombe", "Imo",
|
||||
"Jigawa", "Kaduna", "Kano", "Katsina", "Kebbi", "Kogi", "Kwara", "Lagos", "Nasarawa",
|
||||
"Niger", "Ogun", "Ondo", "Osun", "Oyo", "Plateau", "Rivers", "Sokoto", "Taraba",
|
||||
"Yobe", "Zamfara",
|
||||
]),
|
||||
Country(code: "KE", name: "Kenya", dialCode: "254", states: [
|
||||
"Baringo", "Bomet", "Bungoma", "Busia", "Elgeyo-Marakwet", "Embu", "Garissa",
|
||||
"Homa Bay", "Isiolo", "Kajiado", "Kakamega", "Kericho", "Kiambu", "Kilifi",
|
||||
"Kirinyaga", "Kisii", "Kisumu", "Kitui", "Kwale", "Laikipia", "Lamu", "Machakos",
|
||||
"Makueni", "Mandera", "Marsabit", "Meru", "Migori", "Mombasa", "Murang'a", "Nairobi",
|
||||
"Nakuru", "Nandi", "Narok", "Nyamira", "Nyandarua", "Nyeri", "Samburu", "Siaya",
|
||||
"Taita-Taveta", "Tana River", "Tharaka-Nithi", "Trans Nzoia", "Turkana",
|
||||
"Uasin Gishu", "Vihiga", "Wajir", "West Pokot",
|
||||
]),
|
||||
Country(code: "ZA", name: "South Africa", dialCode: "27", states: [
|
||||
"Eastern Cape", "Free State", "Gauteng", "KwaZulu-Natal", "Limpopo", "Mpumalanga",
|
||||
"North West", "Northern Cape", "Western Cape",
|
||||
]),
|
||||
Country(code: "GB", name: "United Kingdom", dialCode: "44", states: [
|
||||
"England", "Northern Ireland", "Scotland", "Wales",
|
||||
]),
|
||||
Country(code: "US", name: "United States", dialCode: "1", states: [
|
||||
"Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut",
|
||||
"Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa",
|
||||
"Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan",
|
||||
"Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada",
|
||||
"New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina",
|
||||
"North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island",
|
||||
"South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont",
|
||||
"Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming",
|
||||
]),
|
||||
]
|
||||
|
||||
static var `default`: Country { all[0] }
|
||||
}
|
||||
|
||||
enum LoadState<T> {
|
||||
case idle
|
||||
case loading
|
||||
case success(T)
|
||||
case failure(String)
|
||||
}
|
||||
Reference in New Issue
Block a user