import SwiftUI /// Whether the server still lets this build run. The answer is kept, so a build /// that was stopped opens stopped, offline too. A server that doesn't answer /// changes nothing, so an outage never locks anyone out. @MainActor @Observable final class AppUpdate { private static let minimumKey = "minimumBuild" private static let reasonKey = "minimumBuildReason" /// This build, the highest there is if it can't be read, so that it runs. private static let build = (Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String).flatMap(Int.init) ?? .max private(set) var required: Bool private(set) var reason: String? init() { let kept = UserDefaults.standard required = kept.integer(forKey: Self.minimumKey) > Self.build reason = kept.string(forKey: Self.reasonKey) } func check() async { guard let answer: ServerDescription = try? await Client.shared.query("describeServer") else { return } let kept = UserDefaults.standard kept.set(answer.ios?.minimumBuild ?? 0, forKey: Self.minimumKey) kept.set(answer.ios?.reason, forKey: Self.reasonKey) required = (answer.ios?.minimumBuild ?? 0) > Self.build reason = answer.ios?.reason } } /// All a build too old shows: why, and the way to a newer one. struct UpdateScreen: View { private static let appID = "6831466657" let reason: String? @Environment(\.openURL) private var openURL /// A TestFlight build updates in TestFlight, where the App Store may offer /// nothing newer. A sandbox receipt is what tells them apart without asking /// a server; the API Apple offers instead is async and can ask to sign in. private var store: URL { let beta = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" return URL(string: beta ? "itms-beta://beta.itunes.apple.com/v1/app/\(Self.appID)" : "https://apps.apple.com/app/id\(Self.appID)")! } var body: some View { VStack(spacing: Space.s3) { Spacer(minLength: Space.s4) Image("mark").renderingMode(.template).resizable().scaledToFit() .frame(width: 112) .foregroundStyle(.primary) .accessibilityHidden(true) Spacer(minLength: Space.s4) Text("Update Knotless").font(.title3.weight(.semibold)) Text(reason ?? "This version no longer works. Update to keep using it.") .font(.body) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) .padding(.bottom, Space.s3) Button { openURL(store) } label: { Text("Update") .font(.body.weight(.semibold)) .foregroundStyle(Color.page) .frame(maxWidth: .infinity, minHeight: Size.signIn) .background(Color.fg, in: Capsule()) .contentShape(Capsule()) } .buttonStyle(.plain) .padding(.bottom, Space.s6) } .padding(.horizontal, Space.s6) .frame(maxWidth: .infinity, maxHeight: .infinity) } }