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,72 +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
285+ }
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+ }
301+ }
302+
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+ )
312+
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+ }
325+ }
326+ } catch {
327+ await MainActor . run {
328+ certStatuses [ certCopy. folderName] = " Check Error "
267329 }
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- let parsed = try await CertChecker . checkCert (
276- mobileProvision: mp,
277- mobileProvisionFilename: " profile.mobileprovision " ,
278- p12: p12,
279- p12Filename: " certificate.p12 " ,
280- password: pw
281- )
282-
283- let status = ( parsed [ " certificate " ] as? [ String : String ] ) ? [ " status " ] ??
284- ( parsed [ " certificate_matching_status " ] as? String ) ??
285- " Unknown "
286-
287- await MainActor . run {
288- certStatuses [ certCopy. folderName] = status
289330 }
290- } catch {
291- await MainActor . run {
292- certStatuses [ certCopy. folderName] = " Check Error "
293- }
294- }
295- }
331+ }
332+ }
296333 }
297- }
334+
335+ // MARK: - Helpers (moved out of reloadCertificatesAndEnsureSelection)
298336 private func loadExpiries( ) {
299337 for cert in customCertificates {
300338 let folderName = cert. folderName
301339 let certDir = CertificateFileManager . shared. certificatesDirectory. appendingPathComponent ( folderName)
302340 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.
303344 let expiry = signer. getExpirationDate ( provURL: provURL)
304345 certExpiries [ folderName] = expiry
305346 }
306347 }
348+
307349 private func ensureSelection( ) {
308350 if selectedCert == nil || !customCertificates. contains ( where: { $0. folderName == selectedCert } ) {
309351 if let firstCert = customCertificates. first {
@@ -312,10 +354,11 @@ private func reloadCertificatesAndEnsureSelection() {
312354 }
313355 }
314356 }
357+
315358 private func deleteCertificate( _ cert: CustomCertificate ) {
316359 try ? CertificateFileManager . shared. deleteCertificate ( folderName: cert. folderName)
317360 customCertificates = CertificateFileManager . shared. loadCertificates ( )
318-
361+
319362 if selectedCert == cert. folderName {
320363 if let newSelection = customCertificates. first {
321364 selectedCert = newSelection. folderName
@@ -329,6 +372,7 @@ private func reloadCertificatesAndEnsureSelection() {
329372 loadExpiries ( )
330373 }
331374}
375+
332376// MARK: - Add / Edit View
333377struct AddCertificateView : View {
334378 @Environment ( \. dismiss) private var dismiss
@@ -342,10 +386,12 @@ struct AddCertificateView: View {
342386 @State private var errorMessage = " "
343387 @State private var displayName : String = " "
344388 @State private var hasLoadedForEdit = false
389+
345390 init ( editingCertificate: CustomCertificate ? = nil , onSave: ( ( String ) -> Void ) ? = nil ) {
346391 self . editingCertificate = editingCertificate
347392 self . onSave = onSave
348393 }
394+
349395 var body : some View {
350396 NavigationStack {
351397 Form {
@@ -364,7 +410,7 @@ struct AddCertificateView: View {
364410 }
365411 }
366412 . disabled ( isChecking)
367-
413+
368414 Button ( action: { activeSheet = . prov } ) {
369415 HStack {
370416 Image ( systemName: " gearshape.fill " )
@@ -380,20 +426,20 @@ struct AddCertificateView: View {
380426 }
381427 . disabled ( isChecking)
382428 }
383-
429+
384430 Section ( header: Text ( " Display Name " ) ) {
385431 TextField ( " Optional Display Name " , text: $displayName)
386432 . disabled ( isChecking)
387433 }
388-
434+
389435 Section ( header: Text ( " Password " ) ) {
390436 SecureField ( " Enter Password " , text: $password)
391437 . disabled ( isChecking)
392438 Text ( " Enter the password for the certificate. Leave it blank if there is no password needed. " )
393439 . font ( . caption)
394440 . foregroundColor ( . secondary)
395441 }
396-
442+
397443 if !errorMessage. isEmpty {
398444 Text ( errorMessage)
399445 . foregroundColor ( . red)
@@ -440,29 +486,31 @@ struct AddCertificateView: View {
440486 }
441487 }
442488 }
489+
443490 private func loadForEdit( cert: CustomCertificate ) {
444491 let certFolder = CertificateFileManager . shared. certificatesDirectory. appendingPathComponent ( cert. folderName)
445492 let p12URL = certFolder. appendingPathComponent ( " certificate.p12 " )
446493 let provURL = certFolder. appendingPathComponent ( " profile.mobileprovision " )
447494 let passwordURL = certFolder. appendingPathComponent ( " password.txt " )
448495 let nameURL = certFolder. appendingPathComponent ( " name.txt " )
449-
496+
450497 p12File = CertificateFileItem ( name: " certificate.p12 " , url: p12URL)
451498 provFile = CertificateFileItem ( name: " profile.mobileprovision " , url: provURL)
452-
499+
453500 if let pwData = try ? Data ( contentsOf: passwordURL) , let pw = String ( data: pwData, encoding: . utf8) {
454501 password = pw
455502 }
456503 if let nameData = try ? Data ( contentsOf: nameURL) , let nameStr = String ( data: nameData, encoding: . utf8) {
457504 displayName = nameStr
458505 }
459506 }
507+
460508 private func saveCertificate( ) {
461509 guard let p12URL = p12File? . url, let provURL = provFile? . url else { return }
462-
510+
463511 isChecking = true
464512 errorMessage = " "
465-
513+
466514 let workItem : DispatchWorkItem = DispatchWorkItem {
467515 do {
468516 var p12Data : Data
@@ -481,10 +529,10 @@ struct AddCertificateView: View {
481529 p12Data = try Data ( contentsOf: p12URL)
482530 provData = try Data ( contentsOf: provURL)
483531 }
484-
532+
485533 let checkResult = CertificatesManager . check ( p12Data: p12Data, password: self . password, mobileProvisionData: provData)
486534 var dispatchError : String ?
487-
535+
488536 switch checkResult {
489537 case . success( . success) :
490538 if localDisplayName. isEmpty {
@@ -503,7 +551,7 @@ struct AddCertificateView: View {
503551 case . failure( let error) :
504552 dispatchError = " Error: \( error. localizedDescription) "
505553 }
506-
554+
507555 DispatchQueue . main. async {
508556 self . isChecking = false
509557 if let err = dispatchError {
@@ -522,4 +570,3 @@ struct AddCertificateView: View {
522570 DispatchQueue . global ( qos: . userInitiated) . async ( execute: workItem)
523571 }
524572}
525-
0 commit comments