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

Commit ec70407

Browse files
authored
Test release for zsign
1 parent dd001ed commit ec70407

2 files changed

Lines changed: 233 additions & 3 deletions

File tree

Sources/prostore/prostore.swift

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,228 @@
11
import SwiftUI
2+
import UniformTypeIdentifiers
3+
import ZIPFoundation // ZipFoundation
4+
import Zsign // your Swift package that exports `Zsign`
5+
6+
struct FileItem {
7+
var url: URL?
8+
var name: String { url?.lastPathComponent ?? "" }
9+
}
210

311
@main
4-
struct ProStore: App {
12+
struct ZsignOnDeviceApp: App {
513
var body: some Scene {
614
WindowGroup {
715
ContentView()
816
}
917
}
18+
}
19+
20+
struct ContentView: View {
21+
@State private var ipa = FileItem()
22+
@State private var p12 = FileItem()
23+
@State private var prov = FileItem()
24+
@State private var p12Password = ""
25+
@State private var isProcessing = false
26+
@State private var message = ""
27+
@State private var showActivity = false
28+
@State private var activityURL: URL? = nil
29+
@State private var showPickerFor: PickerKind?
30+
31+
enum PickerKind { case ipa, p12, prov }
32+
33+
var body: some View {
34+
NavigationView {
35+
Form {
36+
Section(header: Text("Inputs")) {
37+
HStack {
38+
Text("IPA:")
39+
Spacer()
40+
Text(ipa.name.isEmpty ? "none" : ipa.name).foregroundColor(.secondary)
41+
Button("Pick") { showPickerFor = .ipa }
42+
}
43+
HStack {
44+
Text("P12:")
45+
Spacer()
46+
Text(p12.name.isEmpty ? "none" : p12.name).foregroundColor(.secondary)
47+
Button("Pick") { showPickerFor = .p12 }
48+
}
49+
HStack {
50+
Text("MobileProvision:")
51+
Spacer()
52+
Text(prov.name.isEmpty ? "none" : prov.name).foregroundColor(.secondary)
53+
Button("Pick") { showPickerFor = .prov }
54+
}
55+
SecureField("P12 Password", text: $p12Password)
56+
}
57+
58+
Section {
59+
Button(action: runSign) {
60+
HStack {
61+
Spacer()
62+
if isProcessing { ProgressView() }
63+
Text("Sign IPA").bold()
64+
Spacer()
65+
}
66+
}
67+
.disabled(isProcessing || ipa.url == nil || p12.url == nil || prov.url == nil)
68+
}
69+
70+
Section(header: Text("Status")) {
71+
Text(message).foregroundColor(.primary)
72+
}
73+
}
74+
.navigationTitle("Zsign On Device")
75+
.sheet(item: $showPickerFor, onDismiss: nil) { kind in
76+
DocumentPicker(kind: kind, onPick: { url in
77+
switch kind {
78+
case .ipa: ipa.url = url
79+
case .p12: p12.url = url
80+
case .prov: prov.url = url
81+
}
82+
})
83+
}
84+
.sheet(isPresented: $showActivity) {
85+
if let u = activityURL {
86+
ActivityView(url: u)
87+
} else {
88+
Text("No file to share")
89+
}
90+
}
91+
}
92+
}
93+
94+
func runSign() {
95+
guard let ipaURL = ipa.url, let p12URL = p12.url, let provURL = prov.url else {
96+
message = "Pick all input files first."
97+
return
98+
}
99+
isProcessing = true
100+
message = "Working..."
101+
102+
DispatchQueue.global(qos: .userInitiated).async {
103+
do {
104+
let fm = FileManager.default
105+
let tmp = fm.temporaryDirectory.appendingPathComponent("zsign_ios_\(UUID().uuidString)")
106+
try fm.createDirectory(at: tmp, withIntermediateDirectories: true)
107+
108+
// copy inputs into tmp
109+
let localIPA = tmp.appendingPathComponent(ipaURL.lastPathComponent)
110+
let localP12 = tmp.appendingPathComponent(p12URL.lastPathComponent)
111+
let localProv = tmp.appendingPathComponent(provURL.lastPathComponent)
112+
try fm.copyItem(at: ipaURL, to: localIPA)
113+
try fm.copyItem(at: p12URL, to: localP12)
114+
try fm.copyItem(at: provURL, to: localProv)
115+
116+
// unzip IPA -> tmp
117+
let archive = try Archive(url: localIPA, accessMode: .read)
118+
try archive.extract(to: tmp)
119+
120+
// find Payload/*.app
121+
let payload = tmp.appendingPathComponent("Payload")
122+
guard fm.fileExists(atPath: payload.path) else {
123+
throw NSError(domain: "ZsignOnDevice", code: 1, userInfo: [NSLocalizedDescriptionKey: "Payload not found"])
124+
}
125+
let contents = try fm.contentsOfDirectory(atPath: payload.path)
126+
guard let appName = contents.first(where: { $0.hasSuffix(".app") }) else {
127+
throw NSError(domain: "ZsignOnDevice", code: 2, userInfo: [NSLocalizedDescriptionKey: "No .app bundle in Payload"])
128+
}
129+
let appDir = payload.appendingPathComponent(appName)
130+
131+
// Call Zsign.swift package sign API
132+
DispatchQueue.main.async { message = "Signing \(appName)..." }
133+
134+
// NOTE: match your Zsign API exactly. This call mirrors the wrapper you posted earlier:
135+
let ok = Zsign.sign(
136+
appPath: appDir.path,
137+
provisionPath: localProv.path,
138+
p12Path: localP12.path,
139+
p12Password: p12Password,
140+
entitlementsPath: "", // optional
141+
customIdentifier: "",
142+
customName: "",
143+
customVersion: "",
144+
adhoc: false,
145+
removeProvision: false,
146+
completion: nil
147+
)
148+
149+
guard ok else {
150+
throw NSError(domain: "ZsignOnDevice", code: 3, userInfo: [NSLocalizedDescriptionKey: "Zsign.sign returned false"])
151+
}
152+
153+
// Zsign usually writes changes in-place inside the .app. Rezip Payload -> signed IPA
154+
let signedIpa = tmp.appendingPathComponent("signed_\(appName).ipa")
155+
// create archive with Payload directory
156+
try fm.createDirectory(at: signedIpa.deletingLastPathComponent(), withIntermediateDirectories: true)
157+
let writeArchive = try Archive(url: signedIpa, accessMode: .create)
158+
159+
// recursively add Payload
160+
let enumerator = fm.enumerator(at: payload, includingPropertiesForKeys: nil)!
161+
for case let file as URL in enumerator {
162+
let relative = file.path.replacingOccurrences(of: tmp.path + "/", with: "")
163+
if file.hasDirectoryPath {
164+
try writeArchive.addEntry(with: relative + "/", type: .directory, uncompressedSize: 0, compressionMethod: .deflate) // directories recorded
165+
} else {
166+
let data = try Data(contentsOf: file)
167+
try writeArchive.addEntry(with: relative, type: .file, uncompressedSize: UInt32(data.count), compressionMethod: .deflate, provider: { (position, size) -> Data in
168+
return data.subdata(in: Int(position)..<Int(position + size))
169+
})
170+
}
171+
}
172+
173+
// share file (move to Documents to be easily accessible)
174+
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
175+
let outURL = docs.appendingPathComponent("signed_\(UUID().uuidString).ipa")
176+
if fm.fileExists(atPath: outURL.path) { try fm.removeItem(at: outURL) }
177+
try fm.copyItem(at: signedIpa, to: outURL)
178+
179+
DispatchQueue.main.async {
180+
activityURL = outURL
181+
showActivity = true
182+
message = "Done — signed IPA ready to share!"
183+
isProcessing = false
184+
}
185+
186+
} catch {
187+
DispatchQueue.main.async {
188+
message = "Error: \(error.localizedDescription)"
189+
isProcessing = false
190+
}
191+
}
192+
}
193+
}
194+
}
195+
196+
// DocumentPicker wrapper for picking any file types
197+
struct DocumentPicker: UIViewControllerRepresentable {
198+
var kind: ContentView.PickerKind
199+
var onPick: (URL) -> Void
200+
201+
func makeCoordinator() -> Coordinator { Coordinator(self) }
202+
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
203+
let types: [UTType] = [.item] // let user pick any file; could refine UTType.zip / ipa mimetype
204+
let vc = UIDocumentPickerViewController(forOpeningContentTypes: types, asCopy: true)
205+
vc.delegate = context.coordinator
206+
vc.allowsMultipleSelection = false
207+
return vc
208+
}
209+
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}
210+
211+
class Coordinator: NSObject, UIDocumentPickerDelegate {
212+
let parent: DocumentPicker
213+
init(_ p: DocumentPicker) { parent = p }
214+
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
215+
guard let u = urls.first else { return }
216+
parent.onPick(u)
217+
}
218+
}
219+
}
220+
221+
struct ActivityView: UIViewControllerRepresentable {
222+
let url: URL
223+
func makeUIViewController(context: Context) -> UIActivityViewController {
224+
let vc = UIActivityViewController(activityItems: [url], applicationActivities: nil)
225+
return vc
226+
}
227+
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
10228
}

project.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ packages:
1212
url: https://github.com/ProStore-iOS/ProSourceManager.git
1313
branch: main
1414

15+
ZsignPackage:
16+
url: https://github.com/khcrysalis/Zsign-Package.git
17+
branch: package
18+
19+
ZIPFoundation:
20+
url: https://github.com/weichsel/ZIPFoundation.git
21+
branch: main
22+
1523
targets:
1624
prostore:
1725
type: application
@@ -28,11 +36,15 @@ targets:
2836
CFBundleDisplayName: "ProStore"
2937
CFBundleName: "prostore"
3038
CFBundleVersion: "4"
31-
CFBundleShortVersionString: "1.0.3"
39+
CFBundleShortVersionString: "1.0.4"
3240
UILaunchStoryboardName: "LaunchScreen"
3341
NSPrincipalClass: "UIApplication"
3442
NSAppTransportSecurity:
3543
NSAllowsArbitraryLoads: true
3644
dependencies:
3745
- package: ProSourceManager
38-
product: ProSourceManager
46+
product: ProSourceManager
47+
- package: ZsignPackage
48+
product: Zsign
49+
- package: ZIPFoundation
50+
product: ZIPFoundation

0 commit comments

Comments
 (0)