Skip to content
Closed
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
7 changes: 7 additions & 0 deletions appleApp/FTW-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@
<string>en</string>
<string>sv</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>LaunchBackground</string>
</dict>
</dict>
</plist>
6 changes: 6 additions & 0 deletions appleApp/FTW.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
56 changes: 56 additions & 0 deletions appleApp/FTW.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -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 */
Expand All @@ -44,6 +60,9 @@
F7A000000000000000000008 /* FTW.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FTW.xcconfig; sourceTree = "<group>"; };
F7A000000000000000000009 /* FTW-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FTW-Info.plist"; sourceTree = "<group>"; };
F7A00000000000000000000A /* FTWWidgets-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FTWWidgets-Info.plist"; sourceTree = "<group>"; };
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 */

Expand Down Expand Up @@ -91,10 +110,21 @@
F7A000000000000000000009 /* FTW-Info.plist */,
F7A000000000000000000007 /* FTWWidgets.entitlements */,
F7A00000000000000000000A /* FTWWidgets-Info.plist */,
F7A000000000000000000063 /* License texts */,
F7A000000000000000000004 /* Products */,
);
sourceTree = "<group>";
};
F7A000000000000000000063 /* License texts */ = {
isa = PBXGroup;
children = (
F7A000000000000000000060 /* LICENSE */,
F7A000000000000000000061 /* NOTICE */,
F7A000000000000000000062 /* LICENSING.md */,
);
name = "License texts";
sourceTree = "<group>";
};
F7A000000000000000000004 /* Products */ = {
isa = PBXGroup;
children = (
Expand All @@ -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 = (
Expand Down Expand Up @@ -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;
Expand Down
60 changes: 60 additions & 0 deletions appleApp/FTW/About/AboutLink.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import FTWKit
import SwiftUI

/// 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?

var body: some View {
VStack(spacing: 6) {
// 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)
.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
}

@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 {
/// 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) }
)
}
167 changes: 167 additions & 0 deletions appleApp/FTW/About/AboutView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
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
@Environment(\.dynamicTypeSize) private var typeSize
@ScaledMetric(relativeTo: .title2) private var markSize: CGFloat = 52
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 {
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)) {
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: .topBarTrailing) {
Button(role: .close) { dismiss() }
.tint(nil)
}
}
.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 {
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: 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))
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)
}
}
Loading
Loading