aa0a5fb356
- New WelcomeView gates first-run users with existing-account or signup paths - HostSignupClient posts to host.org-social.org /signup and auto-configures Settings - Show all relay feeds toggle (default off) restricts timeline to #+FOLLOW entries - Contextual empty-state in timeline routes to Discover or toggles relay-wide view - RootTab enum enables programmatic tab switching from child views - App icon background switched to cream for better contrast in iOS home screen - Remove pre-registered test account; fresh installs land on Welcome
97 lines
4.1 KiB
Swift
97 lines
4.1 KiB
Swift
import Foundation
|
|
|
|
/// Creates accounts on an Org Social host (e.g. host.org-social.org).
|
|
///
|
|
/// The host exposes a free sign-up endpoint that returns a fresh vfile token URL
|
|
/// plus the public `social.org` URL for the chosen nickname.
|
|
public struct HostSignupClient: Sendable {
|
|
|
|
public enum SignupError: Error, LocalizedError {
|
|
case invalidURL
|
|
case httpError(statusCode: Int, serverMessage: String?)
|
|
case nicknameRequired
|
|
case nicknameTaken(String)
|
|
case malformedResponse
|
|
case network(underlying: String)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .invalidURL: return "Invalid host URL."
|
|
case .httpError(let code, let msg): return msg ?? "HTTP \(code) from the host."
|
|
case .nicknameRequired: return "Nickname is required."
|
|
case .nicknameTaken(let nick): return "Nickname '\(nick)' is already taken."
|
|
case .malformedResponse: return "The host returned a response we could not parse."
|
|
case .network(let msg): return "Network error: \(msg)"
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct SignupResult: Sendable {
|
|
public let vfileURL: URL
|
|
public let publicURL: URL
|
|
}
|
|
|
|
private let session: URLSession
|
|
|
|
public init(session: URLSession = .shared) { self.session = session }
|
|
|
|
/// Registers `nick` on `host` and returns the resulting vfile token URL and public feed URL.
|
|
///
|
|
/// - Parameters:
|
|
/// - nick: The nickname to claim. Must be non-empty; the server enforces uniqueness.
|
|
/// - host: Base URL of the host, e.g. `https://host.org-social.org`.
|
|
public func signup(nick: String, host: URL = URL(string: "https://host.org-social.org")!) async throws -> SignupResult {
|
|
let trimmed = nick.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { throw SignupError.nicknameRequired }
|
|
guard let url = URL(string: "\(host.absoluteString.trimmingCharacters(in: .init(charactersIn: "/")))/signup") else {
|
|
throw SignupError.invalidURL
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.timeoutInterval = 15
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.httpBody = try? JSONSerialization.data(withJSONObject: ["nick": trimmed])
|
|
|
|
let data: Data
|
|
let response: URLResponse
|
|
do {
|
|
(data, response) = try await session.data(for: request)
|
|
} catch {
|
|
throw SignupError.network(underlying: error.localizedDescription)
|
|
}
|
|
|
|
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
|
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
// Error shape: {"type": "Error", "errors": ["..."]}
|
|
if let json,
|
|
let type = json["type"] as? String,
|
|
type == "Error" {
|
|
let messages = (json["errors"] as? [String]) ?? []
|
|
let first = messages.first ?? "Unknown error."
|
|
if first.localizedCaseInsensitiveContains("already taken") {
|
|
throw SignupError.nicknameTaken(trimmed)
|
|
}
|
|
if first.localizedCaseInsensitiveContains("required") {
|
|
throw SignupError.nicknameRequired
|
|
}
|
|
throw SignupError.httpError(statusCode: statusCode, serverMessage: first)
|
|
}
|
|
|
|
if !(200..<300).contains(statusCode) {
|
|
throw SignupError.httpError(statusCode: statusCode, serverMessage: nil)
|
|
}
|
|
|
|
// Success shape: {"type": "Success", "data": {"vfile": "...", "public-url": "..."}}
|
|
guard let data = json?["data"] as? [String: Any],
|
|
let vfileStr = data["vfile"] as? String,
|
|
let publicStr = data["public-url"] as? String,
|
|
let vfileURL = URL(string: vfileStr),
|
|
let publicURL = URL(string: publicStr) else {
|
|
throw SignupError.malformedResponse
|
|
}
|
|
return SignupResult(vfileURL: vfileURL, publicURL: publicURL)
|
|
}
|
|
}
|