Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 9e09995

Browse files
authored
Make certificates view better and add edit and delete buttons
1 parent 9579a73 commit 9e09995

2 files changed

Lines changed: 182 additions & 43 deletions

File tree

Sources/prostore/views/CertificateView.swift

Lines changed: 180 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,53 @@ class CertificateFileManager {
109109
return candidate
110110
}
111111

112+
func updateCertificate(folderName: String, p12Data: Data, provData: Data, password: String, displayName: String) throws {
113+
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+
118+
let p12HashNew = CertificatesManager.sha256Hex(p12Data)
119+
let provHashNew = CertificatesManager.sha256Hex(provData)
120+
let passwordHashNew = CertificatesManager.sha256Hex(password.data(using: .utf8) ?? Data())
121+
122+
// Check if new version identical to any other existing (exclude self)
123+
let existingFolders = try fileManager.contentsOfDirectory(at: certificatesDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
124+
for folder in existingFolders {
125+
if folder == certificateFolder { continue }
126+
127+
let p12URL = folder.appendingPathComponent("certificate.p12")
128+
let provURL = folder.appendingPathComponent("profile.mobileprovision")
129+
let passwordURL = folder.appendingPathComponent("password.txt")
130+
131+
if fileManager.fileExists(atPath: p12URL.path) && fileManager.fileExists(atPath: provURL.path) && fileManager.fileExists(atPath: passwordURL.path) {
132+
do {
133+
let existingP12Data = try Data(contentsOf: p12URL)
134+
let existingProvData = try Data(contentsOf: provURL)
135+
let existingPasswordData = try Data(contentsOf: passwordURL)
136+
let existingPassword = String(data: existingPasswordData, encoding: .utf8) ?? ""
137+
138+
let p12HashExisting = CertificatesManager.sha256Hex(existingP12Data)
139+
let provHashExisting = CertificatesManager.sha256Hex(existingProvData)
140+
let passwordHashExisting = CertificatesManager.sha256Hex(existingPassword.data(using: .utf8) ?? Data())
141+
142+
if p12HashNew == p12HashExisting && provHashNew == provHashExisting && passwordHashNew == passwordHashExisting {
143+
throw NSError(domain: "CertificateFileManager", code: 2, userInfo: [NSLocalizedDescriptionKey: "This updated certificate matches another existing one"])
144+
}
145+
} catch {
146+
// Skip if can't read existing
147+
continue
148+
}
149+
}
150+
}
151+
152+
// Overwrite files
153+
try p12Data.write(to: certificateFolder.appendingPathComponent("certificate.p12"))
154+
try provData.write(to: certificateFolder.appendingPathComponent("profile.mobileprovision"))
155+
try password.data(using: .utf8)?.write(to: certificateFolder.appendingPathComponent("password.txt"))
156+
try displayName.data(using: .utf8)?.write(to: certificateFolder.appendingPathComponent("name.txt"))
157+
}
158+
112159
func deleteCertificate(folderName: String) throws {
113160
let certificateFolder = certificatesDirectory.appendingPathComponent(folderName)
114161
try fileManager.removeItem(at: certificateFolder)
@@ -123,35 +170,70 @@ class CertificateFileManager {
123170
struct CertificateView: View {
124171
@State private var customCertificates: [CustomCertificate] = []
125172
@State private var showAddCertificateSheet = false
173+
@State private var editingFolder: String? = nil
126174
@State private var selectedCert: String? = nil
175+
@State private var showingDeleteAlert = false
176+
@State private var certToDelete: CustomCertificate?
127177

128178
var body: some View {
129179
NavigationStack {
130180
ScrollView {
131-
LazyVStack(spacing: 20) {
181+
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) {
132182
ForEach(customCertificates) { cert in
133-
VStack(alignment: .leading, spacing: 12) {
134-
Text(cert.displayName)
135-
.font(.title2)
136-
.fontWeight(.semibold)
137-
.foregroundColor(.primary)
138-
}
139-
.padding(20)
140-
.frame(maxWidth: .infinity)
141-
.background(Color(.systemGray6))
142-
.cornerRadius(16)
143-
.overlay(
144-
RoundedRectangle(cornerRadius: 16)
145-
.stroke(selectedCert == cert.folderName ? Color.blue : Color.clear, lineWidth: 3)
146-
)
147-
.onTapGesture {
148-
if selectedCert == cert.folderName {
149-
selectedCert = nil
150-
UserDefaults.standard.removeObject(forKey: "selectedCertificateFolder")
151-
} else {
152-
selectedCert = cert.folderName
153-
UserDefaults.standard.set(selectedCert, forKey: "selectedCertificateFolder")
183+
ZStack(alignment: .top) {
184+
VStack(alignment: .leading, spacing: 12) {
185+
Text(cert.displayName)
186+
.font(.title2)
187+
.fontWeight(.semibold)
188+
.foregroundColor(.primary)
189+
}
190+
.padding(20)
191+
.frame(maxWidth: .infinity)
192+
.background(Color(.systemGray6))
193+
.cornerRadius(16)
194+
.overlay(
195+
RoundedRectangle(cornerRadius: 16)
196+
.stroke(selectedCert == cert.folderName ? Color.blue : Color.clear, lineWidth: 3)
197+
)
198+
.onTapGesture {
199+
if selectedCert == cert.folderName {
200+
selectedCert = nil
201+
UserDefaults.standard.removeObject(forKey: "selectedCertificateFolder")
202+
} else {
203+
selectedCert = cert.folderName
204+
UserDefaults.standard.set(selectedCert, forKey: "selectedCertificateFolder")
205+
}
206+
}
207+
208+
HStack {
209+
Button(action: {
210+
editingFolder = cert.folderName
211+
showAddCertificateSheet = true
212+
}) {
213+
Image(systemName: "pencil")
214+
.foregroundColor(.blue)
215+
.font(.caption)
216+
.padding(8)
217+
.background(Color.white.opacity(0.8))
218+
.clipShape(Circle())
219+
}
220+
221+
Spacer()
222+
223+
Button(action: {
224+
certToDelete = cert
225+
showingDeleteAlert = true
226+
}) {
227+
Image(systemName: "trash")
228+
.foregroundColor(.red)
229+
.font(.caption)
230+
.padding(8)
231+
.background(Color.white.opacity(0.8))
232+
.clipShape(Circle())
233+
}
154234
}
235+
.padding(.top, 12)
236+
.padding(.horizontal, 12)
155237
}
156238
}
157239
}
@@ -161,22 +243,36 @@ struct CertificateView: View {
161243
.navigationBarTitleDisplayMode(.inline)
162244
.toolbar {
163245
ToolbarItem(placement: .navigationBarTrailing) {
164-
Button(action: { showAddCertificateSheet = true }) {
246+
Button(action: {
247+
editingFolder = nil
248+
showAddCertificateSheet = true
249+
}) {
165250
Image(systemName: "plus")
166251
}
167252
}
168253
}
169254
.sheet(isPresented: $showAddCertificateSheet, onDismiss: {
170255
customCertificates = CertificateFileManager.shared.loadCertificates()
256+
editingFolder = nil
171257
// Re-check selected after reload
172258
if let sel = selectedCert, !customCertificates.contains(where: { $0.folderName == sel }) {
173259
selectedCert = nil
174260
UserDefaults.standard.removeObject(forKey: "selectedCertificateFolder")
175261
}
176262
}) {
177-
AddCertificateView()
263+
AddCertificateView(editingFolder: editingFolder)
178264
.presentationDetents([.large])
179265
}
266+
.alert("Delete Certificate?", isPresented: $showingDeleteAlert) {
267+
Button("Delete", role: .destructive) {
268+
if let cert = certToDelete {
269+
deleteCertificate(cert)
270+
}
271+
}
272+
Button("Cancel", role: .cancel) { }
273+
} message: {
274+
Text("Are you sure? This can't be undone.")
275+
}
180276
.onAppear {
181277
customCertificates = CertificateFileManager.shared.loadCertificates()
182278
selectedCert = UserDefaults.standard.string(forKey: "selectedCertificateFolder")
@@ -187,10 +283,20 @@ struct CertificateView: View {
187283
}
188284
}
189285
}
286+
287+
private func deleteCertificate(_ cert: CustomCertificate) {
288+
try? CertificateFileManager.shared.deleteCertificate(folderName: cert.folderName)
289+
customCertificates = CertificateFileManager.shared.loadCertificates()
290+
if selectedCert == cert.folderName {
291+
selectedCert = nil
292+
UserDefaults.standard.removeObject(forKey: "selectedCertificateFolder")
293+
}
294+
}
190295
}
191296

192297
struct AddCertificateView: View {
193298
@Environment(\.dismiss) private var dismiss
299+
let editingFolder: String?
194300

195301
@State private var p12File: CertificateFileItem?
196302
@State private var provFile: CertificateFileItem?
@@ -199,6 +305,10 @@ struct AddCertificateView: View {
199305
@State private var isChecking = false
200306
@State private var errorMessage = ""
201307

308+
init(editingFolder: String? = nil) {
309+
self.editingFolder = editingFolder
310+
}
311+
202312
var body: some View {
203313
NavigationView {
204314
Form {
@@ -248,11 +358,11 @@ struct AddCertificateView: View {
248358
.font(.subheadline)
249359
}
250360
}
251-
.navigationTitle("New Certificate")
361+
.navigationTitle(editingFolder != nil ? "Edit Certificate" : "New Certificate")
252362
.navigationBarTitleDisplayMode(.inline)
253363
.toolbar {
254364
ToolbarItem(placement: .navigationBarLeading) {
255-
Button("X") {
365+
Button("×") {
256366
dismiss()
257367
}
258368
.disabled(isChecking)
@@ -282,6 +392,25 @@ struct AddCertificateView: View {
282392
.onChange(of: password) { _ in
283393
errorMessage = ""
284394
}
395+
.onAppear {
396+
if let folder = editingFolder {
397+
loadForEdit(folder: folder)
398+
}
399+
}
400+
}
401+
}
402+
403+
private func loadForEdit(folder: String) {
404+
let certFolder = CertificateFileManager.shared.certificatesDirectory.appendingPathComponent(folder)
405+
let p12URL = certFolder.appendingPathComponent("certificate.p12")
406+
let provURL = certFolder.appendingPathComponent("profile.mobileprovision")
407+
let passwordURL = certFolder.appendingPathComponent("password.txt")
408+
409+
p12File = CertificateFileItem(name: "certificate.p12", url: p12URL)
410+
provFile = CertificateFileItem(name: "profile.mobileprovision", url: provURL)
411+
412+
if let pwData = try? Data(contentsOf: passwordURL), let pw = String(data: pwData, encoding: .utf8) {
413+
password = pw
285414
}
286415
}
287416

@@ -293,30 +422,40 @@ struct AddCertificateView: View {
293422

294423
DispatchQueue.global(qos: .userInitiated).async {
295424
do {
296-
// Access security-scoped resources
297-
guard p12URL.startAccessingSecurityScopedResource() else {
298-
throw NSError(domain: "AccessError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot access P12 file"])
299-
}
300-
defer { p12URL.stopAccessingSecurityScopedResource() }
301-
302-
guard provURL.startAccessingSecurityScopedResource() else {
303-
throw NSError(domain: "AccessError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot access Provision file"])
425+
var p12Data: Data
426+
var provData: Data
427+
if editingFolder != nil {
428+
// For edit, files are in app container, no security scope needed
429+
p12Data = try Data(contentsOf: p12URL)
430+
provData = try Data(contentsOf: provURL)
431+
} else {
432+
// For new, access security-scoped
433+
guard p12URL.startAccessingSecurityScopedResource() else {
434+
throw NSError(domain: "AccessError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot access P12 file"])
435+
}
436+
defer { p12URL.stopAccessingSecurityScopedResource() }
437+
438+
guard provURL.startAccessingSecurityScopedResource() else {
439+
throw NSError(domain: "AccessError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot access Provision file"])
440+
}
441+
defer { provURL.stopAccessingSecurityScopedResource() }
442+
443+
p12Data = try Data(contentsOf: p12URL)
444+
provData = try Data(contentsOf: provURL)
304445
}
305-
defer { provURL.stopAccessingSecurityScopedResource() }
306-
307-
let p12Data = try Data(contentsOf: p12URL)
308-
let provData = try Data(contentsOf: provURL)
309446

310447
let result = CertificatesManager.check(p12Data: p12Data, password: password, mobileProvisionData: provData)
311448

312449
var dispatchError: String?
313450
switch result {
314451
case .success(.success):
315-
// Get dynamic name
316452
let displayName = CertificatesManager.getCertificateName(p12Data: p12Data, password: password) ?? "Custom Certificate"
317453

318-
// Save
319-
_ = try CertificateFileManager.shared.saveCertificate(p12Data: p12Data, provData: provData, password: password, displayName: displayName)
454+
if let folder = editingFolder {
455+
try CertificateFileManager.shared.updateCertificate(folderName: folder, p12Data: p12Data, provData: provData, password: password, displayName: displayName)
456+
} else {
457+
_ = try CertificateFileManager.shared.saveCertificate(p12Data: p12Data, provData: provData, password: password, displayName: displayName)
458+
}
320459
case .success(.incorrectPassword):
321460
dispatchError = "Incorrect Password"
322461
case .success(.noMatch):

project.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ targets:
3030
properties:
3131
CFBundleDisplayName: "ProStore"
3232
CFBundleName: "prostore"
33-
CFBundleVersion: "27"
34-
CFBundleShortVersionString: "1.4.6"
33+
CFBundleVersion: "28"
34+
CFBundleShortVersionString: "1.5.0"
3535
UILaunchStoryboardName: "LaunchScreen"
3636
NSPrincipalClass: "UIApplication"
3737
NSAppTransportSecurity:

0 commit comments

Comments
 (0)