11import SwiftUI
22import UniformTypeIdentifiers
3+
34// Centralized types to avoid conflicts
45struct CertificateFileItem {
56 var name : String = " "
@@ -10,6 +11,7 @@ struct CustomCertificate: Identifiable {
1011 let displayName : String
1112 let folderName : String
1213}
14+
1315// MARK: - Date Extension for Formatting
1416extension Date {
1517 func formattedWithOrdinal( ) -> String {
@@ -39,8 +41,10 @@ extension Date {
3941 return " \( number) \( suffix) "
4042 }
4143}
44+
4245// MARK: - CertificateView (List + Add/Edit launchers)
4346struct CertificateView : View {
47+ // MARK: - State
4448 @State private var customCertificates : [ CustomCertificate ] = [ ]
4549 @State private var certExpiries : [ String : Date ? ] = [ : ]
4650 @State private var certStatuses : [ String : String ] = [ : ]
@@ -51,6 +55,8 @@ struct CertificateView: View {
5155 @State private var showingDeleteAlert = false
5256 @State private var certToDelete : CustomCertificate ?
5357 @State private var newlyAddedFolder : String ? = nil
58+
59+ // MARK: - Body
5460 var body : some View {
5561 ScrollView {
5662 LazyVGrid ( columns: [ GridItem ( . flexible( ) ) , GridItem ( . flexible( ) ) ] , spacing: 20 ) {
@@ -117,6 +123,8 @@ struct CertificateView: View {
117123 reloadCertificatesAndEnsureSelection ( )
118124 }
119125 }
126+
127+ // MARK: - UI helpers
120128 private func certificateItem( for cert: CustomCertificate ) -> some View {
121129 ZStack ( alignment: . top) {
122130 certificateContent ( for: cert)
@@ -135,6 +143,7 @@ struct CertificateView: View {
135143 certificateButtons ( for: cert)
136144 }
137145 }
146+
138147 private func certificateContent( for cert: CustomCertificate ) -> some View {
139148 VStack ( alignment: . center, spacing: 12 ) {
140149 Text ( cert. displayName)
@@ -167,6 +176,7 @@ struct CertificateView: View {
167176 . stroke ( selectedCert == cert. folderName ? Color . blue : Color . clear, lineWidth: 3 )
168177 )
169178 }
179+
170180 private func expiryDisplay( for expiry: Date ) -> some View {
171181 let now = Date ( )
172182 let components = Calendar . current. dateComponents ( [ . day] , from: now, to: expiry)
@@ -184,6 +194,7 @@ struct CertificateView: View {
184194 . fontWeight ( . medium)
185195 . foregroundColor ( . primary)
186196 }
197+
187198 private func certificateBackground( for cert: CustomCertificate ) -> Color {
188199 let status = certStatuses [ cert. folderName] ?? " Unknown "
189200 if status == " Revoked " {
@@ -204,6 +215,7 @@ struct CertificateView: View {
204215 return . green. opacity ( 0.15 )
205216 }
206217 }
218+
207219 private func certificateButtons( for cert: CustomCertificate ) -> some View {
208220 HStack {
209221 Button ( action: {
@@ -217,9 +229,9 @@ struct CertificateView: View {
217229 . background ( Color ( . systemGray6) . opacity ( 0.8 ) )
218230 . clipShape ( Circle ( ) )
219231 }
220-
232+
221233 Spacer ( )
222-
234+
223235 Button ( action: {
224236 if customCertificates. count > 1 {
225237 certToDelete = cert
@@ -238,86 +250,102 @@ struct CertificateView: View {
238250 . padding ( . top, 12 )
239251 . padding ( . horizontal, 12 )
240252 }
241- private func reloadCertificatesAndEnsureSelection( ) {
242- customCertificates = CertificateFileManager . shared. loadCertificates ( )
243-
244- selectedCert = UserDefaults . standard. string ( forKey: " selectedCertificateFolder " )
245- ensureSelection ( )
246- loadExpiries ( )
247-
248- for cert in customCertificates {
249- let certCopy = cert
250- Task < Void , Never > {
251- do {
252- let dir = CertificateFileManager . shared. certificatesDirectory. appendingPathComponent ( certCopy. folderName)
253-
254- // Check if files exist
255- let p12URL = dir. appendingPathComponent ( " certificate.p12 " )
256- let mpURL = dir. appendingPathComponent ( " profile.mobileprovision " )
257- let pwURL = dir. appendingPathComponent ( " password.txt " )
258-
259- let fileManager = FileManager . default
260- let p12Exists = fileManager. fileExists ( atPath: p12URL. path)
261- let mpExists = fileManager. fileExists ( atPath: mpURL. path)
262- let pwExists = fileManager. fileExists ( atPath: pwURL. path)
263-
264- if !p12Exists || !mpExists || !pwExists {
265- await MainActor . run {
266- certStatuses [ certCopy. folderName] = " Missing Files "
253+
254+ // MARK: - Certificate loading & status
255+ private func reloadCertificatesAndEnsureSelection( ) {
256+ customCertificates = CertificateFileManager . shared. loadCertificates ( )
257+
258+ selectedCert = UserDefaults . standard. string ( forKey: " selectedCertificateFolder " )
259+ ensureSelection ( )
260+ loadExpiries ( )
261+
262+ for cert in customCertificates {
263+ let certCopy = cert
264+
265+ Task {
266+ do {
267+ let dir = CertificateFileManager . shared. certificatesDirectory
268+ . appendingPathComponent ( certCopy. folderName)
269+
270+ // Check if files exist
271+ let p12URL = dir. appendingPathComponent ( " certificate.p12 " )
272+ let mpURL = dir. appendingPathComponent ( " profile.mobileprovision " )
273+ let pwURL = dir. appendingPathComponent ( " password.txt " )
274+
275+ let fileManager = FileManager . default
276+ let p12Exists = fileManager. fileExists ( atPath: p12URL. path)
277+ let mpExists = fileManager. fileExists ( atPath: mpURL. path)
278+ let pwExists = fileManager. fileExists ( atPath: pwURL. path)
279+
280+ if !p12Exists || !mpExists || !pwExists {
281+ await MainActor . run {
282+ certStatuses [ certCopy. folderName] = " Missing Files "
283+ }
284+ return
267285 }
268- return
269- }
270-
271- let p12 = try Data ( contentsOf: p12URL )
272- let mp = try Data ( contentsOf: mpURL )
273- let pw = ( try ? String ( contentsOf : pwURL , encoding : . utf8 ) ) ? . trimmingCharacters ( in : . whitespacesAndNewlines ) ?? " "
274-
275- // 1) show cached immediately (if available)
276- if let cached = CertChecker . cachedResult ( p12Data : p12 , mpData : mp , password : pw ) {
277- let status = ( cached [ " certificate " ] as? [ String : String ] ) ? [ " status " ]
278- ?? ( cached [ " certificate_matching_status " ] as? String )
279- ?? ( cached [ " overall_status " ] as? String )
280- ?? " Unknown "
281- await MainActor . run {
282- certStatuses [ certCopy . folderName ] = status + " (cached) "
286+
287+ // Read files (safe inside Task)
288+ let p12 = try Data ( contentsOf : p12URL )
289+ let mp = try Data ( contentsOf: mpURL )
290+ let pw = ( try ? String ( contentsOf: pwURL , encoding : . utf8 ) ) ? . trimmingCharacters ( in : . whitespacesAndNewlines ) ?? " "
291+
292+ // 1) show cached immediately (if available)
293+ if let cached = CertChecker . cachedResult ( p12Data : p12 , mpData : mp , password : pw ) {
294+ let status = ( cached [ " certificate " ] as? [ String : String ] ) ? [ " status " ]
295+ ?? ( cached [ " certificate_matching_status " ] as? String )
296+ ?? ( cached [ " overall_status " ] as? String )
297+ ?? " Unknown "
298+ await MainActor . run {
299+ certStatuses [ certCopy . folderName ] = status + " (cached) "
300+ }
283301 }
284- }
285302
286- // 2) then fetch fresh and update (awaits network)
287- do {
288- let parsed = try await CertChecker . checkCert (
289- mobileProvision: mp,
290- mobileProvisionFilename: " profile.mobileprovision " ,
291- p12: p12,
292- p12Filename: " certificate.p12 " ,
293- password: pw
294- )
295-
296- let status = ( parsed [ " certificate " ] as? [ String : String ] ) ? [ " status " ]
297- ?? ( parsed [ " certificate_matching_status " ] as? String )
298- ?? ( parsed [ " overall_status " ] as? String )
299- ?? " Unknown "
303+ // 2) then fetch fresh and update (awaits network)
304+ do {
305+ let parsed = try await CertChecker . checkCert (
306+ mobileProvision: mp,
307+ mobileProvisionFilename: " profile.mobileprovision " ,
308+ p12: p12,
309+ p12Filename: " certificate.p12 " ,
310+ password: pw
311+ )
300312
301- await MainActor . run {
302- certStatuses [ certCopy. folderName] = status
313+ let status = ( parsed [ " certificate " ] as? [ String : String ] ) ? [ " status " ]
314+ ?? ( parsed [ " certificate_matching_status " ] as? String )
315+ ?? ( parsed [ " overall_status " ] as? String )
316+ ?? " Unknown "
317+
318+ await MainActor . run {
319+ certStatuses [ certCopy. folderName] = status
320+ }
321+ } catch {
322+ await MainActor . run {
323+ certStatuses [ certCopy. folderName] = " Check Error "
324+ }
303325 }
304326 } catch {
305327 await MainActor . run {
306328 certStatuses [ certCopy. folderName] = " Check Error "
307329 }
308330 }
309- }
331+ }
310332 }
311333 }
334+
335+ // MARK: - Helpers (moved out of reloadCertificatesAndEnsureSelection)
312336 private func loadExpiries( ) {
313337 for cert in customCertificates {
314338 let folderName = cert. folderName
315339 let certDir = CertificateFileManager . shared. certificatesDirectory. appendingPathComponent ( folderName)
316340 let provURL = certDir. appendingPathComponent ( " profile.mobileprovision " )
341+
342+ // signer.getExpirationDate(provURL:) is presumed synchronous in your project.
343+ // If it's async change this to Task + await accordingly.
317344 let expiry = signer. getExpirationDate ( provURL: provURL)
318345 certExpiries [ folderName] = expiry
319346 }
320347 }
348+
321349 private func ensureSelection( ) {
322350 if selectedCert == nil || !customCertificates. contains ( where: { $0. folderName == selectedCert } ) {
323351 if let firstCert = customCertificates. first {
@@ -326,10 +354,11 @@ private func reloadCertificatesAndEnsureSelection() {
326354 }
327355 }
328356 }
357+
329358 private func deleteCertificate( _ cert: CustomCertificate ) {
330359 try ? CertificateFileManager . shared. deleteCertificate ( folderName: cert. folderName)
331360 customCertificates = CertificateFileManager . shared. loadCertificates ( )
332-
361+
333362 if selectedCert == cert. folderName {
334363 if let newSelection = customCertificates. first {
335364 selectedCert = newSelection. folderName
@@ -343,6 +372,7 @@ private func reloadCertificatesAndEnsureSelection() {
343372 loadExpiries ( )
344373 }
345374}
375+
346376// MARK: - Add / Edit View
347377struct AddCertificateView : View {
348378 @Environment ( \. dismiss) private var dismiss
@@ -356,10 +386,12 @@ struct AddCertificateView: View {
356386 @State private var errorMessage = " "
357387 @State private var displayName : String = " "
358388 @State private var hasLoadedForEdit = false
389+
359390 init ( editingCertificate: CustomCertificate ? = nil , onSave: ( ( String ) -> Void ) ? = nil ) {
360391 self . editingCertificate = editingCertificate
361392 self . onSave = onSave
362393 }
394+
363395 var body : some View {
364396 NavigationStack {
365397 Form {
@@ -378,7 +410,7 @@ struct AddCertificateView: View {
378410 }
379411 }
380412 . disabled ( isChecking)
381-
413+
382414 Button ( action: { activeSheet = . prov } ) {
383415 HStack {
384416 Image ( systemName: " gearshape.fill " )
@@ -394,20 +426,20 @@ struct AddCertificateView: View {
394426 }
395427 . disabled ( isChecking)
396428 }
397-
429+
398430 Section ( header: Text ( " Display Name " ) ) {
399431 TextField ( " Optional Display Name " , text: $displayName)
400432 . disabled ( isChecking)
401433 }
402-
434+
403435 Section ( header: Text ( " Password " ) ) {
404436 SecureField ( " Enter Password " , text: $password)
405437 . disabled ( isChecking)
406438 Text ( " Enter the password for the certificate. Leave it blank if there is no password needed. " )
407439 . font ( . caption)
408440 . foregroundColor ( . secondary)
409441 }
410-
442+
411443 if !errorMessage. isEmpty {
412444 Text ( errorMessage)
413445 . foregroundColor ( . red)
@@ -454,29 +486,31 @@ struct AddCertificateView: View {
454486 }
455487 }
456488 }
489+
457490 private func loadForEdit( cert: CustomCertificate ) {
458491 let certFolder = CertificateFileManager . shared. certificatesDirectory. appendingPathComponent ( cert. folderName)
459492 let p12URL = certFolder. appendingPathComponent ( " certificate.p12 " )
460493 let provURL = certFolder. appendingPathComponent ( " profile.mobileprovision " )
461494 let passwordURL = certFolder. appendingPathComponent ( " password.txt " )
462495 let nameURL = certFolder. appendingPathComponent ( " name.txt " )
463-
496+
464497 p12File = CertificateFileItem ( name: " certificate.p12 " , url: p12URL)
465498 provFile = CertificateFileItem ( name: " profile.mobileprovision " , url: provURL)
466-
499+
467500 if let pwData = try ? Data ( contentsOf: passwordURL) , let pw = String ( data: pwData, encoding: . utf8) {
468501 password = pw
469502 }
470503 if let nameData = try ? Data ( contentsOf: nameURL) , let nameStr = String ( data: nameData, encoding: . utf8) {
471504 displayName = nameStr
472505 }
473506 }
507+
474508 private func saveCertificate( ) {
475509 guard let p12URL = p12File? . url, let provURL = provFile? . url else { return }
476-
510+
477511 isChecking = true
478512 errorMessage = " "
479-
513+
480514 let workItem : DispatchWorkItem = DispatchWorkItem {
481515 do {
482516 var p12Data : Data
@@ -495,10 +529,10 @@ struct AddCertificateView: View {
495529 p12Data = try Data ( contentsOf: p12URL)
496530 provData = try Data ( contentsOf: provURL)
497531 }
498-
532+
499533 let checkResult = CertificatesManager . check ( p12Data: p12Data, password: self . password, mobileProvisionData: provData)
500534 var dispatchError : String ?
501-
535+
502536 switch checkResult {
503537 case . success( . success) :
504538 if localDisplayName. isEmpty {
@@ -517,7 +551,7 @@ struct AddCertificateView: View {
517551 case . failure( let error) :
518552 dispatchError = " Error: \( error. localizedDescription) "
519553 }
520-
554+
521555 DispatchQueue . main. async {
522556 self . isChecking = false
523557 if let err = dispatchError {
@@ -536,6 +570,3 @@ struct AddCertificateView: View {
536570 DispatchQueue . global ( qos: . userInitiated) . async ( execute: workItem)
537571 }
538572}
539-
540-
541-
0 commit comments