Use atproto actions with ease in iOS shortcuts
1//
2// SettingsView.swift
3// shortcut
4//
5// Created by Bailey Townsend on 7/7/25.
6//
7
8import Foundation
9import StoreKit
10import SwiftUI
11
12struct SettingsView: View {
13 @State private var showTipJar = false
14 @State private var tmpDirectorySize: String?
15 @State private var deleteTmpLoading = false
16
17 let openSourceProjects = [
18 ("MasterJ93/ATProtoKit", "https://github.com/MasterJ93/ATProtoKit"),
19 ("ATProtoKit/ATIdentityTools", "https://github.com/ATProtoKit/ATIdentityTools"),
20 ("hyperoslo/Cache", "https://github.com/hyperoslo/Cache"),
21 ]
22
23 var body: some View {
24 NavigationStack {
25 VStack {
26 List {
27 // Tip Jar Section
28 Section {
29 Button(action: {
30 showTipJar = true
31 }) {
32 HStack {
33 Image(systemName: "heart.fill")
34 .foregroundColor(.pink)
35 .frame(width: 28)
36 Text("Tip Jar")
37 .foregroundColor(.primary)
38 Spacer()
39 Image(systemName: "chevron.right")
40 .foregroundColor(.secondary)
41 .font(.caption)
42 }
43 }
44 }
45
46 // About Section
47 Section {
48
49 Link(
50 destination: URL(
51 string: "https://attoolbox.baileytownsend.dev/#privacy")!
52 ) {
53 Label(
54 "Privacy Policy",
55 systemImage: "lock.fill"
56 )
57 .font(.body)
58 .foregroundColor(.blue)
59 }
60 Link(
61 destination: URL(
62 string:
63 "https://apps.apple.com/app/at-toolbox/id6747999688?action=write-review"
64 )!
65 ) {
66 Label(
67 "Leave A Review",
68 systemImage: "star.fill"
69 )
70 .font(.body)
71 .foregroundColor(.blue)
72 }
73
74 Link(
75 destination: URL(
76 string: "https://attoolbox.baileytownsend.dev/")!
77 ) {
78 Label(
79 "Website",
80 systemImage: "globe"
81 )
82 .font(.body)
83 .foregroundColor(.blue)
84 }
85
86 Link(
87 destination: URL(string: "https://bsky.app/profile/baileytownsend.dev")!
88 ) {
89 Label(
90 "Created by @baileytownsend.dev",
91 systemImage: "person.text.rectangle"
92 )
93 .font(.body)
94 .foregroundColor(.blue)
95 }
96
97 }
98
99 // Special Thanks Section
100 Section(header: Text("Made possible Thanks to these projects")) {
101
102 ForEach(openSourceProjects, id: \.0) { project in
103
104 HStack {
105 Link(destination: URL(string: project.1)!) {
106 Label(
107 project.0,
108 systemImage: "chevron.left.forwardslash.chevron.right"
109 )
110 .font(.body)
111 .foregroundColor(.blue)
112 }
113
114 }
115
116 }
117
118 }
119
120 Section {
121 if let tmpSize = self.tmpDirectorySize {
122 Button {
123 withAnimation {
124 self.deleteTmpLoading = true
125 }
126
127 Task {
128 defer {
129 Task { @MainActor in
130 withAnimation {
131 self.deleteTmpLoading = false
132 }
133 }
134 }
135
136 let tmpDirectory = FileManager.default.temporaryDirectory
137 do {
138 try DirectoryCleaner.clearDirectoryCompletely(tmpDirectory)
139
140 let totalSize = DirectoryCleaner.getTotalSize(
141 of: tmpDirectory)
142 await MainActor.run {
143 self.tmpDirectorySize = ByteCountFormatter.string(
144 fromByteCount: totalSize, countStyle: .file)
145 }
146 } catch {
147 print("Error: \(error)")
148 }
149 }
150 } label: {
151
152 Text("Clear temp folder: \(tmpSize)")
153 .frame(maxWidth: .infinity)
154 }
155 .buttonStyle(LoadingButtonStyle(isLoading: self.deleteTmpLoading))
156 .disabled(deleteTmpLoading)
157 }
158
159 }
160
161 }
162 .listStyle(InsetGroupedListStyle())
163
164 }
165 .onAppear {
166 let tmpDirectory = FileManager.default.temporaryDirectory
167 let totalSize = DirectoryCleaner.getTotalSize(
168 of: tmpDirectory)
169 self.tmpDirectorySize = ByteCountFormatter.string(
170 fromByteCount: totalSize, countStyle: .file)
171 }
172 .navigationTitle("Info")
173 .sheet(isPresented: $showTipJar) {
174 TipJarView()
175 }
176 }
177 }
178}
179
180struct TipJarView: View {
181 @Environment(\.dismiss) var dismiss
182 @State var showThankYouAlert: Bool = false
183 @State private var transactionListener: Task<Void, Error>? = nil
184
185 var body: some View {
186 NavigationView {
187 VStack(spacing: 20) {
188 // Header
189 VStack(spacing: 8) {
190 Text("❤️")
191 .font(.system(size: 60))
192 Text("Support the Developer")
193 .font(.title2)
194 .fontWeight(.semibold)
195 Text("Your support helps keep the app updated and new features added!")
196 .font(.subheadline)
197 .foregroundColor(.secondary)
198 .multilineTextAlignment(.center)
199 .padding(.horizontal)
200 }
201 .padding(.top, 20)
202
203 // Tip Options
204 List {
205 ProductView(id: "tip1")
206 .listRowSeparator(.hidden)
207
208 ProductView(id: "tip2")
209 .listRowSeparator(.hidden)
210
211 ProductView(id: "tip3")
212 .listRowSeparator(.hidden)
213
214 }
215 .listStyle(.plain)
216 .padding(.horizontal)
217 .alert("Thank you!", isPresented: $showThankYouAlert) {
218 //
219 // Link(
220 // "Post about it to Bluesky",
221 // destination: URL(
222 // string:
223 // "https://bsky.app/intent/compose?text=I just left AT Toolbox a tip!\nhttps://attoolbox.baileytownsend.dev"
224 // )!)
225
226 Button("Close") {
227 self.showThankYouAlert = false
228 }
229 } message: {
230 Text(
231 "Thank you so much for your support! Your tips help fund new features and to keep the app updated!"
232 )
233 }
234 Spacer()
235
236 }
237 .navigationBarTitle("Tip Jar", displayMode: .inline)
238 .navigationBarItems(trailing: Button("Done") { dismiss() })
239 .onAppear {
240 transactionListener = createTransactionTask()
241 }
242 .onDisappear {
243 transactionListener?.cancel()
244 }
245
246 }
247 }
248
249 private func createTransactionTask() -> Task<Void, Error> {
250 return Task {
251 for await update in Transaction.updates {
252
253 switch update {
254 case .verified(let transaction):
255 //TODO show alert
256 //Consume tip right away so another can be made
257 await transaction.finish()
258 self.showThankYouAlert = true
259
260 break
261
262 case .unverified:
263 //guess we will not do anything here?
264 break
265
266 }
267
268 }
269 }
270 }
271
272}
273
274#Preview {
275 SettingsView()
276}