Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .claude/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,24 @@ leaving the real-install call sites unchanged.

---

## InstallAppOperation — self-update backgrounding dropped the `installing` guard (KNOWN, not fixed)

`scheduleSelfUpdateBackgrounding` (InstallAppOperation.swift ~234-263) starts a 3s timer that
suspends the app's XPC connection so a self-update can finish in the background. Upstream
SideStore guarded the suspend with a `var installing` flag (set false once `installIPA`
returned) so it would NOT suspend if the install had already completed. MiniStore dropped the
flag — the timer now suspends unconditionally once `applicationState == .active`.

**Audited 2026-06-28, deliberately left as-is.** For the self-update path `installIPA` blocks
until the user leaves the app, so the timer practically always fires while the install is still
in-flight, making the removed guard a no-op (effect confidence ~0.2). The `applicationState`
guard is preserved. Re-adding the flag is delicate self-update timing code with near-zero
upside; only revisit if a self-update is observed force-suspending after completing. To restore
parity: add `private var installFinished = false`, set it in `installAndFinish`'s success/failure
paths, and early-return from the timer block when set.

---

## ResignAppOperation — Open URL scheme must use the customized bundle ID

The `sidestore-<id>://` scheme baked into the installed app's `CFBundleURLSchemes` (so the
Expand Down
13 changes: 10 additions & 3 deletions AltStore/App Detail/AppDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,22 @@ final class AppDetailModel {

let nc = NotificationCenter.default
let q = OperationQueue.main
// Observers fire on OperationQueue.main, so the block always runs on the main actor;
// assumeIsolated lets us mutate the @MainActor appTick without an extra async hop and
// silences the Swift 6 "Sendable closure" isolation warning.
observers = [
nc.addObserver(forName: .NSManagedObjectContextObjectsDidChange,
object: DatabaseManager.shared.viewContext, queue: q) { [weak self] _ in
self?.appTick += 1
MainActor.assumeIsolated { self?.appTick += 1 }
},
nc.addObserver(forName: UIApplication.willEnterForegroundNotification,
object: nil, queue: q) { [weak self] _ in self?.appTick += 1 },
object: nil, queue: q) { [weak self] _ in
MainActor.assumeIsolated { self?.appTick += 1 }
},
nc.addObserver(forName: UIApplication.didBecomeActiveNotification,
object: nil, queue: q) { [weak self] _ in self?.appTick += 1 },
object: nil, queue: q) { [weak self] _ in
MainActor.assumeIsolated { self?.appTick += 1 }
},
]
}

Expand Down
2 changes: 2 additions & 0 deletions AltStore/Managing Apps/AppExtensionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ struct AppExtensionView: View {
}
}
}
.scrollContentBackground(.hidden)
.background(Color(uiColor: .altBackground).ignoresSafeArea())
.navigationTitle("App Extensions")
.onDisappear {
_ = completion(selection)
Expand Down
12 changes: 10 additions & 2 deletions AltStore/Settings/AnisetteServerList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ struct AnisetteServersView: View {
@StateObject var viewModel: AnisetteViewModel = AnisetteViewModel()
@State var selected: String? = nil
@State private var showingConfirmation = false
@State private var listFetchTask: Task<Void, Never>?
var errorCallback: () -> ()
var refreshCallback: (Result<Void, any Error>) -> Void

Expand Down Expand Up @@ -147,8 +148,15 @@ struct AnisetteServersView: View {
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
.onChange(of: viewModel.source) { _, newValue in
UserDefaults.standard.menuAnisetteList = newValue
// viewModel.getCurrentListOfServers(refreshCallback) // don't spam
viewModel.getCurrentListOfServers()
// Debounce: onChange fires per keystroke, so fetching here directly
// launched a URLSession request against each partial (invalid) URL.
// Cancel the pending fetch and wait for a typing pause before fetching.
listFetchTask?.cancel()
listFetchTask = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
viewModel.getCurrentListOfServers()
}
}

HStack(spacing: 16) {
Expand Down
2 changes: 2 additions & 0 deletions AltStore/Settings/OperationsLoggingContolView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ struct OperationsLoggingControlView: View {

private func CustomList<Content: View>(@ViewBuilder content: () -> Content) -> some View {
List { content() }
.scrollContentBackground(.hidden)
.background(Color(uiColor: .altBackground).ignoresSafeArea())
}

private func CustomSection<Content: View>(header: Text, @ViewBuilder content: () -> Content) -> some View {
Expand Down
10 changes: 6 additions & 4 deletions AltStore/Sources/SourceDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,7 @@ struct SourceDetailView: View {
}
.frame(height: maxContentY)

contentCard(navFraction: navFraction)
.frame(minHeight: geo.size.height - 44, alignment: .top)
contentCard(navFraction: navFraction, geoHeight: geo.size.height)
}
}
.coordinateSpace(name: "sourceDetailScroll")
Expand Down Expand Up @@ -421,7 +420,7 @@ struct SourceDetailView: View {
// MARK: Content Card

@ViewBuilder
private func contentCard(navFraction: CGFloat) -> some View {
private func contentCard(navFraction: CGFloat, geoHeight: CGFloat) -> some View {
let radius = DetailLayout.cornerRadius * (1 - navFraction)
VStack(alignment: .leading, spacing: 10) {
if !source.newsItems.isEmpty {
Expand All @@ -434,7 +433,10 @@ struct SourceDetailView: View {
Spacer(minLength: DetailLayout.padding)
}
.padding(.top, DetailLayout.padding)
.frame(maxWidth: .infinity, alignment: .leading)
// minHeight here (not on the call-site wrapper) so the altBackground fill below covers
// the full card frame. Applying the frame after .background left the expanded bottom
// region transparent, leaking a grey backdrop with OLED on (same fix as AppDetailView).
.frame(maxWidth: .infinity, minHeight: geoHeight - 44, alignment: .topLeading)
.background(
UnevenRoundedRectangle(topLeadingRadius: radius, topTrailingRadius: radius, style: .continuous)
.fill(Color(uiColor: .altBackground))
Expand Down
4 changes: 4 additions & 0 deletions AltStore/Updates/UpdatesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ final class UpdatesModel {
) { [weak self] result in
DispatchQueue.main.async {
guard let self else { return }
// The completion lands async; if the queue was already cleared (nav lost, a
// sibling failure, or cancel) in that window, removeFirst() would crash. Bail —
// cancelRemainingQueue already reset button states and progress.
guard !self.updateQueue.isEmpty else { return }
self.updateQueue.removeFirst()
self.buttonStates.removeValue(forKey: bundleID)

Expand Down
Loading