From 9e7db2eb993b0e6d2fb8adcff6da8d9cceab65e8 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 12 Oct 2026 18:41:10 +0200 Subject: [PATCH 1/5] Show Source & licenses, pinned to the build's commit, and remove this phone The app store permission in LICENSE asks that the license and an offer of the exact version's source be reachable in the app. A Copy Files phase puts LICENSE, NOTICE and LICENSING.md from the repository root into the bundle, and a script phase records the commit: FTW_SOURCE_REVISION from CI, else git, with a tree that has changes marked as a local build. Source & licenses shows version, build and commit, links to the tree and the license at that commit, the three texts joined for a phone's width, and that there is no third-party code. A test fails when the package or the project gains a dependency. Remove this phone clears the key, the pairing, the sealed readings and the widgets' records (AppModel.leave) and says how to remove the phone on the box. The registry has no op for a phone to remove itself. Both are reached from the foot of Now and of pairing. Co-Authored-By: Claude Opus 5.5 --- appleApp/FTW.xcconfig | 6 + appleApp/FTW.xcodeproj/project.pbxproj | 56 ++++++ appleApp/FTW/About/AboutLink.swift | 55 +++++ appleApp/FTW/About/AboutView.swift | 150 ++++++++++++++ appleApp/FTW/About/LicenseView.swift | 65 ++++++ appleApp/FTW/About/RemovePhoneView.swift | 66 ++++++ appleApp/FTW/UI/NowView.swift | 1 + appleApp/FTW/UI/PairView.swift | 1 + .../Sources/FTWKit/Identity/AboutWords.swift | 87 ++++++++ .../Sources/FTWKit/Identity/BuildSource.swift | 93 +++++++++ .../Sources/FTWKit/Identity/LicenseText.swift | 143 +++++++++++++ .../Sources/FTWKit/Identity/Origin.swift | 5 +- .../FTWKit/Identity/PhoneRemoval.swift | 27 +++ .../FTWKit/Tests/FTWKitTests/AboutTests.swift | 189 ++++++++++++++++++ 14 files changed, 942 insertions(+), 2 deletions(-) create mode 100644 appleApp/FTW/About/AboutLink.swift create mode 100644 appleApp/FTW/About/AboutView.swift create mode 100644 appleApp/FTW/About/LicenseView.swift create mode 100644 appleApp/FTW/About/RemovePhoneView.swift create mode 100644 appleApp/FTWKit/Sources/FTWKit/Identity/AboutWords.swift create mode 100644 appleApp/FTWKit/Sources/FTWKit/Identity/BuildSource.swift create mode 100644 appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift create mode 100644 appleApp/FTWKit/Sources/FTWKit/Identity/PhoneRemoval.swift create mode 100644 appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift diff --git a/appleApp/FTW.xcconfig b/appleApp/FTW.xcconfig index 8a90e57..e4a5a20 100644 --- a/appleApp/FTW.xcconfig +++ b/appleApp/FTW.xcconfig @@ -22,3 +22,9 @@ CODE_SIGN_STYLE = Automatic // anyone without one. The entitlements, and with them the App Group, still // travel with the app. CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = - + +// The commit Source & licenses links to, which the license asks the app to +// offer. CI passes the full commit (xcodebuild … FTW_SOURCE_REVISION=$GITHUB_SHA). +// Left empty, the "Record the source revision" phase asks git, and a tree +// with changes is a local build that links to the repository instead. +FTW_SOURCE_REVISION = diff --git a/appleApp/FTW.xcodeproj/project.pbxproj b/appleApp/FTW.xcodeproj/project.pbxproj index 9a2ab24..f3b1b06 100644 --- a/appleApp/FTW.xcodeproj/project.pbxproj +++ b/appleApp/FTW.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ F7A000000000000000000014 /* FTWKit in Frameworks */ = {isa = PBXBuildFile; productRef = F7A000000000000000000016 /* FTWKit */; }; F7A000000000000000000030 /* FTWDemo in Frameworks */ = {isa = PBXBuildFile; productRef = F7A000000000000000000031 /* FTWDemo */; }; F7A000000000000000000032 /* FTWShared in Frameworks */ = {isa = PBXBuildFile; productRef = F7A000000000000000000033 /* FTWShared */; }; + F7A000000000000000000064 /* LICENSE in Copy the license texts */ = {isa = PBXBuildFile; fileRef = F7A000000000000000000060 /* LICENSE */; }; + F7A000000000000000000065 /* NOTICE in Copy the license texts */ = {isa = PBXBuildFile; fileRef = F7A000000000000000000061 /* NOTICE */; }; + F7A000000000000000000066 /* LICENSING.md in Copy the license texts */ = {isa = PBXBuildFile; fileRef = F7A000000000000000000062 /* LICENSING.md */; }; F7A000000000000000000034 /* FTWWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F7A000000000000000000040 /* FTWWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -35,6 +38,19 @@ name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; + F7A000000000000000000067 /* Copy the license texts */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Licenses; + dstSubfolderSpec = 7; + files = ( + F7A000000000000000000064 /* LICENSE in Copy the license texts */, + F7A000000000000000000065 /* NOTICE in Copy the license texts */, + F7A000000000000000000066 /* LICENSING.md in Copy the license texts */, + ); + name = "Copy the license texts"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -44,6 +60,9 @@ F7A000000000000000000008 /* FTW.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FTW.xcconfig; sourceTree = ""; }; F7A000000000000000000009 /* FTW-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FTW-Info.plist"; sourceTree = ""; }; F7A00000000000000000000A /* FTWWidgets-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FTWWidgets-Info.plist"; sourceTree = ""; }; + F7A000000000000000000060 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = SOURCE_ROOT; }; + F7A000000000000000000061 /* NOTICE */ = {isa = PBXFileReference; lastKnownFileType = text; name = NOTICE; path = ../NOTICE; sourceTree = SOURCE_ROOT; }; + F7A000000000000000000062 /* LICENSING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = LICENSING.md; path = ../LICENSING.md; sourceTree = SOURCE_ROOT; }; F7A000000000000000000040 /* FTWWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FTWWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -91,10 +110,21 @@ F7A000000000000000000009 /* FTW-Info.plist */, F7A000000000000000000007 /* FTWWidgets.entitlements */, F7A00000000000000000000A /* FTWWidgets-Info.plist */, + F7A000000000000000000063 /* License texts */, F7A000000000000000000004 /* Products */, ); sourceTree = ""; }; + F7A000000000000000000063 /* License texts */ = { + isa = PBXGroup; + children = ( + F7A000000000000000000060 /* LICENSE */, + F7A000000000000000000061 /* NOTICE */, + F7A000000000000000000062 /* LICENSING.md */, + ); + name = "License texts"; + sourceTree = ""; + }; F7A000000000000000000004 /* Products */ = { isa = PBXGroup; children = ( @@ -114,6 +144,8 @@ F7A000000000000000000011 /* Sources */, F7A000000000000000000012 /* Frameworks */, F7A000000000000000000013 /* Resources */, + F7A000000000000000000067 /* Copy the license texts */, + F7A000000000000000000068 /* Record the source revision */, F7A000000000000000000036 /* Embed Foundation Extensions */, ); buildRules = ( @@ -215,6 +247,30 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + F7A000000000000000000068 /* Record the source revision */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Record the source revision"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/Licenses/REVISION", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# The commit Source & licenses links to. CI passes FTW_SOURCE_REVISION\n# (GITHUB_SHA). Otherwise git says. A tree with changes, or no git at all,\n# is a local build, and the screen says so.\nset -eu\nout=\"$SCRIPT_OUTPUT_FILE_0\"\nmkdir -p \"$(dirname \"$out\")\"\nrev=\"${FTW_SOURCE_REVISION:-}\"\nif [ -n \"$rev\" ]; then\n if [ ${#rev} -ne 40 ] || [ -n \"$(printf '%s' \"$rev\" | tr -d '0-9a-f')\" ]; then\n echo \"error: FTW_SOURCE_REVISION must be a full 40-character commit, not '$rev'\"\n exit 1\n fi\nelif head=$(git -C \"$SRCROOT\" rev-parse --verify HEAD 2>/dev/null) &&\n changes=$(git -C \"$SRCROOT\" --no-optional-locks status --porcelain 2>/dev/null); then\n rev=\"$head\"\n if [ -n \"$changes\" ]; then rev=\"$head-dirty\"; fi\nfi\nprintf '%s' \"$rev\" > \"$out\"\necho \"Source revision: ${rev:-unknown}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ F7A000000000000000000011 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/appleApp/FTW/About/AboutLink.swift b/appleApp/FTW/About/AboutLink.swift new file mode 100644 index 0000000..8c03aa7 --- /dev/null +++ b/appleApp/FTW/About/AboutLink.swift @@ -0,0 +1,55 @@ +import FTWKit +import SwiftUI + +/// The way to Source & licenses and to removing this phone, at the foot of +/// Now and of pairing. The license asks that its text and the source offer +/// be reachable from inside the app, and App Review asks the same of +/// removing what the app set up. +struct AboutLink: View { + let app: AppModel + @State private var open: AboutView.Start? + @Environment(\.dynamicTypeSize) private var typeSize + + var body: some View { + let stack = typeSize.isAccessibilitySize ? AnyLayout(VStackLayout(spacing: 8)) : AnyLayout(HStackLayout(spacing: 20)) + VStack(spacing: 6) { + stack { + Button(AboutWords.text("Source & licenses")) { open = .about } + if AboutView.canRemove(app) { + Button(AboutWords.text("Remove this phone")) { open = .remove } + } + } + .font(.footnote.weight(.medium)) + .foregroundStyle(Theme.accent) + .buttonStyle(.plain) + Text(verbatim: "FTW \(BuildSource.bundled.versionText)") + .font(.caption) + .foregroundStyle(Theme.fgMuted) + } + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.top, 28) + .padding(.bottom, 8) + .sheet(item: $open) { start in + AboutView(app: app, start: start) + } + #if DEBUG + // For screenshots: `-FTWAbout about`, `remove` or a file name such + // as `LICENSE` opens Source & licenses there. + .task { + if let name = UserDefaults.standard.string(forKey: "FTWAbout") { open = AboutView.Start(name) } + } + #endif + } +} + +extension BuildSource { + /// This build, as its bundle records it. The "Record the source revision" + /// phase writes the commit; CI sets FTW_SOURCE_REVISION to GITHUB_SHA. + static let bundled = BuildSource( + version: AppInfo.version, + build: AppInfo.buildNumber, + recorded: Bundle.main.url(forResource: "REVISION", withExtension: nil, subdirectory: "Licenses") + .flatMap { try? String(contentsOf: $0, encoding: .utf8) } + ) +} diff --git a/appleApp/FTW/About/AboutView.swift b/appleApp/FTW/About/AboutView.swift new file mode 100644 index 0000000..fb167e1 --- /dev/null +++ b/appleApp/FTW/About/AboutView.swift @@ -0,0 +1,150 @@ +import FTWKit +import SwiftUI + +/// Source & licenses: what this build is, where its exact source is, and +/// the license texts the build copied in from the repository root. +struct AboutView: View { + enum Start: Hashable, Identifiable { + case about, remove + case document(LicenseText.Document) + + var id: Self { self } + + init?(_ name: String) { + switch name { + case "about": self = .about + case "remove": self = .remove + default: + guard let document = LicenseText.Document(rawValue: name) else { return nil } + self = .document(document) + } + } + } + + enum Route: Hashable { + case document(LicenseText.Document) + case remove + } + + let app: AppModel + @State private var path: [Route] + @Environment(\.dismiss) private var dismiss + private let source = BuildSource.bundled + + init(app: AppModel, start: Start = .about) { + self.app = app + switch start { + case .about: _path = State(initialValue: []) + case .remove: _path = State(initialValue: [.remove]) + case .document(let document): _path = State(initialValue: [.document(document)]) + } + } + + /// A paired phone has something to remove. The demo has not, and its + /// way out is Exit demo. + static func canRemove(_ app: AppModel) -> Bool { + !app.isDemo && (app.vault.isEnrolled || app.sites.current() != nil) + } + + var body: some View { + NavigationStack(path: $path) { + List { + Section { header } + + Section { + LabeledContent(AboutWords.text("Version"), value: source.version) + LabeledContent(AboutWords.text("Build"), value: source.build) + LabeledContent(AboutWords.text("Commit")) { + Text(source.revisionText) + .font(source.isLocal ? .body : .body.monospaced()) + .textSelection(.enabled) + } + } header: { + Text(AboutWords.text("This version")) + } footer: { + if let note = source.localNote { Text(note) } + } + + Section { + Link(destination: source.sourceURL) { + Label(source.isLocal ? AboutWords.text("Source code on GitHub") : AboutWords.text("Source code of this version"), systemImage: "chevron.left.forwardslash.chevron.right") + } + Link(destination: source.licenseURL) { + Label(source.isLocal ? AboutWords.text("License on GitHub") : AboutWords.text("License at this commit"), systemImage: "doc.text") + } + } header: { + Text(AboutWords.text("Source code")) + } footer: { + Text(AboutWords.text("FTW is free software. You may run, study, change and share it under the GNU AGPL v3 with the two additional permissions in LICENSE. The link gives the complete source of this exact version, at no charge.")) + } + + Section { + ForEach(LicenseText.Document.allCases) { document in + NavigationLink(value: Route.document(document)) { + VStack(alignment: .leading, spacing: 2) { + Text(document.title) + Text(document.rawValue) + .font(.caption.monospaced()) + .foregroundStyle(Theme.fgMuted) + } + } + } + } header: { + Text(AboutWords.text("Licenses")) + } footer: { + Text(verbatim: "\(AboutWords.text("Third-party code")): \(AboutWords.text("None. FTW uses only Apple's own frameworks."))") + } + + if Self.canRemove(app) { + Section(AboutWords.text("This phone")) { + NavigationLink(value: Route.remove) { + Text(AboutWords.text("Remove this phone")) + .foregroundStyle(Theme.importing) + } + } + } + } + .scrollContentBackground(.hidden) + .background(Theme.surface) + .navigationTitle(AboutWords.text("Source & licenses")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(role: .close) { dismiss() } + } + } + .navigationDestination(for: Route.self) { route in + switch route { + case .document(let document): + LicenseView(document: document, source: source) + case .remove: + RemovePhoneView(app: app) { dismiss() } + } + } + } + .tint(Theme.accent) + } + + /// The notices the AGPL asks an interactive program to show: who holds + /// the copyright, that there is no warranty, and where the license is. + private var header: some View { + HStack(alignment: .top, spacing: 14) { + Image(systemName: "bolt.fill") + .font(.title2) + .foregroundStyle(Theme.accent) + .frame(width: 52, height: 52) + .background(Color(white: 0.05), in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .accessibilityHidden(true) + VStack(alignment: .leading, spacing: 4) { + Text("FTW").font(.title2.weight(.bold)) + Text(AboutWords.text("© 2026 Sourceful Labs AB and contributors.")) + .font(.footnote) + Text(AboutWords.text("There is no warranty, to the extent the law allows.")) + .font(.footnote) + .foregroundStyle(Theme.fgDim) + } + } + .padding(.vertical, 4) + .accessibilityElement(children: .combine) + } +} diff --git a/appleApp/FTW/About/LicenseView.swift b/appleApp/FTW/About/LicenseView.swift new file mode 100644 index 0000000..716eb53 --- /dev/null +++ b/appleApp/FTW/About/LicenseView.swift @@ -0,0 +1,65 @@ +import FTWKit +import SwiftUI + +/// One license file, as the build copied it into the app from the +/// repository root, with its lines joined for a phone's width. +struct LicenseView: View { + let document: LicenseText.Document + let source: BuildSource + private let raw: String? + private let blocks: [LicenseText.Block] + + init(document: LicenseText.Document, source: BuildSource) { + self.document = document + self.source = source + raw = Bundle.main.url(forResource: document.rawValue, withExtension: nil, subdirectory: "Licenses") + .flatMap { try? String(contentsOf: $0, encoding: .utf8) } + blocks = raw.map { LicenseText.blocks($0, markdown: document.isMarkdown) } ?? [] + } + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading, spacing: 12) { + if raw == nil { + Text(AboutWords.text("This text is missing from this build.")) + .foregroundStyle(Theme.importing) + } + ForEach(blocks) { block in + row(block) + } + } + .textSelection(.enabled) + .padding(16) + .frame(maxWidth: 640, alignment: .leading) + .frame(maxWidth: .infinity) + } + .background(Theme.surface) + .navigationTitle(document.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + if let raw { + ToolbarItem(placement: .primaryAction) { + ShareLink(item: raw, preview: SharePreview(document.rawValue)) + } + } + } + } + + @ViewBuilder private func row(_ block: LicenseText.Block) -> some View { + switch block.kind { + case .heading(let level): + Text(block.text) + .font(level == 1 ? .title3.weight(.semibold) : .headline) + .foregroundStyle(Theme.fg) + .padding(.top, 8) + .accessibilityAddTraits(.isHeader) + case .rule: + Divider().overlay(Theme.line) + case .paragraph: + Text(document.isMarkdown ? LicenseText.inline(block.text, base: source.documentBaseURL) : AttributedString(block.text)) + .font(.callout) + .foregroundStyle(Theme.fg) + .fixedSize(horizontal: false, vertical: true) + } + } +} diff --git a/appleApp/FTW/About/RemovePhoneView.swift b/appleApp/FTW/About/RemovePhoneView.swift new file mode 100644 index 0000000..eb6a7cc --- /dev/null +++ b/appleApp/FTW/About/RemovePhoneView.swift @@ -0,0 +1,66 @@ +import FTWKit +import SwiftUI + +/// Removing this phone: what the app deletes here, and how the owner takes +/// the phone off the box, since no op lets a phone do that itself. +struct RemovePhoneView: View { + let app: AppModel + /// Closes Source & licenses, which has nothing left to show after. + let close: () -> Void + @State private var confirming = false + + var body: some View { + List { + Section { + Text(AboutWords.text("This deletes what FTW keeps on this phone: the key to your home, the pairing, the saved readings and what the widgets show. Your box and everything on it stay as they are.")) + Text(AboutWords.text("FTW has no account with Sourceful. Your box keeps a row for this phone until you remove it there.")) + .foregroundStyle(Theme.fgDim) + } + .font(.callout) + + if let id = app.vault.deviceIDOnBox { + Section { + LabeledContent(AboutWords.text("Your box calls this phone")) { + Text(PhoneRemoval.nameOnBox(id)) + .font(.body.monospaced()) + .foregroundStyle(Theme.fg) + .textSelection(.enabled) + } + ForEach(Array(PhoneRemoval.boxSteps(id).enumerated()), id: \.offset) { index, step in + Label { + Text(step) + } icon: { + Image(systemName: "\(index + 1).circle") + .foregroundStyle(Theme.accent) + } + } + } header: { + Text(AboutWords.text("On your box")) + } footer: { + Text(AboutWords.text("Once it is gone there, the box no longer lets this phone in. Do it before or after the step below.")) + } + } + + Section { + Button(AboutWords.text("Remove from this phone"), role: .destructive) { confirming = true } + } header: { + Text(AboutWords.text("On this phone")) + } footer: { + Text(AboutWords.text("If you made a passkey for FTW, it stays in the Passwords app, where you can delete it. Keep it if another device of yours still uses FTW.")) + } + } + .scrollContentBackground(.hidden) + .background(Theme.surface) + .navigationTitle(AboutWords.text("Remove this phone")) + .navigationBarTitleDisplayMode(.inline) + .confirmationDialog(AboutWords.text("Remove this phone?"), isPresented: $confirming, titleVisibility: .visible) { + Button(AboutWords.text("Remove"), role: .destructive) { + close() + app.leave() + } + Button(AboutWords.text("Cancel"), role: .cancel) {} + } message: { + Text(AboutWords.text("FTW forgets your home on this phone. To come back, scan a new pairing QR from your box.")) + } + } +} diff --git a/appleApp/FTW/UI/NowView.swift b/appleApp/FTW/UI/NowView.swift index 78c2797..30148b7 100644 --- a/appleApp/FTW/UI/NowView.swift +++ b/appleApp/FTW/UI/NowView.swift @@ -42,6 +42,7 @@ struct NowView: View { home.prices.deactivate() home.savings.deactivate() } + AboutLink(app: app) } /// Paired, and not one reading has ever arrived. A hollow house would diff --git a/appleApp/FTW/UI/PairView.swift b/appleApp/FTW/UI/PairView.swift index 8942377..b1e1c67 100644 --- a/appleApp/FTW/UI/PairView.swift +++ b/appleApp/FTW/UI/PairView.swift @@ -22,6 +22,7 @@ struct PairView: View { ScrollView { VStack(alignment: .leading, spacing: 16) { content + AboutLink(app: app) } .padding(20) .frame(maxWidth: 520, alignment: .leading) diff --git a/appleApp/FTWKit/Sources/FTWKit/Identity/AboutWords.swift b/appleApp/FTWKit/Sources/FTWKit/Identity/AboutWords.swift new file mode 100644 index 0000000..1cdc90a --- /dev/null +++ b/appleApp/FTWKit/Sources/FTWKit/Identity/AboutWords.swift @@ -0,0 +1,87 @@ +import Foundation + +/// The words of Source & licenses and of removing this phone, in English or +/// Swedish as the phone prefers. Keyed by the English, so a missing entry +/// shows English rather than nothing; a test reads the views and fails on +/// any sentence without a Swedish entry. +public enum AboutWords { + public enum Language: String, Sendable { + case en, sv + + /// The phone's choice among the two the app speaks. + public static var preferred: Language { + Bundle.preferredLocalizations(from: ["en", "sv"]).first == "sv" ? .sv : .en + } + } + + @TaskLocal public static var language: Language = .preferred + + public static func text(_ en: String) -> String { + language == .sv ? (sv[en] ?? en) : en + } + + /// `%@` in order, replaced with ready text. + public static func format(_ en: String, _ args: String...) -> String { + var out = text(en) + for arg in args { + guard let r = out.range(of: "%@") else { break } + out.replaceSubrange(r, with: arg) + } + return out + } + + static let sv: [String: String] = [ + // Source & licenses + "Source & licenses": "Källkod och licenser", + "Done": "Klar", + "This version": "Den här versionen", + "Version": "Version", + "Build": "Bygge", + "Commit": "Commit", + "Local build": "Lokalt bygge", + "Source code": "Källkod", + "Source code of this version": "Källkoden till den här versionen", + "Source code on GitHub": "Källkoden på GitHub", + "License at this commit": "Licensen i den här commiten", + "License on GitHub": "Licensen på GitHub", + "FTW is free software. You may run, study, change and share it under the GNU AGPL v3 with the two additional permissions in LICENSE. The link gives the complete source of this exact version, at no charge.": + "FTW är fri programvara. Du får köra, studera, ändra och dela den enligt GNU AGPL v3 med de två extra tillstånden i LICENSE. Länken ger hela källkoden till exakt den här versionen, utan kostnad.", + "Built from uncommitted changes on top of %@. The links go to the repository, which does not hold these changes.": + "Byggd från ändringar som inte är incheckade, ovanpå %@. Länkarna går till repot, som inte har de här ändringarna.", + "Built without a known commit. The links go to the repository, which may not hold this exact code.": + "Byggd utan känd commit. Länkarna går till repot, som kanske inte har exakt den här koden.", + "Licenses": "Licenser", + "License": "Licens", + "Notices": "Notiser", + "Licensing in plain words": "Licensen i klartext", + "Third-party code": "Tredjepartskod", + "None. FTW uses only Apple's own frameworks.": "Ingen. FTW använder bara Apples egna ramverk.", + "© 2026 Sourceful Labs AB and contributors.": "© 2026 Sourceful Labs AB och bidragsgivare.", + "There is no warranty, to the extent the law allows.": "Det finns ingen garanti, så långt lagen tillåter.", + "This text is missing from this build.": "Texten saknas i det här bygget.", + + // Removing this phone + "This phone": "Den här telefonen", + "Remove this phone": "Ta bort den här telefonen", + "This deletes what FTW keeps on this phone: the key to your home, the pairing, the saved readings and what the widgets show. Your box and everything on it stay as they are.": + "Det här raderar det FTW sparar på telefonen: nyckeln till ditt hem, parkopplingen, sparade mätvärden och det widgetarna visar. Boxen och allt på den blir kvar som det är.", + "FTW has no account with Sourceful. Your box keeps a row for this phone until you remove it there.": + "FTW har inget konto hos Sourceful. Boxen har kvar en rad för telefonen tills du tar bort den där.", + "On your box": "På boxen", + "Your box calls this phone": "Boxen kallar telefonen", + "Open your box's local dashboard while on your home network.": "Öppna boxens lokala sida medan du är på hemnätverket.", + "Go to Settings → FTW app.": "Gå till Settings → FTW app.", + "Choose Remove next to %@.": "Välj Remove bredvid %@.", + "Once it is gone there, the box no longer lets this phone in. Do it before or after the step below.": + "När den är borta där släpper boxen inte längre in telefonen. Gör det före eller efter steget nedan.", + "On this phone": "På telefonen", + "Remove from this phone": "Ta bort från telefonen", + "If you made a passkey for FTW, it stays in the Passwords app, where you can delete it. Keep it if another device of yours still uses FTW.": + "Om du skapade en lösennyckel för FTW finns den kvar i appen Lösenord, där du kan radera den. Behåll den om en annan av dina enheter fortfarande använder FTW.", + "Remove this phone?": "Ta bort den här telefonen?", + "FTW forgets your home on this phone. To come back, scan a new pairing QR from your box.": + "FTW glömmer ditt hem på telefonen. För att komma tillbaka skannar du en ny QR-kod för parkoppling från boxen.", + "Remove": "Ta bort", + "Cancel": "Avbryt", + ] +} diff --git a/appleApp/FTWKit/Sources/FTWKit/Identity/BuildSource.swift b/appleApp/FTWKit/Sources/FTWKit/Identity/BuildSource.swift new file mode 100644 index 0000000..d50adcf --- /dev/null +++ b/appleApp/FTWKit/Sources/FTWKit/Identity/BuildSource.swift @@ -0,0 +1,93 @@ +import Foundation + +/// What this build was made from, and where its source is. +/// +/// The AGPL and the app store permission in LICENSE ask that the license and +/// an offer of the exact version's source be reachable in the app. The build +/// writes its commit into the bundle (the "Record the source revision" phase +/// in the Xcode project), and this turns that record into the links and the +/// words Source & licenses shows. +public struct BuildSource: Equatable, Sendable { + public enum Revision: Equatable, Sendable { + /// A clean tree at a known commit. The links pin it. + case commit(String) + /// Uncommitted changes on top of `base`, or no commit known at all. + /// The links go to the repository, which may not hold this code. + case local(base: String?) + } + + public let version: String + public let build: String + public let revision: Revision + + /// What the build phase wrote: 40 hex digits for a clean tree, the same + /// with `-dirty` for uncommitted changes, and nothing when no commit was + /// known. Anything else is a local build too. + public init(version: String, build: String, recorded: String?) { + self.version = version + self.build = build + let text = (recorded ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + if Self.isCommit(text) { + revision = .commit(text) + } else if text.hasSuffix(Self.dirtySuffix), Self.isCommit(String(text.dropLast(Self.dirtySuffix.count))) { + revision = .local(base: String(text.dropLast(Self.dirtySuffix.count))) + } else { + revision = .local(base: nil) + } + } + + static let dirtySuffix = "-dirty" + + static func isCommit(_ text: String) -> Bool { + text.count == 40 && text.allSatisfy { $0.isHexDigit && !$0.isUppercase } + } + + public var commit: String? { + if case .commit(let sha) = revision { return sha } + return nil + } + + public var isLocal: Bool { commit == nil } + + /// Seven characters, as GitHub shows a commit. + public static func short(_ sha: String) -> String { String(sha.prefix(7)) } + + /// The tree at this commit, whose Code menu offers the archive. A local + /// build gets the repository. + public var sourceURL: URL { + guard let commit else { return Origin.sourceURL } + return Origin.sourceURL.appending(path: "tree/\(commit)") + } + + /// The license as it stood at this commit, not on the moving branch. + public var licenseURL: URL { + Origin.sourceURL.appending(path: "blob/\(commit ?? "main")/LICENSE") + } + + /// Where a relative link in LICENSING.md points, at the same revision. + public var documentBaseURL: URL { + Origin.sourceURL.appending(path: "blob/\(commit ?? "main")/") + } + + /// "0.1.0 (1)", as Settings shows an app's version. + public var versionText: String { "\(version) (\(build))" } + + /// The commit as the screen names it. + public var revisionText: String { + switch revision { + case .commit(let sha): return Self.short(sha) + case .local: return AboutWords.text("Local build") + } + } + + /// Why the links do not pin this build, or nil when they do. + public var localNote: String? { + switch revision { + case .commit: return nil + case .local(let base?): + return AboutWords.format("Built from uncommitted changes on top of %@. The links go to the repository, which does not hold these changes.", Self.short(base)) + case .local(nil): + return AboutWords.text("Built without a known commit. The links go to the repository, which may not hold this exact code.") + } + } +} diff --git a/appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift b/appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift new file mode 100644 index 0000000..d06bf7e --- /dev/null +++ b/appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift @@ -0,0 +1,143 @@ +import Foundation + +/// The license files as a phone reads them. +/// +/// The files are wrapped at about 72 columns for a terminal. Shown as they +/// are, every line breaks twice on a phone: once where the file breaks it and +/// once where the screen does. This joins the lines of a paragraph and keeps +/// every word in order; it changes line breaks, never text. +public enum LicenseText { + /// The three files the build copies into the app from the repository + /// root. Nothing here is a copy that could drift from the original. + public enum Document: String, CaseIterable, Sendable, Identifiable { + case license = "LICENSE" + case notice = "NOTICE" + case licensing = "LICENSING.md" + + public var id: String { rawValue } + + public var isMarkdown: Bool { self == .licensing } + + public var title: String { + switch self { + case .license: return AboutWords.text("License") + case .notice: return AboutWords.text("Notices") + case .licensing: return AboutWords.text("Licensing in plain words") + } + } + } + + public struct Block: Equatable, Sendable, Identifiable { + public enum Kind: Equatable, Sendable { + /// 1 is a document's title, 2 a section. + case heading(Int) + case paragraph + /// A line of dashes or equals signs between parts. + case rule + } + + public let id: Int + public let kind: Kind + /// Lines that start a list item or end short in the file keep their + /// break as "\n". Every other break is a space. + public let text: String + } + + public static func blocks(_ text: String, markdown: Bool) -> [Block] { + let lines = text.replacingOccurrences(of: "\r\n", with: "\n").components(separatedBy: "\n") + // The column the file wraps at, so a line that ends well short of it + // is known to end there on purpose. + let width = min(80, lines.map(\.count).max() ?? 0) + var out: [Block] = [] + var paragraph: [String] = [] + + func add(_ kind: Block.Kind, _ text: String) { + out.append(Block(id: out.count, kind: kind, text: text)) + } + + func flush() { + guard !paragraph.isEmpty else { return } + defer { paragraph = [] } + if !markdown, paragraph.count == 1, let level = plainHeading(paragraph[0]) { + add(.heading(level), paragraph[0].trimmingCharacters(in: .whitespaces)) + return + } + add(.paragraph, join(paragraph, width: width)) + } + + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty { + flush() + } else if isRule(trimmed) { + flush() + add(.rule, "") + } else if markdown, let (level, title) = markdownHeading(trimmed) { + flush() + add(.heading(level), title) + } else { + paragraph.append(line) + } + } + flush() + return out + } + + /// A Markdown paragraph with its links made absolute against `base`, so + /// LICENSING.md's `[LICENSE](LICENSE)` opens the file at the build's + /// commit rather than nowhere. Plain text when it does not parse. + public static func inline(_ text: String, base: URL) -> AttributedString { + guard var out = try? AttributedString(markdown: text, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace), baseURL: base) else { + return AttributedString(text) + } + for run in out.runs { + if let link = run.link { + out[run.range].link = link.absoluteURL + } + } + return out + } + + /// Joins a paragraph's lines. A line keeps its break when the next one + /// starts a list item, or when it ends well short of the wrap column, + /// as an address or a title over a line does. + static func join(_ lines: [String], width: Int) -> String { + var out = "" + for (i, raw) in lines.enumerated() { + let line = raw.trimmingCharacters(in: .whitespaces) + if i > 0 { + let short = lines[i - 1].count < width - 20 + out += startsItem(line) || short ? "\n" : " " + } + out += line + } + return out + } + + static func startsItem(_ line: String) -> Bool { + line.hasPrefix("- ") || line.hasPrefix("* ") || line.hasPrefix("• ") + || line.range(of: #"^(\(?[a-z0-9]{1,3}\)|[0-9]{1,2}\.) "#, options: .regularExpression) != nil + } + + static func isRule(_ line: String) -> Bool { + line.count >= 3 && Set(line).count == 1 && (line.first == "-" || line.first == "=" || line.first == "_") + } + + static func markdownHeading(_ line: String) -> (Int, String)? { + let hashes = line.prefix(while: { $0 == "#" }).count + guard (1...6).contains(hashes), line.dropFirst(hashes).first == " " else { return nil } + return (min(hashes, 2), String(line.dropFirst(hashes + 1))) + } + + /// In plain text a title is a line on its own: set in capitals, centred + /// with spaces, or a numbered section such as "0. Definitions." + static func plainHeading(_ line: String) -> Int? { + let trimmed = line.trimmingCharacters(in: .whitespaces) + guard trimmed.count <= 60 else { return nil } + let letters = trimmed.filter(\.isLetter) + if !letters.isEmpty, letters == letters.uppercased(), letters.count >= 4 { return 1 } + if line.prefix(while: { $0 == " " }).count >= 8 { return 1 } + if trimmed.range(of: #"^[0-9]{1,2}\. [A-Z][^.]*\.$"#, options: .regularExpression) != nil { return 2 } + return nil + } +} diff --git a/appleApp/FTWKit/Sources/FTWKit/Identity/Origin.swift b/appleApp/FTWKit/Sources/FTWKit/Identity/Origin.swift index 8afd8bc..83df598 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Identity/Origin.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Identity/Origin.swift @@ -15,7 +15,8 @@ public enum Origin { /// The relay. One value, no setting. public static let relayURL = URL(string: "wss://relay.ftw.energy")! - /// Source and licence, as the AGPL asks a network client to offer them. + /// The repository. A fork changes this line, as LICENSING.md asks: the + /// links in Source & licenses must lead to the fork's own source. + /// `BuildSource` pins them to the build's commit. public static let sourceURL = URL(string: "https://github.com/srcfl/ftw-app")! - public static let licenseURL = URL(string: "https://github.com/srcfl/ftw-app/blob/main/LICENSE")! } diff --git a/appleApp/FTWKit/Sources/FTWKit/Identity/PhoneRemoval.swift b/appleApp/FTWKit/Sources/FTWKit/Identity/PhoneRemoval.swift new file mode 100644 index 0000000..de7b472 --- /dev/null +++ b/appleApp/FTWKit/Sources/FTWKit/Identity/PhoneRemoval.swift @@ -0,0 +1,27 @@ +import Foundation + +/// Removing this phone, as far as the phone can reach. +/// +/// The registry has no op that lets a phone take itself off its box, so the +/// app does what is in its own hands, `AppModel.leave`: the key, the pairing, +/// the sealed readings and the widgets' records go. It then says how the +/// owner removes the phone on the box. That is also the one way that always +/// works: over a session the box refuses to remove its last owner, and on +/// its own page it allows it. +/// +/// FTW creates no account. A pairing is a key on this phone and a row on the +/// household's own box, and this is where both are dealt with. +public enum PhoneRemoval { + /// The row on the box's Settings → FTW app page, which names each phone + /// by the first eight characters of its key (`Vault.deviceIDOnBox`). + /// The box's page is in English, so the name is too. + public static func nameOnBox(_ deviceID: String) -> String { "Phone \(deviceID)" } + + public static func boxSteps(_ deviceID: String) -> [String] { + [ + AboutWords.text("Open your box's local dashboard while on your home network."), + AboutWords.text("Go to Settings → FTW app."), + AboutWords.format("Choose Remove next to %@.", nameOnBox(deviceID)), + ] + } +} diff --git a/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift b/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift new file mode 100644 index 0000000..581d177 --- /dev/null +++ b/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift @@ -0,0 +1,189 @@ +import Foundation +import Testing +@testable import FTWKit + +/// Source & licenses and removing this phone: what the license asks the app +/// to offer, and the words it uses to offer it. +@Suite struct AboutTests { + static let sha = "5a14ff0960d99fa0ca982c82fb2b7c1d5e213d25" + + // MARK: The build's source + + @Test func aCleanCommitPinsBothLinks() { + let source = BuildSource(version: "0.1.0", build: "7", recorded: Self.sha + "\n") + #expect(source.revision == .commit(Self.sha)) + #expect(source.sourceURL.absoluteString == "https://github.com/srcfl/ftw-app/tree/\(Self.sha)") + #expect(source.licenseURL.absoluteString == "https://github.com/srcfl/ftw-app/blob/\(Self.sha)/LICENSE") + #expect(source.documentBaseURL.absoluteString == "https://github.com/srcfl/ftw-app/blob/\(Self.sha)/") + #expect(source.versionText == "0.1.0 (7)") + #expect(source.revisionText == "5a14ff0") + #expect(source.localNote == nil) + } + + @Test func uncommittedChangesAreALocalBuildOnTheirBase() { + let source = BuildSource(version: "0.1.0", build: "1", recorded: Self.sha + "-dirty") + #expect(source.revision == .local(base: Self.sha)) + #expect(source.isLocal) + #expect(source.sourceURL == Origin.sourceURL) + #expect(source.licenseURL.absoluteString == "https://github.com/srcfl/ftw-app/blob/main/LICENSE") + AboutWords.$language.withValue(.en) { + #expect(source.revisionText == "Local build") + #expect(source.localNote?.contains("5a14ff0") == true) + } + AboutWords.$language.withValue(.sv) { + #expect(source.revisionText == "Lokalt bygge") + } + } + + /// Only 40 lower-case hex digits name a commit. Anything else, including + /// nothing at all, links to the repository and says so. + @Test(arguments: [nil, "", "dev", "5a14ff0", Self.sha.uppercased(), Self.sha + "0", "g" + Self.sha.dropFirst(), "-dirty"]) + func anythingElseIsALocalBuild(_ recorded: String?) { + let source = BuildSource(version: "0.1.0", build: "1", recorded: recorded) + #expect(source.revision == .local(base: nil)) + #expect(source.sourceURL == Origin.sourceURL) + #expect(source.localNote != nil) + } + + // MARK: The license files + + /// Joining lines must never change the text: every word of every file, + /// in order, and nothing else. + @Test(arguments: LicenseText.Document.allCases) + func reflowKeepsEveryWord(_ document: LicenseText.Document) throws { + let text = try RepoFiles.read(document.rawValue) + let blocks = LicenseText.blocks(text, markdown: document.isMarkdown) + let original = text.split(whereSeparator: \.isNewline) + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !LicenseText.isRule($0) } + .map { document.isMarkdown ? $0.replacingOccurrences(of: #"^#{1,6} "#, with: "", options: .regularExpression) : $0 } + .flatMap { $0.split(whereSeparator: \.isWhitespace) } + let shown = blocks.flatMap { $0.text.split(whereSeparator: \.isWhitespace) } + #expect(shown == original) + #expect(!blocks.isEmpty) + } + + @Test func theLicenseReadsAsParagraphsAndSections() throws { + let blocks = LicenseText.blocks(try RepoFiles.read("LICENSE"), markdown: false) + // The heading over the file stays two lines. + #expect(blocks.first?.text == "FTW native app\nCopyright 2026 Sourceful Labs AB and contributors.") + // A wrapped paragraph becomes one line the screen wraps itself. + let opening = try #require(blocks.first { $0.text.hasPrefix("This version of the project, as a whole") }) + #expect(!opening.text.contains("\n")) + #expect(blocks.contains { $0.kind == .heading(2) && $0.text == "0. Definitions." }) + #expect(blocks.contains { $0.kind == .heading(1) && $0.text == "TERMS AND CONDITIONS" }) + #expect(blocks.contains { $0.kind == .rule }) + // Both of the license's own permissions are there. + #expect(blocks.contains { $0.text.contains("FTW app store distribution permission") } || blocks.contains { $0.text.contains("Energyplan") }) + // A list keeps one item to a line. + let items = blocks.filter { $0.text.contains("\n- ") } + for block in items { + for line in block.text.split(separator: "\n").dropFirst() { + #expect(!line.hasPrefix(" ")) + } + } + } + + @Test func licensingMarkdownHasItsHeadings() throws { + let blocks = LicenseText.blocks(try RepoFiles.read("LICENSING.md"), markdown: true) + #expect(blocks.first == LicenseText.Block(id: 0, kind: .heading(1), text: "FTW native app licensing")) + #expect(blocks.contains { $0.kind == .heading(2) && $0.text == "Source for a distributed version" }) + #expect(!blocks.contains { $0.text.hasPrefix("#") }) + } + + @Test func relativeLinksOpenTheFileAtTheBuildsCommit() throws { + let source = BuildSource(version: "0.1.0", build: "1", recorded: Self.sha) + let text = LicenseText.inline("See [LICENSE](LICENSE) and [the vision](https://github.com/srcfl/ftw/blob/master/VISION.md).", base: source.documentBaseURL) + let links = text.runs.compactMap(\.link).map(\.absoluteString) + #expect(links == ["https://github.com/srcfl/ftw-app/blob/\(Self.sha)/LICENSE", "https://github.com/srcfl/ftw/blob/master/VISION.md"]) + #expect(String(text.characters) == "See LICENSE and the vision.") + } + + @Test func listsAndShortLinesKeepTheirBreaks() { + let text = """ + All other conditions of the GNU AGPL remain in force. In particular: + + - Offer the Corresponding Source of the exact version you convey, under the + GNU AGPL and at no charge, outside the service and free of its terms. + - Make the license text and that source offer reachable from within the + program. + """ + let blocks = LicenseText.blocks(text, markdown: false) + #expect(blocks.count == 2) + #expect(blocks[1].text == "- Offer the Corresponding Source of the exact version you convey, under the GNU AGPL and at no charge, outside the service and free of its terms.\n- Make the license text and that source offer reachable from within the program.") + } + + // MARK: Third-party code + + /// Source & licenses says FTW carries no third-party code. That stays + /// true only while neither the package nor the project pulls any in; add + /// one and this fails until the screen lists it with its license. + @Test func thereIsNoThirdPartyCode() throws { + let package = try RepoFiles.read("appleApp/FTWKit/Package.swift") + #expect(!package.contains(".package(")) + let project = try RepoFiles.read("appleApp/FTW.xcodeproj/project.pbxproj") + #expect(!project.contains("XCRemoteSwiftPackageReference")) + } + + // MARK: Removing this phone + + @Test func theBoxNamesThePhoneByItsKey() throws { + let vectors = try IdentityVectors.load() + let name = PhoneRemoval.nameOnBox(vectors.enrollment.deviceIdOnBox) + #expect(name == "Phone \(vectors.enrollment.deviceIdOnBox)") + AboutWords.$language.withValue(.en) { + #expect(PhoneRemoval.boxSteps("abcd1234").last == "Choose Remove next to Phone abcd1234.") + } + AboutWords.$language.withValue(.sv) { + #expect(PhoneRemoval.boxSteps("abcd1234").last == "Välj Remove bredvid Phone abcd1234.") + } + } + + @MainActor + @Test func theNameOnTheBoxIsTheVaultsOwn() async throws { + let store = MemoryStore() + let vault = Vault(store: store) + #expect(vault.deviceIDOnBox == nil) + _ = try vault.deviceKey(vault.localWrappingKey()) + let id = try #require(vault.deviceIDOnBox) + #expect(id.count == Vault.boxDeviceIDChars) + #expect(PhoneRemoval.nameOnBox(id).hasSuffix(id)) + } + + // MARK: Words + + /// Every sentence the About screens hand to `AboutWords` has a Swedish + /// entry. Read from the source, so a new sentence cannot slip through. + @Test func everySentenceHasSwedish() throws { + let files = [ + "appleApp/FTWKit/Sources/FTWKit/Identity/BuildSource.swift", + "appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift", + "appleApp/FTWKit/Sources/FTWKit/Identity/PhoneRemoval.swift", + ] + (try RepoFiles.list("appleApp/FTW/About")) + let pattern = #/AboutWords\.(?:text|format)\("((?:[^"\\]|\\.)*)"/# + var seen = 0 + for file in files { + let source = try RepoFiles.read(file) + for match in source.matches(of: pattern) { + let english = String(match.1).replacingOccurrences(of: "\\\"", with: "\"") + seen += 1 + #expect(AboutWords.sv[english] != nil, "no Swedish for \"\(english)\" in \(file)") + } + } + #expect(seen > 20) + for (english, swedish) in AboutWords.sv { + #expect(!swedish.isEmpty) + #expect(english.components(separatedBy: "%@").count == swedish.components(separatedBy: "%@").count, "\(english)") + } + } +} + +extension RepoFiles { + /// The Swift files in a folder of the repository, as paths from its root. + static func list(_ folder: String) throws -> [String] { + try FileManager.default.contentsOfDirectory(atPath: root.appendingPathComponent(folder).path) + .filter { $0.hasSuffix(".swift") } + .sorted() + .map { "\(folder)/\($0)" } + } +} From 352e0a4adf04f484889a134f657d6a45dc5d5731 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 12 Oct 2026 18:41:21 +0200 Subject: [PATCH 2/5] Give the app an iOS 26 icon and a launch screen that matches its first frame The icon is an Icon Composer document made from FTW's mark in ftw-webapp/public/icons/icon.svg: the amber bolt as a glass layer on the mark's near-black. iOS draws the light, dark, tinted and clear looks from it. The 1024 image and the macOS sizes left over from #5 are gone. The launch screen fills with the app's surface colour, light and dark, so nothing changes colour between the launch screen and the first frame. Co-Authored-By: Claude Opus 5.5 --- appleApp/FTW-Info.plist | 5 ++ appleApp/FTW/AppIcon.icon/Assets/bolt.svg | 4 + appleApp/FTW/AppIcon.icon/icon.json | 45 +++++++++++ .../AppIcon.appiconset/Contents.json | 74 ------------------ .../AppIcon.appiconset/icon-ios-1024.png | Bin 11194 -> 0 bytes .../AppIcon.appiconset/icon-mac-128@1x.png | Bin 3895 -> 0 bytes .../AppIcon.appiconset/icon-mac-128@2x.png | Bin 5938 -> 0 bytes .../AppIcon.appiconset/icon-mac-16@1x.png | Bin 553 -> 0 bytes .../AppIcon.appiconset/icon-mac-16@2x.png | Bin 1080 -> 0 bytes .../AppIcon.appiconset/icon-mac-256@1x.png | Bin 5938 -> 0 bytes .../AppIcon.appiconset/icon-mac-256@2x.png | Bin 11672 -> 0 bytes .../AppIcon.appiconset/icon-mac-32@1x.png | Bin 1080 -> 0 bytes .../AppIcon.appiconset/icon-mac-32@2x.png | Bin 2044 -> 0 bytes .../AppIcon.appiconset/icon-mac-512@1x.png | Bin 11672 -> 0 bytes .../AppIcon.appiconset/icon-mac-512@2x.png | Bin 31447 -> 0 bytes .../LaunchBackground.colorset/Contents.json | 38 +++++++++ 16 files changed, 92 insertions(+), 74 deletions(-) create mode 100644 appleApp/FTW/AppIcon.icon/Assets/bolt.svg create mode 100644 appleApp/FTW/AppIcon.icon/icon.json delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-ios-1024.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-128@1x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-128@2x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-16@1x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-16@2x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-256@1x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-256@2x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-32@1x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-32@2x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-512@1x.png delete mode 100644 appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-512@2x.png create mode 100644 appleApp/FTW/Assets.xcassets/LaunchBackground.colorset/Contents.json diff --git a/appleApp/FTW-Info.plist b/appleApp/FTW-Info.plist index 03bab63..e20acc0 100644 --- a/appleApp/FTW-Info.plist +++ b/appleApp/FTW-Info.plist @@ -9,5 +9,10 @@ en sv + UILaunchScreen + + UIColorName + LaunchBackground + diff --git a/appleApp/FTW/AppIcon.icon/Assets/bolt.svg b/appleApp/FTW/AppIcon.icon/Assets/bolt.svg new file mode 100644 index 0000000..54e730e --- /dev/null +++ b/appleApp/FTW/AppIcon.icon/Assets/bolt.svg @@ -0,0 +1,4 @@ + + + + diff --git a/appleApp/FTW/AppIcon.icon/icon.json b/appleApp/FTW/AppIcon.icon/icon.json new file mode 100644 index 0000000..d018b86 --- /dev/null +++ b/appleApp/FTW/AppIcon.icon/icon.json @@ -0,0 +1,45 @@ +{ + "fill-specializations" : [ + { + "value" : { + "solid" : "srgb:0.05098,0.05098,0.05098,1.00000" + } + }, + { + "appearance" : "dark", + "value" : { + "solid" : "srgb:0.00000,0.00000,0.00000,1.00000" + } + } + ], + "groups" : [ + { + "layers" : [ + { + "glass" : true, + "image-name" : "bolt.svg", + "name" : "bolt", + "position" : { + "scale" : 1.1, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "specular" : true, + "translucency" : { + "enabled" : true, + "value" : 0.2 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} diff --git a/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/Contents.json b/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 592f5b9..0000000 --- a/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "images": [ - { - "filename": "icon-ios-1024.png", - "idiom": "universal", - "platform": "ios", - "size": "1024x1024" - }, - { - "filename": "icon-mac-16@1x.png", - "idiom": "mac", - "scale": "1x", - "size": "16x16" - }, - { - "filename": "icon-mac-16@2x.png", - "idiom": "mac", - "scale": "2x", - "size": "16x16" - }, - { - "filename": "icon-mac-32@1x.png", - "idiom": "mac", - "scale": "1x", - "size": "32x32" - }, - { - "filename": "icon-mac-32@2x.png", - "idiom": "mac", - "scale": "2x", - "size": "32x32" - }, - { - "filename": "icon-mac-128@1x.png", - "idiom": "mac", - "scale": "1x", - "size": "128x128" - }, - { - "filename": "icon-mac-128@2x.png", - "idiom": "mac", - "scale": "2x", - "size": "128x128" - }, - { - "filename": "icon-mac-256@1x.png", - "idiom": "mac", - "scale": "1x", - "size": "256x256" - }, - { - "filename": "icon-mac-256@2x.png", - "idiom": "mac", - "scale": "2x", - "size": "256x256" - }, - { - "filename": "icon-mac-512@1x.png", - "idiom": "mac", - "scale": "1x", - "size": "512x512" - }, - { - "filename": "icon-mac-512@2x.png", - "idiom": "mac", - "scale": "2x", - "size": "512x512" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-ios-1024.png b/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-ios-1024.png deleted file mode 100644 index 91a152afe650065ad54ba85c8123bc094cc7b8ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11194 zcmeHN`CrZ1|39})C6#1}C=}U}t!cr%Sz9c3$da*SOALt+l6z6I3@*i#wu_=M#E_`8 zC_~n=#0MWGQ&gl3?YH~>KF>M#@tDWs`!9Uv7jt`^*YbS5&TBd6yl203#?(%RJq-yV zojlwp%_2kxKXr({7XDK&yZM}uA-g;#xp{Amt9?7OaAi|y+1H>ip;JbkdE%es>odXQ z_uC%#tc*6l*3$}4T9mX)(rK7a_>0BK2iF=JCyhQh_q$12ShuH(GX485x?MV2ceelP zzKgHDdwSy7?(W0V&6oE7>6)$2Q-8mNkEPH4jCNh+mmqE`4SHNx*3__|=BS_X*s7+u zqU?`3iR#AMH@yilEg0OS3uM(j@Q>*}5&qrv@6UhZ@NW|QFBgON@*W$Ph$QVsgbXeY zep|fx<%?_s;&4<%B-Jl|SGQTM-&uYxK*+GlYWUh-d%h^tjf@#Zh~3*)TW);&+*-Gz zv1ENtn{W1)n~(~J)RP%6+9Ryu)a|+I0;L|W1J2~j%5)&_pMQekYmtTpy&}K5;sqJD>PHW6ev2085)tan;5?Sp}R>8dttmPKy zzUCE6n}uhInGM&4n%`rOIDVQ_))ac(BB$}u5nDO&qY;auA?cndt8H-|__jK|F016c zYj*B&q7{7*K^e5uR4x2DQ&pK;|NcY4)rL%~y{Su@g2iJaH4L5mgbTulp9`uR%l}dr zIBXi2tz(+^3%2_}y`4(<`%o=*wJtNYmkSWZI8PT9uD4Xnn(K=SWaEv@BxDpeo06== zRsBUe4z38$!vrJYQlIRn!!>`$B^V2JIW{7l%Q{%SiB)myZWz&mkt^F=RATjc{AFgD zLMk={G|Q%G>H8Y!5wb0=m)Z&_=kJCadfmf?yEbxi!j}-mt;yv=Z@>vhfReU&G|Q$! zq%TA~7MXF8eWSX{Z`;FA<8Tim8d^Yzb^L<@VL1SI96^YC#r=9A8b&7of?38$RW7h! zwgfu!MgyVF9U^xj5++4zd?x<@^7CILlnWz(pb`j%uDisQZG^IJX)+Gy zN5JuNw&R-ZpgH-o6~`78*;PK-(p>Y0h~)J%HWZFfm`EGnoe_JPquo{#;&8QpzTgVX zUI=pZ+gs(rR3RCt^I_=2rb#?htBvOozo3PhKVbE$v%N|@0RD`I)oqr&xuKIVH1;qy z0?yeWhz@%O@gV9z+1ZmkZti(4hIyOrMhI4IvuUtbf$!dJ=AA?fs_(}3VcOjD2zj^l zSpfsL8Zoe3eWG4CtYa$a39hLpHCzRqJLZk>GhogoAyoq}XYd#@k);V-F%^l38Nni@ z_ysUUA3AZE%ON7S;Cpb-X{G2tFK_a&uK?!8G3%Mn))sIx`=Q3<9ROS76($4&{;ZLN z{O7I;QIn+s+635{H@_c?jpEpzZt@31`6jsEgV4y1Ji(~YjpB{mP896ery;gTFzXSp z^KDp9b=gs(<^B*NmIwJ(nFYPj3QW~45RSBu!Dg+tAY|<7YVO=J7(L=)%A-bwMi}rd zt)^VZ25_$v;B|j2(EO}+4iGi(8qEQG2Y^ibrQFeR01#ouVLnAhxN|Ip`GoAV-@rk3 z)F#%MQCxUv+zoQd)|iEV1;QV8!ihVNHf%btl>_XiMxpbvI$}z6AKXK zN?CkQ!-dF|3~^?#r1~%t5FWwO)(~6eeW9Mmx^LVqGG_>SBrr0rxs?&DKQOM}4rsU# zl)-a4vuN2xh=??C0e7*|fsmTnlQ|U2!%OA&pYsi6T#*NOgc#SX=^8G$HKxXpxn=$* z08iY^BYzrzH>V8YP|OF8km#Q8$lPLF5^&LuNV$@KAEDubTmHxQGPgWY&E~1-yR=QL z2H=fzda_-#J5;bF56lkX`yc^epEV{N zO5rvm>B&rlxdy$~*Kpy0GPxIbYb3H@vOh0G=>P!MUl+zAe80k7ukca~ zhY|z84;S!09ff{Tm>U1jLR`;|JX}cB!n4`j#Xlgi77Mhqn?Lx?NHH~pM`#%gZcFRV zeLo>03b)9A7Jj?Ni`p&*3yWhp#AA@~zyi&AyO=!|*719}hqlPar>l8YOGl#gkEZ{# zV7A(oCzlfz!awlHg+rp-%N;x>fsg&3v{bmEQ`p&eO)-jL>$O+rQE5%f!#}=H(EQoaMhnXH5iE!WY~k{Yzjua z!#OO_3>xfoO61qYfoG7eHt}^7ai``WnLl(M_0NLdQts3>EckUE&Amkj^gVx>t6Jcc zBpR?EhZ6)Y1zpdzla%rNQZGD1FB^>ZXE~)=FhCREjJ{@UtrAAVpG-K~!Bj0f3H6ICOGulU zTKp^h4O7|r!jef^`bxCayo4~JXdtS*yEaBxzR(?GCgm_p>6Qy#)D%u6H+I(xwlH=6 zuQa_IFk4A~qplXh>_(U^fmwSSmCP1qYhdY>ZiG3AYt7NA1 zxARremN3D@8fpfifG6($^^a>MUB znx3o|Ho!~`%xo|atfm9)Vm)Gt5V*j!mg$|bd zY7I*_u+Y$gMLMor(1G%k7&)7}sAoz*wguU>T=)}`X{dm~15}B7Vbja9LUJ)oh@>`g z*?R8TEVh6{C*bq(Fkv==NXgzFoO}o5Pz@PJI`1jXVp`gFoF43|(H92N2=&k=Df{bX z+kxuP2qIsjFRVgJGV)$dt_9geL+%6aJ@v9~;I7>er`^?5T{IbB#{sNMx$p}O5bB_^ zi@IovhKv&wC7)aYvYEQb3uF&)+m{PBDY;@&2SZy`Vmw@nL^!4S!Ydjpw02J$)e=gB~oiKmVsN5P&TK~HK#rWKttF?jY8(e)NmsY*_RQfC^FN1P?nj^G_#Fn+I zejc8^WBIrC*~5iZX$umSdhWzw{V7pcL;^*3IOU78FPYhny@Hoq4XSLPN z+IW-4sqxJ!y?UEE%;~KoRdr{tlHdf_>K#@!S1dAJU#d&3RQD2hd{iF{PTjVp<5t`o z%*$82&21~K7B^oLH_(e}V`=sNm%;;w8=)8O`?+3NAHSnAcSlo``dM&7Zs_|jR!%s6 zRSZ-u8M?l}dqt1T5)auH*|%8X!=t5Db3-q^)roshCnBEKDwEmkt{2q2ij+0h7(cz`K!pL=jmp zez>fvtv0DkN*ZQ)ouMh7F9(l)&i1m@k&BCn?t8yD&%@cpC{#f)S zU~YnXSHZgUg4T@4OFJ4u0H!pfzslq@8u>_5bwu^vQ=eY8S7$%!zEgNqoBqvWyYEc+ z6kac$W(zyun$-ndVZyF6$V41w9XgiF-uZ(i3M>OvCiK0eT3;yGlrQ8>0{O6pjEvu} zA#VbCHzz;Dn0pGdzd@l-50dvjOjzntF7w6cxjeL777e=&7(Me(XxP#ByVG}*@xZ&dq3Z%~ta< z*dBT`Tb=Eq?m%z4L1x_3$rdDUtp?a?u$yppdf)t@(QW|jMx4dElQxO=Y_{_2t1_YY zOBkmZB}fg2{Tx>kf)UAsA@S5BfnzlaaTPnP@eVh!yeCjNxLjt9D;7pRi!Qw}b-)Jr z=nL93Ubi$h5hJHxG%Qda(U~L1h2nVQgIP&h6Xn5|M?| zIr1ntmpXt)Ivk-JqJirSjfPyK)L5Hf-ZNbEw2c|hHYOPz(hKZVJC4-vcKlT@#1yIX zf5aYZ#aHAI<)q(p+M3mkmEBX@pXGji{oz)6FPElaB|Y}0){RS@vu{JQ@OT({8_r*G zb$CYlm>5!9`7)t4NA`ML*5goT%VkxW9p3!6yW90wMxW3e-idd%Jy_CSissPrdAM{R z{dACVNo4mJ??#{0!_^f*6JIvPXPsE{Jv4Z`UL@EK$f3+TPS9E_UtwMAx&tH!jnmWhj+g!K!ZaqL&Tv2GWBK=gY=AwY4PX1but4 zJ;Y_Jsoj~SXf_Eq)x&euciuGJRGF|?e9s>?b<#>&#kKVx#hZWLyzW|t?1c}B!N%Es zd)E7g!er>&b9Gs@e$D&m1#B8(61O5~MswK@+IIVm>?E#xYaQY)?EDb1e#=iYg;l6e zdf@fH`zk#vD5&Ag@I`fx^>Q~jqV$wIaE0jKn&bM$DDg(<-!&%Vp2c?x5U%!!5N5vU zb6d+3*)V!pNmEl%y14b;r=s!KOSkLTMLhDGUpBtw>AcdGzvtyPcx^{4@Q2Dn@viBj&vEwC7f)Fl%*#t$~_X~S`9Zfi3n0| zO4bHB(}UO|uxaQKDY2Z4BMc=cqjEV2D$N!=^&=7$ip+C@YZw7jf`}WSS}VZz=Zg zqK*6npo~WSrySQQo&cNaILTf+Mi%UYa!0QGDwLmGMR3Z|^4|jG^r{a@KLlu!FKl{n zvMI>NZR8)&u5Y7TQ1W7MTXJsNu)=UBpyVka+iAE#o&?~}r)v4rJ)sxVP$&Ue2N^`k z-9Vnkxd)i3JLJQMn*O5EXt}1?aH*AnazAhv!sGkyWI6@q2QyQ&{QIGW;kKt7X(pKs z=Ml_?qr~N8y;u%03j^gD#1bPxve`y{7*VKKfZTyMu}FxNy*kBiVS?x}vfO*3Ruv9G z4lTs-jH<7M`bI?jxtms%C)Bq&AVSunP0mn14%^vPK{Ag)eFD@Qx|2niIKDEd*R_$` zBjW;1x%%#%)g7F0o0EzRl(-&gTO#>W&{uJH!LNUjw%5L&q<^9s4m~uP_Ne?(Om+2$U zf#PXQc_5T`e(g?`(+8QtONW?bS4gTj+4~kH?_%U(RH1h~RY;#P6iaFu6goRv=5R7T z8Yz4@nZ8`aXvjzog-Bb32F`JXOgY2v!BWDz| z4>4}s&%u41aU)9=vllTk?$F?7WPda*#dJ<~1o@RW3xxk(N}kNP{V4aGqf`kU7mDe_ z7@1CySKgeA+oxh`S4M6Gd7FlOgpykr8TUN$DvFhW|8Yt#XXLpcZ_|*GkP0_W-V3sY zhK!n1xc$MnKQQhXh6Jzq3bz4|FfBu% z<)9bqH}7bZF~p-Bs`4`;Jb&RyhK0_IZg_~HPS82A(v}s`Wij3leKhR!h4aNz2EZ5q zh6ZFX(Fjbl6W9EirHQcgV1YeT(-~@Xhq1zVr;lg0-6vSl2jat`uUkj9nNBEy%NJ}J z8cVo6zu1haKTFY!4q+MTPrA1%Wnx0)g7hIl+&zl^cdX>riWg&{Z%1TE89T3*IyPv4zWxhVX0xH^c5 zo1 zlZ(R)?&iR#ZY+G3!5hMPD!fLKjy zuoG=^T$p8#kUoK#->_J|DQo9k2%d&u;|kGTJ&Q zjmK*f2kl^i&Or9|*b%O8i F{{tiiD*ONd diff --git a/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-128@1x.png b/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-128@1x.png deleted file mode 100644 index e548c7ee15d7cc0389792482075b88bfd10ac926..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3895 zcmV-756JL|P) zdyFL2b%(!mZ*|Y3yJy&ChFSK(4hU8luR#P%lsLl1!NGQd!Aekwl7I~)Q5-pzu}z%F zRo`P>^FOL%`=^ja6zO8_Q1*NV}%Z{I#}@ZiC5YwZ^2+=aj< z5m8l5v^R*aQB@npm}h~5g+k$_si~=2(`HBAuQmHV>u`KSbRmU8;S6KUuLD;DmjK&D z#IEso*wCmdz#G8xz*D~O|Day4zZLy=+&!+DM_5Y%SPmI}w=w2(z||r$1}hg1>+JG2 z7;#G%kx zk=VEjcuit>6!>bnTz)3n&d}w?((RkS@&${<;w{FQJAgAH!}}ZxbTW>Q65c}*5u>U; za7VdZz5{5`uR;y6kQZSw~S%{ zH#|JN$vO8k5xGoN0}l0Z8J(j*AR?ZsPKn4h3kwVXO_xN_4dXN${}B;+zp6HXTo;a= zaR@&W!JLR()h!XMRsaH+v9YoKTCMhYWPBg+R(afvV-pY>ea;wj$^88MURD_xnpJ)w z0|eD-_3oyYpBaDcu!P;gCg!sUeB zKwI``R{$13M1Dg=M#x(KM!*4Lt^HIdJGU!9dw-R{+e-&7C76qjZ>d zY;>GL-V;VjIsD!cfdi~D<`<&(E!(wi0RUrB^q;scz*?bF54hp-QFd+VXK~pfy`caF z5RtRm-LY)}7-QboeA1*~1nNrx=bqWmXFoc|D^qp)a;8@UW*obt-lBI>kP`u}FuWiB95SR+(x0e^G- z4$eD!5K{@*Gg$}IbMC(*QeC`ZjZm)oeBgp1KK+p~v=-0+CT5oKJV{oNa~x9b0RpGw zbB23v9>?uE`Gru;*MHdXQT=gMu*tdU)xmti{ z)6oT_QUJyX)y06_XBYU(>vs^;0>lUyVb5e8^)-B-0#YdeP#V7Sts8ez9P*&y2<@%B z^3Ebk&kO;tn$!uvv%-8O;D!&5aK)vY2r53-N|^Ke%AU8=lL0`=1Rw&7%Z{y^bNt=! z??f98kT{r9uKWnmZ zSlKAN_*M<|9VR6i0Hj0!)(DlF&sRRVm2=N25Y&Rhj4V+Z&?}Sm1eUm*n8XXf8lhYZ zxcHm`U$}NFuI3-r!o>)#;du3(MI=S;A5tIyRmB*%>xP}=`Yjq9KFg;X4h+>QVFu9W z8C;4KkZ1vTR+umQ+;qh#AG&OF82BC0`W3(vCTEwKDf_7k03pEw2ur*iJIDIC{kM1E z>cKIGcjF9T-+?9O>kcomcjVY4_Hspp<%Z*%H;ywl){id7!@Y;23K-$VH)}}XQZok- zlSl!?C0*T}HPU7y++&g`9FT+bM2 zEIIZaSi+=e1TZFX0FMQ zn@`v~r#;xXc#h$Y8u*KXss>vqtx1syP8dG*x_zxyZ*xvP+!r8&q*HzQe9cf+X{TN96`{7&7U3@ zz)>Y$%dVs6`9xZL4p4+3P;x^#?t6HW$9_J~rcw^SM~x3Xo(5VG>Wl8E60fzwDVRRP zf$2qV|G`@f^qZDrhK)+k3cy-nuH4|ti-x)RV_RsH8z5m?=9Ck^oOTqjsVb5;eCazA zOwKmw&zqhM{F>>>Em3G#vQaas^NjD0V2vkDoli15eCw zTG8ui*B{f90swHT^ye*K`tBPv8m%woVnry|13r53Ft`2Y_LjO0RUl`m%s2Rphu$Qg z>)pKnNP1ELs^A%6w&L^SXJ(`25?kBX^8|mf!LPih1bqfq4^B8-3}KgHOCDc)aDta7 zYHS+r?ZB^@o)iE?Kd)tk(~9j~f;Di^!KLREAUOFV5Y8#K;PLa97r6J|rYR-F^~dzA z03iA?9oL>Q0tfk=uydp@6u?Q2uO?hO=uf`Ak7Zve=1kk?+{pB77SIXt^8q78kMYqy z@b!fGzY9XwfA__Bsk+6%$2f-e{`X`87 z{|}#<;U~|{B_`)T7O4_|Iz_Ev_l`k0&N6ULLGp&_=|w*Oz`m&EC)41#rYRfm3YJiii=GF%%s4Zqo#3YV506e{DdC#`~!!m#%2m`sCubNg+QjGl$08L12!G#B*tMk}kZ@!#XQ(Xr{OJP|m(!Q@f^d>Lvty1U@2Zqu};sp?A0GkFa<7f22vLm1160deifdIlHkh917 z*m8Ov4HVzku*9q3j#@P=Z6sd+gwGJ{9O)yUGsvLjpC6s(>6aHMr7Y(^7ReVtoB?bd zz!fd8?yd3X54}ktC1d?3Bk=-I4VQx5b!I=t2%mpoAMY+Cx5TSmIuSrmb(*S*82H6= zi#+!D0e<>?qD#Ei{)Zz)WVzjaP8A}s>?_+g=gE1(AK$-^d_K`7UTxc^9RVnS@B7bn zu#{{j+Ian_6U`&nB_NE54%tiT<{1o3@_}Qq@@?Xy+uS3RURO8(w*5 zkr(z=DGu14UCELqkI^tLhYC1KZGdGk$(>rrcoJ*EL-{;uK?nAP9n| zqW3K?cGqqfV5g_2tH5*7e0rO!>a9QFu)nPQI8*{rKvZ?QR4VOh`m80|b>ndxda~UU zol2A*`J$Z>h=_{Fqfz6utf2q`0N?k&@0?p`_hcDsf+6$(|Jv!!?Vy0I*XxtUn5QM| z2R3{+kYF4j&bimiKDOtDye(irngSy!+w zIKq2>z&UqewOXAf>XJJ-*lN0fIK6JD>b0u6pH(O^qa~V+d~IZW%PKOl)sE*oz*MW% z{UUNTFh?eWwZZ`+B1TpJs9Y}p7hSm5-I&mEfL&NvcveKN0%oI_!0*DbGeY9Yx4=2~ zhm}g@KEjE-RX4P5PLEh1D3wa*0pAso_p55`nl}8*;RZ!H6!1-{>UEV$O3xKe?4o;(;4W*xKXI_o(XkoOAb9tJVD+x<6eXY+LIa->g_WG&J;K;CEE@SAnzA zum(>nL=EB%RsDAn`BAxCegs&G{(DWcs`aoyvo`=>Y;3Ghuh%b7)!ia;ma1L|Y$6@r zATHo&h{&_RjEMYap-^~fYHF(1v|XIvujvr`dfx0<2#}2v9FKSubw3Sw^JUI*-@o%a_c_n~oaay+j#wjw<%IzN$U_G$j{*RL ziZCFAfd1UVN__#4Sv_QFPKdfQI~HSjebz!a9I9Ct@KfwGa~+qs@F@gcL~}=$4B?KB9oK12({Vd z-`aBIz2P&-*iQ}B7nlvh(edlvzOm<(Y-D8QJ*s;#)j7FKgs@x!3p_d?2XcJY z@!O5DVJNyeQ7NS>3H+u`PXOgP_8LxkEmXb!aUh1E3&s;jf$w_XzODVU&Lv{Q%BfsM z@*@x^vyVIITs}EtyJ@1Lfoef)0KWmv!*VXjAdlVC+q*_lITxBe`xt`hahq>(-6vk15*iXaXPGB$%k6_KPUr^=DDmSu#^ zz$s{Lg3`r2}HOVeL;k5NuNe0nqyIfh(hW}8I`Al5sk;J$0{m5C_rD2 zMzx6;sDWF#xz8;u%`~vVQdvjzrY5%M!Rmg7$7EOUSHn6CoJOzO1JdBk@*E|;$DA@K z)GZGXj*Ca^?F*TPJ%uSu}<31UWV2 z(de1FFCCidY7twXtp!;SiN1UqAX3f9_^OKzFW2|2`75zi0)wciyxpP#gRVq?qBm*f$PZ8g9StQgrE z)>u9`V=7bx^Sm{qW=ts-Btz&Q;qFhUJWcGmuhYj8(u;G;#2N4=7D-GXcvDK_;W<7` zbOAe$`p1LP!b(gkOnF5d0AzJ+;cU3kUatpPnv)lvWr_>;F@o#97^mwtNaV+7!BaIN z19R}i;H5a;0jcn94s8IV!uk2D?D1vn`-2zpWB#qhNNOztmoa|tsekG8LCeUPf;y2g z(IST#XDfM^1v6en29n!qa@^97o_@r1Waw-iF5!l^lki=Qq}QOb&!6`K7;HJKu4ycQ zN=^sA+B?&F__Xd z&_^YCIayPVN~wtGpt5p)$Gqsw>Ms%bRx9EO_-7;6*u^a#rT!N+PA7(#zdJTyuhw}o z!DQ|Oc<1z}b^x=~jD3n0Fmt-CVPzhU7>jt}&N@3;xg)bv6c0*uzwb)dWflRE&6fMn zz`uN50UYb{((+LP2s|TtzXuP0W1z$*C(=TXNde zG%?~4r7N#;@td$^O{?YTQI6Ba8=l)b;XY`8JdpR`Q3Sht#d(D}0FjjbrEHC-0EYP_+(P&U#@W=3# zKjij<-rue{Ql_F7Tvi`UJ(Wl$FC>ok9vFPStbW3^Q7pu&VR$4>)c5s&f@DycXAraq z+~ne#aLzoN!&^A}%g`4E1@hsH#i?h6P8%5*`N4w+srRb6QB%Q(DBvEx%7kzkU#|sp zI~uptyHCrJH#XF3RQv~7HqYiDKjx~Gs}a&|cOA?|G(pQv^5nx9g6FpWQ1o-vKfSH7 zYQp(OYKiSzQ^mQ9iU!XV)A+vW?KEB3bS^h7w+5~{GVURFaxw1M?yKk(5w*te-D2^} z-}2_?_oUwP7rJI*DxuBq9y|A(8FMhYZr>w;=XP?#;&v{c(stG%U(&x+@w!k~IB5B+ z_+0QGMR4tLt--Qn(o-SeO!O<}eHSgVbhBo#t!WDpu2t*ZV#-l9;#I<9eE84f#}bYE zmXG!T{;|Qh2;e49B*!wZU(`)(=>x_)1IN6ZBVj*P=@Y%$mzErxQvOT9?e2ATcW=@Q zb=5q|lMq9Xy~1tX0&ZSO!I54IOuaE6--G@B>A^2r!jn53_jTSiYRd45n&DT4@mU_F}~Ni0b@dPKuD=^TlKIP4cuZU z-KThgcbZqxqB4A(cBCw$Si}{heU<#MMY31hD;`BZ4R_cs3XD2*`A9mgJI;sCD-52s zdZ*HYfu%9B)>G*RH(%aCCbohiZ@x!o3`EVAuck5#YzeC{^0>8NhDD1Scyt+t2Tn%3 z(fY_8Pd@jV>j>3xv3zM#8740gMJX6iLtSeAaajbV#jn%nDQjJ6DD(*sq~K(y2=43< zLZ=2r5wkdd5sAN~3;cT`_^mmcN4V2(qh3DRt2Tcno0O2y9%cvwj)J8-M_E@S=wwe{%8%_J73~0AeR7Ez>vVwekGm4&tI@`4bgO z_NV?9Zy&E%Qt3n#DPw?swL24c6^S?10sfPg`Ml3i93^ z2ppoBPm)j||M3JL=&y`;p@CpJ3FZ)5@_scVRe@|OD3g!{>FFg(7B(K}A>9!I#Mlcg zvIo;G^j{N?*6hb0d8UdzbPsiew<8_q;8-%cdWG;Z_oW4nzOf?R`jxii?o)v9?nfl| zkT7`S$+N#1+NBZ-s~pDw>2RZb9*qWzTI{r$+IfN&dxaqHF_N-xxQFES75t3%MYke~ z79rfZ`h1=t8aP$?@vzsun>U)mw*902QdJ-uc>+RPE;PU^1mlnasHKC_;#VYcz7B_w=hX9W8~(c@9-`z#n|ps_LtufN15DD_xJp>5HUgjUVBqiYd-t*g z@c}l~OZG>>xUiVA@_3g88EKc0^|J{XC8ZO%*akE-I)!4mLWNOlmuOS1GKq7ObzI;{ z^pZz3u(dt8^N#dTV7$<9JdbbB9x=t26)L?V@FMd2pPWuPwC9z{zjEbFz1xK@IE00EsxXLV^6kAJehZnG<%g^K z9gpNjJA(J9ui=CJw^(9*ftZi+0xh8H_-*E@tT{?Uu_B@3>vp@0)fB! z-mDiFOa=0Y^*NME)69=~a$%#2RC((J2s@QV_YeyCvD&{fpV!-&jZ1XI@A+gTuhxb2 zdrUMeNX=`Wmx{#I(tDc87_iTgsHfE8H&UyV*Heh#pflT>nt`Ez!s%2!w*IHp95`H6 z5KCV?ST%bGF-Llw5o|JEk=8CH_yEnA%5IO?HI(e!NP{={9LwGGcwn_o;qiJ(GAt8L zEczi-V7j|aC7xSG5M0+p;8Bd}R6p|NO*0)*V)``+t+_#rXB9s?_nv1PF7MJo;Eza+ zHLW~MPq&@h#rcH0vnEcSSSQh27Vr#Tk#oy`(XX*nh&xa~{4x{00zq?ebZLZyR>UB0*_3-81&M&xBc0<+mb;bs(|zyBCD1o$oZtz|1B|B zb5tN#+%^m!6AtNtpPWaK34~tr2cK4*A;TEo*0&LGCjx)Tu6QG05ag42mKy;-Ljfb7{SD&{ zg~>I5$TJBr2QR<=4T>N{$^UNy2>G&fguw<7vSk9{D{zawy-|ySMu%#FqcPKeJE7`z zEXRzV6I2MB{2&iy1-VzLMy)Unk@GhH#73wBD3sJLP+&a47}r)0hLRkVg%>6A({&sI z^MgE>0^#y^Xd7ulCL}C@*2<8Q4a#l(l_rUx8*PI4BM^TJA6;`NE?7R~+eMLRC}qgw z&As5Nv|p00M?q`zg9WbDW0o9BR#vgsPkVx9Z~sQ4aak3U$@F zcRT3=1<`jpE8T-bt2u7dyG~5+0Yqt#Uc)Mx8%Hpn2K84eDE@+0)qHwl+bqaDQc|sf zT`N|5zv6C7@uw-wA9H=^>8lv~EA=PG<}fUox-+G#sV%7N;2t1F{`sXY7){?p`Jy2} z71mD-*!iC~OFZUNgt#$JLXvpW-yp9nBk!>cY;~t?)nZrd&(Ooxsg(D`V&S%v=VdctX@vy(8uF8YL<4(MKsL}Hpn*r3uA+_#X1 zPLs-}AyORx$^OtJdR=%nYwGET1|S0-*Jii|to5xgl;`phi-u4%F+tKRIba}^c%^uz ztm!s%X6DO;zH}tf(btKd^@NrdkF&ybmw-6*s(W~z-I7?f3p@m*Sx)BlEfYwaizQNJ zfeMSgW}UgnrzG+XBdIL7*X(u z%9A>|i}xG|kv6GM>#3xDgwYi6HMgy3Jl-wV+NhP`y7>F1{>LQ~g{eD_3rdvx-b}wKpG(Uc^glE>rVtep=VuszzPLj7c4m5X<#ZywcQj#&=P2?Nx&2M}=jtk<+^Y0iH zWz)mhadH5R;2}eH9O$%~yw(*mio3~ljAl6xpg##Y#FZsufXYl{gLURhmo7D+1v=U5 zrr>OP0Z_(RMtEDbQJlCe3&Z8!Sj9rHk0hx z0qw@{hT)gk#il75K!AbvgO+Z@_3-9V9-=r>D?>1wz4^HqnZDGxi_(=>tPhL#)+Oeq zi~U(OuO5p1#^G@ITGKst&Ui)(l2ti#d8DRi@o2p|MeTgT5D~$R?mD)5ai;S1u$NKYK3E)@wX;A+cFd|tlm_|nx`BT_y z!OL;H$css&>+B>=3joF>`{v_#OVpU^WIYG6`js;lm-`qzzftyRI`n292eigP(D57` zC}7S?WaYa_-4{--~#g=vH?1@DlClOaY(ci!5f3R zMdOR2_5jsbTZES>rk6i)>KWF|y|^|lcvbu!S+G$$2mu}wwr-GNUnR=#bks>o=VIMv z&l8tztdu$Us=CrnU%uJM3njX~IhRLcT6$4M#lpZ4n|P*{H{3USNs)us^#i(AD;Mk6 z<9P<4LNG-2HnMWt{bD&Qmfh%yhCT8eF=3OHgj2mBTM?K)8n}Hp|L2cx*agIw6qiZd+-yW*^UVD8XIQ2@s z8M?cvkj-L)D7#--HuS^yP*=>Qa8Vd$0 zKSshhLiD)4A`;l4aHDp_^?`FOIG9rM=h5%CM)^JaK7QU|HR(@$aoExwFfLK2VYha7 z)GS6-UPlsW!0K-DP2*EqiS}KzWbO-o1k- zTwHFQIB~+?-p=kth~Bt0nvgY9FOaYeVKyKaf#AJBZ$h@VdL zQQaaNR~kRgHs`pPa>y#qoxsH=43^n@sCKI?*I)k=CHepT)3E1>&*t2D(uT5Fgzmz@ NA*&;nPb@qW{|5&3xOe~n diff --git a/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-16@1x.png b/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-16@1x.png deleted file mode 100644 index dd8790bd5ec621054fb30d5ae94212afbdb868c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 553 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 NA*&;nPb@qW{|5&3xOe~n diff --git a/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-256@2x.png b/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-256@2x.png deleted file mode 100644 index 42deceacdc1dd49e4d9eed75f7adf9b183c4b949..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11672 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 a5B~+jNiku(f?u%!0000 diff --git a/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-512@1x.png b/appleApp/FTW/Assets.xcassets/AppIcon.appiconset/icon-mac-512@1x.png deleted file mode 100644 index 42deceacdc1dd49e4d9eed75f7adf9b183c4b949..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11672 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

