783cd0e1f2
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).
84 lines
3.5 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|