iOS client for Grain grain.social
ios photography atproto
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Delete Account action to native settings

Adds a Delete Account row at the bottom of Settings, Account that calls
the new social.grain.unspecced.deleteAccount xrpc, signs the user out,
and dismisses the settings sheet. Uses a destructive alert so the user
can't trip the action accidentally. Error surface in the section footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+52 -1
+6
Grain/API/Endpoints/RecordEndpoints.swift
··· 53 53 func deleteGallery(rkey: String, auth: AuthContext? = nil) async throws { 54 54 try await procedure("social.grain.unspecced.deleteGallery", input: DeleteGalleryInput(rkey: rkey), auth: auth) 55 55 } 56 + 57 + func deleteAccount(auth: AuthContext? = nil) async throws { 58 + try await procedure("social.grain.unspecced.deleteAccount", input: EmptyInput(), auth: auth) 59 + } 56 60 } 61 + 62 + private struct EmptyInput: Codable, Sendable {}
+46 -1
Grain/Views/Settings/SettingsView.swift
··· 28 28 LabeledContent("Appearance", value: appearanceLabel) 29 29 } 30 30 NavigationLink("Account") { 31 - AccountDetailView() 31 + AccountDetailView(client: client) 32 32 } 33 33 NavigationLink("Notifications") { 34 34 NotificationSettingsView(client: client) ··· 126 126 127 127 private struct AccountDetailView: View { 128 128 @Environment(AuthManager.self) private var auth 129 + @Environment(\.dismiss) private var dismiss 130 + let client: XRPCClient 129 131 @State private var safariURL: URL? 132 + @State private var showDeleteConfirm = false 133 + @State private var isDeleting = false 134 + @State private var deleteError: String? 130 135 131 136 var body: some View { 132 137 List { ··· 170 175 } 171 176 } 172 177 } 178 + 179 + Section { 180 + Button(role: .destructive) { 181 + showDeleteConfirm = true 182 + } label: { 183 + if isDeleting { 184 + HStack { 185 + ProgressView() 186 + Text("Deleting…") 187 + } 188 + } else { 189 + Text("Delete Account") 190 + } 191 + } 192 + .disabled(isDeleting) 193 + } footer: { 194 + if let deleteError { 195 + Text(deleteError).foregroundStyle(.red) 196 + } 197 + } 173 198 } 174 199 .sheet(item: $safariURL) { url in 175 200 SafariView(url: url) ··· 177 202 } 178 203 .navigationTitle("Account") 179 204 .tint(.primary) 205 + .alert("Delete your Grain account?", isPresented: $showDeleteConfirm) { 206 + Button("Delete Account", role: .destructive) { Task { await performDelete() } } 207 + Button("Cancel", role: .cancel) {} 208 + } message: { 209 + Text("This removes all your Grain galleries, stories, photos, favorites, comments, follows, and blocks. Your atproto identity is separate and is not affected. This cannot be undone.") 210 + } 211 + } 212 + 213 + private func performDelete() async { 214 + guard let authContext = await auth.authContext() else { return } 215 + isDeleting = true 216 + deleteError = nil 217 + do { 218 + try await client.deleteAccount(auth: authContext) 219 + auth.logout() 220 + dismiss() 221 + } catch { 222 + deleteError = error.localizedDescription 223 + isDeleting = false 224 + } 180 225 } 181 226 } 182 227