@@ -32,28 +32,25 @@ class CertificateFileManager {
3232 }
3333
3434 func loadCertificates( ) -> [ CustomCertificate ] {
35- guard let subdirectories = try ? fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: [ . isDirectoryKey] , options: [ . skipsHiddenFiles] ) else {
35+ var resultCerts : [ CustomCertificate ] = [ ]
36+ guard let folders = try ? fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] ) else {
3637 return [ ]
3738 }
3839
39- var certificates : [ CustomCertificate ] = [ ]
40- for folder in subdirectories {
41- var isDir : ObjCBool = false
42- if fileManager. fileExists ( atPath: folder. path, isDirectory: & isDir) , !isDir. boolValue { continue }
43-
40+ for folder in folders {
4441 let nameURL = folder. appendingPathComponent ( " name.txt " )
4542 if fileManager. fileExists ( atPath: nameURL. path) {
46- do {
47- let nameData = try Data ( contentsOf: nameURL)
48- if let displayName = String ( data: nameData, encoding: . utf8) {
49- certificates. append ( CustomCertificate ( displayName: displayName, folderName: folder. lastPathComponent) )
50- }
51- } catch {
52- print ( " Error loading name: \( error) " )
43+ if let nameData = try ? Data ( contentsOf: nameURL) ,
44+ let nameString = String ( data: nameData, encoding: . utf8) {
45+ resultCerts. append ( CustomCertificate ( displayName: nameString, folderName: folder. lastPathComponent) )
5346 }
47+ } else {
48+ // Fallback display name if missing
49+ resultCerts. append ( CustomCertificate ( displayName: folder. lastPathComponent, folderName: folder. lastPathComponent) )
5450 }
5551 }
56- return certificates. sorted { $0. displayName < $1. displayName }
52+
53+ return resultCerts
5754 }
5855
5956 func saveCertificate( p12Data: Data , provData: Data , password: String , displayName: String ) throws -> String {
@@ -68,7 +65,6 @@ class CertificateFileManager {
6865 let p12URL = folder. appendingPathComponent ( " certificate.p12 " )
6966 let provURL = folder. appendingPathComponent ( " profile.mobileprovision " )
7067 let passwordURL = folder. appendingPathComponent ( " password.txt " )
71-
7268 if fileManager. fileExists ( atPath: p12URL. path) && fileManager. fileExists ( atPath: provURL. path) && fileManager. fileExists ( atPath: passwordURL. path) {
7369 do {
7470 let existingP12Data = try Data ( contentsOf: p12URL)
@@ -90,44 +86,37 @@ class CertificateFileManager {
9086 }
9187 }
9288
93- // Find unique folder name
94- var candidate = baseName
95- var count = 1
96- while fileManager. fileExists ( atPath: certificatesDirectory. appendingPathComponent ( candidate) . path) {
97- candidate = " \( baseName) ( \( count) ) "
98- count += 1
89+ // Create folder
90+ var finalName = baseName
91+ var counter = 1
92+ var folderURL = certificatesDirectory. appendingPathComponent ( finalName)
93+ while fileManager. fileExists ( atPath: folderURL. path) {
94+ counter += 1
95+ finalName = " \( baseName) - \( counter) "
96+ folderURL = certificatesDirectory. appendingPathComponent ( finalName)
9997 }
98+ try fileManager. createDirectory ( at: folderURL, withIntermediateDirectories: true )
10099
101- let certificateFolder = certificatesDirectory. appendingPathComponent ( candidate)
102- try fileManager. createDirectory ( at: certificateFolder, withIntermediateDirectories: true )
103-
104- try p12Data. write ( to: certificateFolder. appendingPathComponent ( " certificate.p12 " ) )
105- try provData. write ( to: certificateFolder. appendingPathComponent ( " profile.mobileprovision " ) )
106- try password. data ( using: . utf8) ? . write ( to: certificateFolder. appendingPathComponent ( " password.txt " ) )
107- try displayName. data ( using: . utf8) ? . write ( to: certificateFolder. appendingPathComponent ( " name.txt " ) )
100+ try p12Data. write ( to: folderURL. appendingPathComponent ( " certificate.p12 " ) )
101+ try provData. write ( to: folderURL. appendingPathComponent ( " profile.mobileprovision " ) )
102+ try password. data ( using: . utf8) ? . write ( to: folderURL. appendingPathComponent ( " password.txt " ) )
103+ try displayName. data ( using: . utf8) ? . write ( to: folderURL. appendingPathComponent ( " name.txt " ) )
108104
109- return candidate
105+ return finalName
110106 }
111107
112108 func updateCertificate( folderName: String , p12Data: Data , provData: Data , password: String , displayName: String ) throws {
113109 let certificateFolder = certificatesDirectory. appendingPathComponent ( folderName)
114- guard fileManager. fileExists ( atPath: certificateFolder. path) else {
115- throw NSError ( domain: " CertificateFileManager " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Certificate folder not found " ] )
116- }
117-
118110 let p12HashNew = CertificatesManager . sha256Hex ( p12Data)
119111 let provHashNew = CertificatesManager . sha256Hex ( provData)
120112 let passwordHashNew = CertificatesManager . sha256Hex ( password. data ( using: . utf8) ?? Data ( ) )
121113
122- // Check if new version identical to any other existing (exclude self)
114+ // Prevent accidental duplicate update matching another cert
123115 let existingFolders = try fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] )
124- for folder in existingFolders {
125- if folder == certificateFolder { continue }
126-
116+ for folder in existingFolders where folder. lastPathComponent != folderName {
127117 let p12URL = folder. appendingPathComponent ( " certificate.p12 " )
128118 let provURL = folder. appendingPathComponent ( " profile.mobileprovision " )
129119 let passwordURL = folder. appendingPathComponent ( " password.txt " )
130-
131120 if fileManager. fileExists ( atPath: p12URL. path) && fileManager. fileExists ( atPath: provURL. path) && fileManager. fileExists ( atPath: passwordURL. path) {
132121 do {
133122 let existingP12Data = try Data ( contentsOf: p12URL)
@@ -167,10 +156,11 @@ class CertificateFileManager {
167156 }
168157}
169158
159+ // MARK: - CertificateView (List + Add/Edit launchers)
170160struct CertificateView : View {
171161 @State private var customCertificates : [ CustomCertificate ] = [ ]
172162 @State private var showAddCertificateSheet = false
173- @State private var editingCertificate : CustomCertificate ? = nil
163+ @State private var editingCertificate : CustomCertificate ? = nil // Used only for edit sheet (.sheet(item:))
174164 @State private var selectedCert : String ? = nil
175165 @State private var showingDeleteAlert = false
176166 @State private var certToDelete : CustomCertificate ?
@@ -198,7 +188,6 @@ struct CertificateView: View {
198188 . onTapGesture {
199189 // Only allow deselection if there are other certificates available
200190 if selectedCert == cert. folderName && customCertificates. count > 1 {
201- // Find another certificate to select
202191 if let nextCert = customCertificates. first ( where: { $0. folderName != cert. folderName } ) {
203192 selectedCert = nextCert. folderName
204193 UserDefaults . standard. set ( selectedCert, forKey: " selectedCertificateFolder " )
@@ -211,8 +200,8 @@ struct CertificateView: View {
211200
212201 HStack {
213202 Button ( action: {
203+ // EDIT: trigger identifiable sheet
214204 editingCertificate = cert
215- showAddCertificateSheet = true
216205 } ) {
217206 Image ( systemName: " pencil " )
218207 . foregroundColor ( . blue)
@@ -225,7 +214,6 @@ struct CertificateView: View {
225214 Spacer ( )
226215
227216 Button ( action: {
228- // Prevent deletion if it's the only certificate
229217 if customCertificates. count > 1 {
230218 certToDelete = cert
231219 showingDeleteAlert = true
@@ -252,26 +240,25 @@ struct CertificateView: View {
252240 . toolbar {
253241 ToolbarItem ( placement: . navigationBarTrailing) {
254242 Button ( action: {
255- editingCertificate = nil
256243 showAddCertificateSheet = true
257244 } ) {
258245 Image ( systemName: " plus " )
259246 }
260247 }
261248 }
249+ // ADD sheet (new certificate only)
262250 . sheet ( isPresented: $showAddCertificateSheet, onDismiss: {
263- customCertificates = CertificateFileManager . shared. loadCertificates ( )
264- editingCertificate = nil
265- // Ensure at least one certificate is selected
266- ensureSelection ( )
251+ reloadCertificatesAndEnsureSelection ( )
267252 } ) {
268- if let editingCertificate = editingCertificate {
269- AddCertificateView ( editingCertificate: editingCertificate)
270- . presentationDetents ( [ . large] )
271- } else {
272- AddCertificateView ( )
273- . presentationDetents ( [ . large] )
274- }
253+ AddCertificateView ( )
254+ . presentationDetents ( [ . large] )
255+ }
256+ // EDIT sheet (identifiable)
257+ . sheet ( item: $editingCertificate, onDismiss: {
258+ reloadCertificatesAndEnsureSelection ( )
259+ } ) { editing in
260+ AddCertificateView ( editingCertificate: editing)
261+ . presentationDetents ( [ . large] )
275262 }
276263 . alert ( " Delete Certificate? " , isPresented: $showingDeleteAlert) {
277264 Button ( " Delete " , role: . destructive) {
@@ -284,17 +271,18 @@ struct CertificateView: View {
284271 Text ( " Are you sure? This can't be undone. " )
285272 }
286273 . onAppear {
287- customCertificates = CertificateFileManager . shared. loadCertificates ( )
288- selectedCert = UserDefaults . standard. string ( forKey: " selectedCertificateFolder " )
289-
290- // Ensure at least one certificate is selected
291- ensureSelection ( )
274+ reloadCertificatesAndEnsureSelection ( )
292275 }
293276 }
294277 }
295278
279+ private func reloadCertificatesAndEnsureSelection( ) {
280+ customCertificates = CertificateFileManager . shared. loadCertificates ( )
281+ selectedCert = UserDefaults . standard. string ( forKey: " selectedCertificateFolder " )
282+ ensureSelection ( )
283+ }
284+
296285 private func ensureSelection( ) {
297- // If no certificate is selected or the selected one doesn't exist, select the first one
298286 if selectedCert == nil || !customCertificates. contains ( where: { $0. folderName == selectedCert } ) {
299287 if let firstCert = customCertificates. first {
300288 selectedCert = firstCert. folderName
@@ -307,7 +295,6 @@ struct CertificateView: View {
307295 try ? CertificateFileManager . shared. deleteCertificate ( folderName: cert. folderName)
308296 customCertificates = CertificateFileManager . shared. loadCertificates ( )
309297
310- // If we're deleting the currently selected certificate, select another one
311298 if selectedCert == cert. folderName {
312299 if let newSelection = customCertificates. first {
313300 selectedCert = newSelection. folderName
@@ -317,12 +304,11 @@ struct CertificateView: View {
317304 UserDefaults . standard. removeObject ( forKey: " selectedCertificateFolder " )
318305 }
319306 }
320-
321- // Ensure selection is maintained
322307 ensureSelection ( )
323308 }
324309}
325310
311+ // MARK: - Add / Edit View
326312struct AddCertificateView : View {
327313 @Environment ( \. dismiss) private var dismiss
328314 let editingCertificate : CustomCertificate ?
@@ -333,6 +319,7 @@ struct AddCertificateView: View {
333319 @State private var activeSheet : CertificatePickerKind ?
334320 @State private var isChecking = false
335321 @State private var errorMessage = " "
322+ @State private var displayName : String = " "
336323 @State private var hasLoadedForEdit = false
337324
338325 init ( editingCertificate: CustomCertificate ? = nil ) {
@@ -374,6 +361,11 @@ struct AddCertificateView: View {
374361 . disabled ( isChecking)
375362 }
376363
364+ Section ( header: Text ( " Display Name " ) ) {
365+ TextField ( " Optional Display Name " , text: $displayName)
366+ . disabled ( isChecking)
367+ }
368+
377369 Section ( header: Text ( " Password " ) ) {
378370 SecureField ( " Enter Password " , text: $password)
379371 . disabled ( isChecking)
@@ -423,7 +415,6 @@ struct AddCertificateView: View {
423415 errorMessage = " "
424416 }
425417 . onAppear {
426- // Load data for editing only once and only if we haven't already loaded it
427418 if let cert = editingCertificate, !hasLoadedForEdit {
428419 loadForEdit ( cert: cert)
429420 hasLoadedForEdit = true
@@ -437,13 +428,17 @@ struct AddCertificateView: View {
437428 let p12URL = certFolder. appendingPathComponent ( " certificate.p12 " )
438429 let provURL = certFolder. appendingPathComponent ( " profile.mobileprovision " )
439430 let passwordURL = certFolder. appendingPathComponent ( " password.txt " )
431+ let nameURL = certFolder. appendingPathComponent ( " name.txt " )
440432
441433 p12File = CertificateFileItem ( name: " certificate.p12 " , url: p12URL)
442434 provFile = CertificateFileItem ( name: " profile.mobileprovision " , url: provURL)
443435
444436 if let pwData = try ? Data ( contentsOf: passwordURL) , let pw = String ( data: pwData, encoding: . utf8) {
445437 password = pw
446438 }
439+ if let nameData = try ? Data ( contentsOf: nameURL) , let nameStr = String ( data: nameData, encoding: . utf8) {
440+ displayName = nameStr
441+ }
447442 }
448443
449444 private func saveCertificate( ) {
@@ -457,32 +452,30 @@ struct AddCertificateView: View {
457452 var p12Data : Data
458453 var provData : Data
459454 if editingCertificate != nil {
460- // For edit, files are in app container, no security scope needed
461455 p12Data = try Data ( contentsOf: p12URL)
462456 provData = try Data ( contentsOf: provURL)
463457 } else {
464- // For new, access security-scoped
465- guard p12URL. startAccessingSecurityScopedResource ( ) else {
466- throw NSError ( domain: " AccessError " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Cannot access P12 file " ] )
458+ guard p12URL. startAccessingSecurityScopedResource ( ) ,
459+ provURL. startAccessingSecurityScopedResource ( ) else {
460+ DispatchQueue . main. async {
461+ isChecking = false
462+ errorMessage = " Security-scoped resource access failed. "
463+ }
464+ return
467465 }
468- defer { p12URL. stopAccessingSecurityScopedResource ( ) }
469-
470- guard provURL. startAccessingSecurityScopedResource ( ) else {
471- throw NSError ( domain: " AccessError " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " Cannot access Provision file " ] )
466+ defer {
467+ p12URL. stopAccessingSecurityScopedResource ( )
468+ provURL. stopAccessingSecurityScopedResource ( )
472469 }
473- defer { provURL. stopAccessingSecurityScopedResource ( ) }
474-
475470 p12Data = try Data ( contentsOf: p12URL)
476471 provData = try Data ( contentsOf: provURL)
477472 }
478473
479- let result = CertificatesManager . check ( p12Data: p12Data, password: password, mobileProvisionData: provData)
480-
474+ let checkResult = CustomCertificatesManager . check ( p12Data: p12Data, password: password, mobileProvisionData: provData)
481475 var dispatchError : String ?
482- switch result {
483- case . success( . success) :
484- let displayName = CertificatesManager . getCertificateName ( p12Data: p12Data, password: password) ?? " Custom Certificate "
485-
476+
477+ switch checkResult {
478+ case . success( . success( _, _) ) :
486479 if let folder = editingCertificate? . folderName {
487480 try CertificateFileManager . shared. updateCertificate ( folderName: folder, p12Data: p12Data, provData: provData, password: password, displayName: displayName)
488481 } else {
0 commit comments