Files
andros 783cd0e1f2 Add in-app account deletion and bump build to 1.0 (4)
Settings → Danger zone → Delete account. For vfile accounts the host
file is removed via POST /delete; for every method local credentials,
follows, drafts, blocked accounts and muted words are wiped and the
app returns to the Welcome screen. Required by App Review guideline
5.1.1(v).
2026-05-04 11:18:29 +02:00

84 lines
3.5 KiB
Swift

import Foundation
/// Deletes a hosted vfile account from an Org Social host.
///
/// Mirrors `HostSignupClient`: the endpoint is `POST {scheme}://{host}/delete`
/// with body `{"vfile": "<token-url>"}`. The host responds with the standard
/// `{"type": "Success"|"Error", ...}` envelope.
///
/// This is irreversible on the server side and is the App Store's preferred
/// path for Guideline 5.1.1(v) Account Deletion.
public struct HostDeleteClient: Sendable {
public enum DeleteError: Error, LocalizedError {
case invalidURL
case invalidToken
case fileNotFound
case httpError(statusCode: Int, serverMessage: String?)
case malformedResponse
case network(underlying: String)
public var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid host URL."
case .invalidToken: return "Invalid vfile token."
case .fileNotFound: return "Account not found on the host."
case .httpError(let code, let msg): return msg ?? "HTTP \(code) from the host."
case .malformedResponse: return "The host returned a response we could not parse."
case .network(let msg): return "Network error: \(msg)"
}
}
}
private let session: URLSession
public init(session: URLSession = .shared) { self.session = session }
/// Deletes the social.org file referenced by `vfileURL` from its host.
///
/// - Parameter vfileURL: The vfile token URL returned by `/signup`. Used both
/// to derive the host (scheme + authority) and as the `vfile` body field.
public func delete(vfileURL: URL) async throws {
guard let scheme = vfileURL.scheme, let host = vfileURL.host else {
throw DeleteError.invalidURL
}
let base = vfileURL.port.map { "\(scheme)://\(host):\($0)" } ?? "\(scheme)://\(host)"
guard let url = URL(string: "\(base)/delete") else { throw DeleteError.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: ["vfile": vfileURL.absoluteString])
let data: Data
let response: URLResponse
do {
(data, response) = try await session.data(for: request)
} catch {
throw DeleteError.network(underlying: error.localizedDescription)
}
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
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("invalid") && first.localizedCaseInsensitiveContains("token") {
throw DeleteError.invalidToken
}
if first.localizedCaseInsensitiveContains("not found") {
throw DeleteError.fileNotFound
}
throw DeleteError.httpError(statusCode: statusCode, serverMessage: first)
}
if !(200..<300).contains(statusCode) {
throw DeleteError.httpError(statusCode: statusCode, serverMessage: nil)
}
}
}