bIY$l_&BjY+_re0J0g~UnbZ$jUw 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 o Date: Mon, 12 Oct 2026 18:43:21 +0200 Subject: [PATCH 3/5] Declare privacy and export compliance for the App Store Both the app and the widget extension carry a privacy manifest: no tracking, no tracking domains, nothing collected. Readings and commands travel end to end encrypted between the phone and the household's box; the relay forwards ciphertext and stores nothing. Required reasons, checked against the Release binaries: - File timestamp, C617.1, in both. FTWShared's CacheReader picks the newest home by the modification date of its App Group files. - UserDefaults, CA92.1, in the app. Only Debug builds read it today, for the screenshot launch arguments. ITSAppUsesNonExemptEncryption is NO. Every primitive comes from CryptoKit in iOS; the app implements no algorithm of its own. The pull request gives the reasoning and sources, and what France needs. Co-Authored-By: Claude Opus 5.5 --- appleApp/FTW-Info.plist | 2 ++ appleApp/FTW/PrivacyInfo.xcprivacy | 31 +++++++++++++++++++++++ appleApp/FTWWidgets/PrivacyInfo.xcprivacy | 23 +++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 appleApp/FTW/PrivacyInfo.xcprivacy create mode 100644 appleApp/FTWWidgets/PrivacyInfo.xcprivacy diff --git a/appleApp/FTW-Info.plist b/appleApp/FTW-Info.plist index e20acc0..1bb587f 100644 --- a/appleApp/FTW-Info.plist +++ b/appleApp/FTW-Info.plist @@ -9,6 +9,8 @@ en sv + ITSAppUsesNonExemptEncryption + UILaunchScreen UIColorName diff --git a/appleApp/FTW/PrivacyInfo.xcprivacy b/appleApp/FTW/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..dba570d --- /dev/null +++ b/appleApp/FTW/PrivacyInfo.xcprivacy @@ -0,0 +1,31 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + diff --git a/appleApp/FTWWidgets/PrivacyInfo.xcprivacy b/appleApp/FTWWidgets/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..64e785c --- /dev/null +++ b/appleApp/FTWWidgets/PrivacyInfo.xcprivacy @@ -0,0 +1,23 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + + From e76660d1a79213ca8165ee78a93e5a72ad04d97f Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 12 Oct 2026 18:57:30 +0200 Subject: [PATCH 4/5] Tidy Source & licenses after a look in the simulator - The close button is the neutral glass one; as a confirmation action it turned amber. - The removal question points at the button that asked it, not at the top of the list. - The mark in the header grows with the text, and the header stacks at accessibility sizes. - A title in capitals over a short line, such as the license's name over its version, reads as one heading. Co-Authored-By: Claude Opus 5.5 --- appleApp/FTW/About/AboutView.swift | 12 ++++++++---- appleApp/FTW/About/RemovePhoneView.swift | 19 ++++++++++--------- .../Sources/FTWKit/Identity/LicenseText.swift | 14 +++++++++++++- .../FTWKit/Tests/FTWKitTests/AboutTests.swift | 1 + 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/appleApp/FTW/About/AboutView.swift b/appleApp/FTW/About/AboutView.swift index fb167e1..7c52993 100644 --- a/appleApp/FTW/About/AboutView.swift +++ b/appleApp/FTW/About/AboutView.swift @@ -29,6 +29,8 @@ struct AboutView: View { let app: AppModel @State private var path: [Route] @Environment(\.dismiss) private var dismiss + @Environment(\.dynamicTypeSize) private var typeSize + @ScaledMetric(relativeTo: .title2) private var markSize: CGFloat = 52 private let source = BuildSource.bundled init(app: AppModel, start: Start = .about) { @@ -109,8 +111,9 @@ struct AboutView: View { .navigationTitle(AboutWords.text("Source & licenses")) .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .confirmationAction) { + ToolbarItem(placement: .topBarTrailing) { Button(role: .close) { dismiss() } + .tint(nil) } } .navigationDestination(for: Route.self) { route in @@ -128,12 +131,13 @@ struct AboutView: View { /// The notices the AGPL asks an interactive program to show: who holds /// the copyright, that there is no warranty, and where the license is. private var header: some View { - HStack(alignment: .top, spacing: 14) { + let stack = typeSize.isAccessibilitySize ? AnyLayout(VStackLayout(alignment: .leading, spacing: 12)) : AnyLayout(HStackLayout(alignment: .top, spacing: 14)) + return stack { Image(systemName: "bolt.fill") .font(.title2) .foregroundStyle(Theme.accent) - .frame(width: 52, height: 52) - .background(Color(white: 0.05), in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .frame(width: markSize, height: markSize) + .background(Color(white: 0.05), in: RoundedRectangle(cornerRadius: markSize * 0.23, style: .continuous)) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 4) { Text("FTW").font(.title2.weight(.bold)) diff --git a/appleApp/FTW/About/RemovePhoneView.swift b/appleApp/FTW/About/RemovePhoneView.swift index eb6a7cc..2b95d95 100644 --- a/appleApp/FTW/About/RemovePhoneView.swift +++ b/appleApp/FTW/About/RemovePhoneView.swift @@ -43,6 +43,16 @@ struct RemovePhoneView: View { Section { Button(AboutWords.text("Remove from this phone"), role: .destructive) { confirming = true } + // On the button, so the question points at what asked it. + .confirmationDialog(AboutWords.text("Remove this phone?"), isPresented: $confirming, titleVisibility: .visible) { + Button(AboutWords.text("Remove"), role: .destructive) { + close() + app.leave() + } + Button(AboutWords.text("Cancel"), role: .cancel) {} + } message: { + Text(AboutWords.text("FTW forgets your home on this phone. To come back, scan a new pairing QR from your box.")) + } } header: { Text(AboutWords.text("On this phone")) } footer: { @@ -53,14 +63,5 @@ struct RemovePhoneView: View { .background(Theme.surface) .navigationTitle(AboutWords.text("Remove this phone")) .navigationBarTitleDisplayMode(.inline) - .confirmationDialog(AboutWords.text("Remove this phone?"), isPresented: $confirming, titleVisibility: .visible) { - Button(AboutWords.text("Remove"), role: .destructive) { - close() - app.leave() - } - Button(AboutWords.text("Cancel"), role: .cancel) {} - } message: { - Text(AboutWords.text("FTW forgets your home on this phone. To come back, scan a new pairing QR from your box.")) - } } } diff --git a/appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift b/appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift index d06bf7e..39fda9a 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Identity/LicenseText.swift @@ -62,6 +62,14 @@ public enum LicenseText { add(.heading(level), paragraph[0].trimmingCharacters(in: .whitespaces)) return } + // A title block: lines set in the middle of the page, or a + // name in capitals over a short line, such as the license's + // name over its version. + let titled = paragraph.count == 2 && plainHeading(paragraph[0]) == 1 && paragraph[1].trimmingCharacters(in: .whitespaces).count <= 40 + if !markdown, paragraph.count > 1, titled || paragraph.allSatisfy(isCentred) { + add(.heading(1), paragraph.map { $0.trimmingCharacters(in: .whitespaces) }.joined(separator: "\n")) + return + } add(.paragraph, join(paragraph, width: width)) } @@ -129,6 +137,10 @@ public enum LicenseText { return (min(hashes, 2), String(line.dropFirst(hashes + 1))) } + static func isCentred(_ line: String) -> Bool { + line.prefix(while: { $0 == " " }).count >= 8 && line.trimmingCharacters(in: .whitespaces).count <= 60 + } + /// In plain text a title is a line on its own: set in capitals, centred /// with spaces, or a numbered section such as "0. Definitions." static func plainHeading(_ line: String) -> Int? { @@ -136,7 +148,7 @@ public enum LicenseText { guard trimmed.count <= 60 else { return nil } let letters = trimmed.filter(\.isLetter) if !letters.isEmpty, letters == letters.uppercased(), letters.count >= 4 { return 1 } - if line.prefix(while: { $0 == " " }).count >= 8 { return 1 } + if isCentred(line) { return 1 } if trimmed.range(of: #"^[0-9]{1,2}\. [A-Z][^.]*\.$"#, options: .regularExpression) != nil { return 2 } return nil } diff --git a/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift b/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift index 581d177..45de0f2 100644 --- a/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift +++ b/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift @@ -72,6 +72,7 @@ import Testing #expect(!opening.text.contains("\n")) #expect(blocks.contains { $0.kind == .heading(2) && $0.text == "0. Definitions." }) #expect(blocks.contains { $0.kind == .heading(1) && $0.text == "TERMS AND CONDITIONS" }) + #expect(blocks.contains { $0.kind == .heading(1) && $0.text == "GNU AFFERO GENERAL PUBLIC LICENSE\nVersion 3, 19 November 2007" }) #expect(blocks.contains { $0.kind == .rule }) // Both of the license's own permissions are there. #expect(blocks.contains { $0.text.contains("FTW app store distribution permission") } || blocks.contains { $0.text.contains("Energyplan") }) From 42b95dc5e91d26bd5c96d4b9acc21b31ed6dcbf7 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 12 Oct 2026 19:03:33 +0200 Subject: [PATCH 5/5] Link the privacy policy and support from inside the app App Review's 5.1.1(i) asks that the privacy policy be easy to find in the app. The foot of Now and of pairing links it beside Source & licenses, and Source & licenses has a Privacy and support section. Both pages are srcfl/ftw-web#18's, at ftw.energy, in Swedish when the app is. The footer's links stay on one line while they fit and stack when they do not. Co-Authored-By: Claude Opus 5.5 --- appleApp/FTW/About/AboutLink.swift | 27 +++++++++++-------- appleApp/FTW/About/AboutView.swift | 13 +++++++++ .../Sources/FTWKit/Identity/AboutWords.swift | 6 +++++ .../Sources/FTWKit/Identity/Origin.swift | 14 ++++++++++ .../FTWKit/Tests/FTWKitTests/AboutTests.swift | 12 +++++++++ 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/appleApp/FTW/About/AboutLink.swift b/appleApp/FTW/About/AboutLink.swift index 8c03aa7..65d2de1 100644 --- a/appleApp/FTW/About/AboutLink.swift +++ b/appleApp/FTW/About/AboutLink.swift @@ -1,23 +1,20 @@ import FTWKit import SwiftUI -/// The way to Source & licenses and to removing this phone, at the foot of -/// Now and of pairing. The license asks that its text and the source offer -/// be reachable from inside the app, and App Review asks the same of -/// removing what the app set up. +/// The way to Source & licenses, the privacy policy and removing this phone, +/// at the foot of Now and of pairing. The license asks that its text and the +/// source offer be reachable from inside the app; App Review asks the same +/// of the privacy policy (5.1.1(i)) and of removing what the app set up. struct AboutLink: View { let app: AppModel @State private var open: AboutView.Start? - @Environment(\.dynamicTypeSize) private var typeSize var body: some View { - let stack = typeSize.isAccessibilitySize ? AnyLayout(VStackLayout(spacing: 8)) : AnyLayout(HStackLayout(spacing: 20)) VStack(spacing: 6) { - stack { - Button(AboutWords.text("Source & licenses")) { open = .about } - if AboutView.canRemove(app) { - Button(AboutWords.text("Remove this phone")) { open = .remove } - } + // One line while it fits, one link to a line when it does not. + ViewThatFits(in: .horizontal) { + HStack(spacing: 20) { links } + VStack(spacing: 8) { links } } .font(.footnote.weight(.medium)) .foregroundStyle(Theme.accent) @@ -41,6 +38,14 @@ struct AboutLink: View { } #endif } + + @ViewBuilder private var links: some View { + Button(AboutWords.text("Source & licenses")) { open = .about } + Link(AboutWords.text("Privacy"), destination: Origin.privacyURL()) + if AboutView.canRemove(app) { + Button(AboutWords.text("Remove this phone")) { open = .remove } + } + } } extension BuildSource { diff --git a/appleApp/FTW/About/AboutView.swift b/appleApp/FTW/About/AboutView.swift index 7c52993..78906e1 100644 --- a/appleApp/FTW/About/AboutView.swift +++ b/appleApp/FTW/About/AboutView.swift @@ -80,6 +80,19 @@ struct AboutView: View { Text(AboutWords.text("FTW is free software. You may run, study, change and share it under the GNU AGPL v3 with the two additional permissions in LICENSE. The link gives the complete source of this exact version, at no charge.")) } + Section { + Link(destination: Origin.privacyURL()) { + Label(AboutWords.text("Privacy policy"), systemImage: "hand.raised") + } + Link(destination: Origin.supportURL()) { + Label(AboutWords.text("Support"), systemImage: "questionmark.circle") + } + } header: { + Text(AboutWords.text("Privacy and support")) + } footer: { + Text(AboutWords.text("FTW collects nothing about you. Readings and commands travel encrypted between this phone and your box, and Sourceful cannot read them.")) + } + Section { ForEach(LicenseText.Document.allCases) { document in NavigationLink(value: Route.document(document)) { diff --git a/appleApp/FTWKit/Sources/FTWKit/Identity/AboutWords.swift b/appleApp/FTWKit/Sources/FTWKit/Identity/AboutWords.swift index 1cdc90a..d369896 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Identity/AboutWords.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Identity/AboutWords.swift @@ -59,6 +59,12 @@ public enum AboutWords { "© 2026 Sourceful Labs AB and contributors.": "© 2026 Sourceful Labs AB och bidragsgivare.", "There is no warranty, to the extent the law allows.": "Det finns ingen garanti, så långt lagen tillåter.", "This text is missing from this build.": "Texten saknas i det här bygget.", + "Privacy": "Integritet", + "Privacy policy": "Integritetspolicy", + "Support": "Support", + "Privacy and support": "Integritet och support", + "FTW collects nothing about you. Readings and commands travel encrypted between this phone and your box, and Sourceful cannot read them.": + "FTW samlar inte in något om dig. Mätvärden och kommandon går krypterade mellan telefonen och boxen, och Sourceful kan inte läsa dem.", // Removing this phone "This phone": "Den här telefonen", diff --git a/appleApp/FTWKit/Sources/FTWKit/Identity/Origin.swift b/appleApp/FTWKit/Sources/FTWKit/Identity/Origin.swift index 83df598..937eec1 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Identity/Origin.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Identity/Origin.swift @@ -19,4 +19,18 @@ public enum Origin { /// links in Source & licenses must lead to the fork's own source. /// `BuildSource` pins them to the build's commit. public static let sourceURL = URL(string: "https://github.com/srcfl/ftw-app")! + + /// The privacy policy, which App Review asks to be easy to find in the + /// app, and the support page beside it. Swedish where the app speaks it. + public static func privacyURL(_ language: AboutWords.Language = AboutWords.language) -> URL { + site(language, "privacy") + } + + public static func supportURL(_ language: AboutWords.Language = AboutWords.language) -> URL { + site(language, "support") + } + + private static func site(_ language: AboutWords.Language, _ page: String) -> URL { + URL(string: "https://ftw.energy/\(language == .sv ? "sv/" : "")\(page)")! + } } diff --git a/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift b/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift index 45de0f2..a51a89c 100644 --- a/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift +++ b/appleApp/FTWKit/Tests/FTWKitTests/AboutTests.swift @@ -45,6 +45,18 @@ import Testing #expect(source.localNote != nil) } + /// App Review asks for the privacy policy inside the app. The pages are + /// srcfl/ftw-web's, in English and Swedish. + @Test func privacyAndSupportFollowTheLanguage() { + #expect(Origin.privacyURL(.en).absoluteString == "https://ftw.energy/privacy") + #expect(Origin.privacyURL(.sv).absoluteString == "https://ftw.energy/sv/privacy") + #expect(Origin.supportURL(.en).absoluteString == "https://ftw.energy/support") + #expect(Origin.supportURL(.sv).absoluteString == "https://ftw.energy/sv/support") + AboutWords.$language.withValue(.sv) { + #expect(Origin.privacyURL() == Origin.privacyURL(.sv)) + } + } + // MARK: The license files /// Joining lines must never change the text: every word of every file,