|
1 | 1 | import SwiftUI |
2 | 2 | import ZIPFoundation |
3 | 3 |
|
4 | | -// MARK: - Release Models (for Loyahdev) |
| 4 | +// MARK: - Release Models |
5 | 5 | struct Release: Codable, Identifiable, Equatable, Hashable { |
6 | 6 | let id: Int |
7 | 7 | let name: String |
@@ -47,297 +47,6 @@ struct BlobResponse: Codable { |
47 | 47 | let content: String? |
48 | 48 | } |
49 | 49 |
|
50 | | -// MARK: - Loyahdev Certificates View |
51 | | -struct LoyahdevCertificatesView: View { |
52 | | - @Environment(\.dismiss) private var dismiss |
53 | | - @State private var releases: [Release] = [] |
54 | | - @State private var selectedRelease: Release? = nil |
55 | | - @State private var statusMessage = "" |
56 | | - @State private var isChecking = false |
57 | | - @State private var isLoadingReleases = true |
58 | | - @State private var p12Data: Data? = nil |
59 | | - @State private var provData: Data? = nil |
60 | | - @State private var password: String? = nil |
61 | | - @State private var displayName = "" |
62 | | - @State private var expiry: Date? = nil |
63 | | - |
64 | | - private var isSuccess: Bool { |
65 | | - statusMessage.contains("Success") |
66 | | - } |
67 | | - |
68 | | - private var statusColor: Color { |
69 | | - if statusMessage.contains("Downloading") { |
70 | | - return .yellow |
71 | | - } else if isSuccess { |
72 | | - return .green |
73 | | - } else { |
74 | | - return .red |
75 | | - } |
76 | | - } |
77 | | - |
78 | | - private let dateFormatter: DateFormatter = { |
79 | | - let f = DateFormatter() |
80 | | - f.dateStyle = .medium |
81 | | - return f |
82 | | - }() |
83 | | - |
84 | | - var body: some View { |
85 | | - NavigationStack { |
86 | | - Form { |
87 | | - Section("Select Loyahdev Certificate") { |
88 | | - Picker("Certificate", selection: $selectedRelease) { |
89 | | - if isLoadingReleases { |
90 | | - Text("-- Loading --").tag(nil as Release?) |
91 | | - } else { |
92 | | - Text("-- Select a certificate --").tag(nil as Release?) |
93 | | - ForEach(releases) { release in |
94 | | - Text(cleanName(release.name)).tag(release as Release?) |
95 | | - } |
96 | | - } |
97 | | - } |
98 | | - } |
99 | | - Section { |
100 | | - Text("Provided by loyahdev") |
101 | | - .font(.caption) |
102 | | - .foregroundColor(.secondary) |
103 | | - } |
104 | | - if let release = selectedRelease { |
105 | | - Section("Details") { |
106 | | - Text("Tag: \(release.tagName)") |
107 | | - if !statusMessage.isEmpty { |
108 | | - Text(statusMessage) |
109 | | - .foregroundColor(statusColor) |
110 | | - } |
111 | | - Text("Published: \(dateFormatter.string(from: isoDate(string: release.publishedAt)))") |
112 | | - if let exp = expiry { |
113 | | - expiryDisplay(for: exp) |
114 | | - } |
115 | | - } |
116 | | - } |
117 | | - Section { |
118 | | - Button("Add Certificate") { |
119 | | - addCertificate() |
120 | | - } |
121 | | - .disabled(p12Data == nil || provData == nil || password == nil || isChecking) |
122 | | - } |
123 | | - } |
124 | | - .navigationTitle("Loyahdev Certificates") |
125 | | - .navigationBarTitleDisplayMode(.inline) |
126 | | - .navigationBarItems(leading: |
127 | | - Button("×") { |
128 | | - dismiss() |
129 | | - } |
130 | | - ) |
131 | | - .onAppear { |
132 | | - fetchReleases() |
133 | | - } |
134 | | - .onChange(of: selectedRelease) { newValue in |
135 | | - if newValue != nil && !isChecking { |
136 | | - clearCertificateData() |
137 | | - checkCertificate() |
138 | | - } else if newValue == nil { |
139 | | - clearCertificateData() |
140 | | - } |
141 | | - } |
142 | | - } |
143 | | - } |
144 | | - |
145 | | - private func clearCertificateData() { |
146 | | - statusMessage = "" |
147 | | - expiry = nil |
148 | | - p12Data = nil |
149 | | - provData = nil |
150 | | - password = nil |
151 | | - displayName = "" |
152 | | - } |
153 | | - |
154 | | - private func expiryDisplay(for expiry: Date) -> some View { |
155 | | - let now = Date() |
156 | | - let components = Calendar.current.dateComponents([.day], from: now, to: expiry) |
157 | | - let days = components.day ?? 0 |
158 | | - let displayDate = expiry.formattedWithOrdinal() |
159 | | - let expiryText: String |
160 | | - let expiryColor: Color |
161 | | - if days > 0 { |
162 | | - expiryText = "Expires on the \(displayDate)" |
163 | | - expiryColor = .green |
164 | | - } else { |
165 | | - expiryText = "Expired on the \(displayDate)" |
166 | | - expiryColor = .red |
167 | | - } |
168 | | - return Text(expiryText) |
169 | | - .foregroundColor(expiryColor) |
170 | | - .font(.caption) |
171 | | - } |
172 | | - |
173 | | - private func isoDate(string: String) -> Date { |
174 | | - let formatter = ISO8601DateFormatter() |
175 | | - return formatter.date(from: string) ?? Date() |
176 | | - } |
177 | | - |
178 | | - private func cleanName(_ name: String) -> String { |
179 | | - name.replacingOccurrences(of: "\\\\", with: "").replacingOccurrences(of: "\\", with: "") |
180 | | - } |
181 | | - |
182 | | - private func getPAT() async -> String? { |
183 | | - guard let url = URL(string: "https://certapi.loyah.dev/pac") else { return nil } |
184 | | - do { |
185 | | - let (data, _) = try await URLSession.shared.data(from: url) |
186 | | - return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) |
187 | | - } catch { |
188 | | - return nil |
189 | | - } |
190 | | - } |
191 | | - |
192 | | - private func fetchReleases() { |
193 | | - Task { |
194 | | - let pat = await getPAT() |
195 | | - let url = URL(string: "https://api.github.com/repos/loyahdev/certificates/releases")! |
196 | | - var request = URLRequest(url: url) |
197 | | - if let pat = pat { |
198 | | - request.setValue("token \(pat)", forHTTPHeaderField: "Authorization") |
199 | | - } |
200 | | - do { |
201 | | - let (data, response) = try await URLSession.shared.data(for: request) |
202 | | - var decodeData = data |
203 | | - if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200, pat != nil { |
204 | | - let fallbackRequest = URLRequest(url: url) |
205 | | - let (fallbackData, _) = try await URLSession.shared.data(for: fallbackRequest) |
206 | | - decodeData = fallbackData |
207 | | - } |
208 | | - let decoder = JSONDecoder() |
209 | | - decoder.dateDecodingStrategy = .deferredToDate |
210 | | - let decoded = try decoder.decode([Release].self, from: decodeData) |
211 | | - await MainActor.run { |
212 | | - self.releases = decoded.sorted { isoDate(string: $0.publishedAt) > isoDate(string: $1.publishedAt) } |
213 | | - self.isLoadingReleases = false |
214 | | - } |
215 | | - } catch { |
216 | | - await MainActor.run { |
217 | | - self.statusMessage = "Failed to fetch releases: \(error.localizedDescription)" |
218 | | - self.isLoadingReleases = false |
219 | | - } |
220 | | - } |
221 | | - } |
222 | | - } |
223 | | - |
224 | | - private func findCertificateFiles(in directory: URL) throws -> (p12Urls: [URL], provUrls: [URL]) { |
225 | | - var p12Urls: [URL] = [] |
226 | | - var provUrls: [URL] = [] |
227 | | - if let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) { |
228 | | - for case let fileURL as URL in enumerator { |
229 | | - let path = fileURL.path |
230 | | - if !path.contains("__MACOSX") { |
231 | | - if path.hasSuffix(".p12") { |
232 | | - p12Urls.append(fileURL) |
233 | | - } else if path.hasSuffix(".mobileprovision") { |
234 | | - provUrls.append(fileURL) |
235 | | - } |
236 | | - } |
237 | | - } |
238 | | - } |
239 | | - return (p12Urls, provUrls) |
240 | | - } |
241 | | - |
242 | | - private func checkCertificate() { |
243 | | - guard let release = selectedRelease, |
244 | | - let asset = release.assets.first(where: { $0.name.hasSuffix(".zip") }), |
245 | | - let downloadUrl = URL(string: asset.browserDownloadUrl) else { |
246 | | - statusMessage = "Invalid release" |
247 | | - return |
248 | | - } |
249 | | - isChecking = true |
250 | | - statusMessage = "Downloading..." |
251 | | - Task { |
252 | | - let pat = await getPAT() |
253 | | - var downloadRequest = URLRequest(url: downloadUrl) |
254 | | - if let pat = pat { |
255 | | - downloadRequest.setValue("token \(pat)", forHTTPHeaderField: "Authorization") |
256 | | - } |
257 | | - do { |
258 | | - var tempData = Data() |
259 | | - var response = URLResponse() |
260 | | - (tempData, response) = try await URLSession.shared.data(for: downloadRequest) |
261 | | - if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200, pat != nil { |
262 | | - let fallbackRequest = URLRequest(url: downloadUrl) |
263 | | - (tempData, _) = try await URLSession.shared.data(for: fallbackRequest) |
264 | | - } |
265 | | - let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) |
266 | | - try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil) |
267 | | - defer { |
268 | | - try? FileManager.default.removeItem(at: tempDir) |
269 | | - } |
270 | | - let zipPath = tempDir.appendingPathComponent("temp.zip") |
271 | | - try tempData.write(to: zipPath) |
272 | | - let extractDir = tempDir.appendingPathComponent("extracted") |
273 | | - try FileManager.default.unzipItem(at: zipPath, to: extractDir, progress: nil) |
274 | | - // Find files |
275 | | - let (p12Urls, provUrls) = try findCertificateFiles(in: extractDir) |
276 | | - guard p12Urls.count == 1, provUrls.count == 1 else { |
277 | | - throw NSError(domain: "Extraction", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unable to extract certificate"]) |
278 | | - } |
279 | | - let p12Url = p12Urls[0] |
280 | | - let provUrl = provUrls[0] |
281 | | - let p12DataLocal = try Data(contentsOf: p12Url) |
282 | | - let provDataLocal = try Data(contentsOf: provUrl) |
283 | | - var successPw: String? |
284 | | - for pwCandidate in ["Hydrogen", "Sideloadingdotorg", "nocturnacerts"] { |
285 | | - switch CertificatesManager.check(p12Data: p12DataLocal, password: pwCandidate, mobileProvisionData: provDataLocal) { |
286 | | - case .success(.success): |
287 | | - successPw = pwCandidate |
288 | | - break |
289 | | - default: |
290 | | - break |
291 | | - } |
292 | | - } |
293 | | - guard let pw = successPw else { |
294 | | - throw NSError(domain: "Password", code: 1, userInfo: [NSLocalizedDescriptionKey: "Password check failed"]) |
295 | | - } |
296 | | - let exp = signer.getExpirationDate(provData: provDataLocal) |
297 | | - let dispName = CertificatesManager.getCertificateName(mobileProvisionData: provDataLocal) ?? cleanName(release.name) |
298 | | - await MainActor.run { |
299 | | - self.p12Data = p12DataLocal |
300 | | - self.provData = provDataLocal |
301 | | - self.password = pw |
302 | | - self.displayName = dispName |
303 | | - self.expiry = exp |
304 | | - self.statusMessage = "Success: Ready to add \(dispName)" |
305 | | - self.isChecking = false |
306 | | - } |
307 | | - } catch { |
308 | | - await MainActor.run { |
309 | | - self.statusMessage = "Error: \(error.localizedDescription)" |
310 | | - self.isChecking = false |
311 | | - } |
312 | | - } |
313 | | - } |
314 | | - } |
315 | | - |
316 | | - private func addCertificate() { |
317 | | - guard let p12DataLocal = p12Data, |
318 | | - let provDataLocal = provData, |
319 | | - let pw = password, |
320 | | - !displayName.isEmpty else { return } |
321 | | - isChecking = true |
322 | | - statusMessage = "Adding..." |
323 | | - Task { |
324 | | - do { |
325 | | - _ = try CertificateFileManager.shared.saveCertificate(p12Data: p12DataLocal, provData: provDataLocal, password: pw, displayName: displayName) |
326 | | - await MainActor.run { |
327 | | - self.statusMessage = "Added successfully" |
328 | | - self.isChecking = false |
329 | | - self.dismiss() |
330 | | - } |
331 | | - } catch { |
332 | | - await MainActor.run { |
333 | | - self.statusMessage = "Error adding: \(error.localizedDescription)" |
334 | | - self.isChecking = false |
335 | | - } |
336 | | - } |
337 | | - } |
338 | | - } |
339 | | -} |
340 | | - |
341 | 50 | // MARK: - Official Certificates View |
342 | 51 | struct OfficialCertificatesView: View { |
343 | 52 | @Environment(\.dismiss) private var dismiss |
|
0 commit comments