Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fc2b752
iPhone M2: Charge now as a Control, Siri and App Shortcuts, local Liv…
frahlg Oct 12, 2026
0e84efa
feat(protocol): copy the FTW Cloud names from the registry
frahlg Oct 12, 2026
5bf1d43
iPhone M2: no AppIntentsPackage, one-line Live Activity, demo flag
frahlg Oct 12, 2026
b0c2a2d
iPhone M2: Siri levels are the car view's chips; the card says what c…
frahlg Oct 12, 2026
c22250d
iPhone M2: the Live Activity's small view fits the watch's card
frahlg Oct 12, 2026
2c946bf
iPhone M2: build with the iOS 26 SDK too
frahlg Oct 12, 2026
f69ffab
feat(cloud): read and age a box-signed home from FTW Cloud
frahlg Oct 13, 2026
8c8299d
iPhone M3: check a push, write its words, pick its buttons
frahlg Oct 13, 2026
c6bf01c
iPhone M3: a notification service extension writes every push
frahlg Oct 13, 2026
2f66504
feat(cloud): the app reads its home from FTW Cloud first
frahlg Oct 13, 2026
5d11477
iPhone M3: a repeated push shows the generic line, quietly
frahlg Oct 13, 2026
2b80122
iPhone M3: notification buttons run the M2 command path
frahlg Oct 13, 2026
3bf6347
iPhone M3: a debug build can take a pairing link at launch
frahlg Oct 13, 2026
fca88b0
feat(cloud): ask again after a refusal, log each layer, and a live test
frahlg Oct 13, 2026
6e97d19
fix(cloud): FTW Cloud turned off stays off after a restart
frahlg Oct 13, 2026
f3c2ff6
fix(app): honest band at start, prompt command failure, a calmer clock
frahlg Oct 13, 2026
329dfce
fix(app): the band and the cards read one snapshot; time to live per …
frahlg Oct 13, 2026
78d743f
Merge remote-tracking branch 'origin/claude/ios-m3-notify' into claud…
frahlg Oct 13, 2026
1b79dfc
test: the recording carrier returns what send returns
frahlg Oct 13, 2026
31c63d7
feat(cloud): the app's FTW Cloud session carries its push token
frahlg Oct 13, 2026
57afd80
feat(cloud): release builds dial FTW Cloud with its Noise key
frahlg Oct 13, 2026
7907cc9
Merge main (#15) into the release Noise key
frahlg Oct 13, 2026
1d77dde
Merge main (#16, #17, #22, #23) into the app's FTW Cloud reading (C3)
frahlg Oct 13, 2026
42071e4
Merge main into claude/ios-m3-notify
frahlg Oct 13, 2026
77dba30
fix(app): Swedish help when a connect fails; prices keep the home's zone
frahlg Oct 13, 2026
5c4c08b
Merge #19 (with main 9e391d5) into the release Noise key
frahlg Oct 13, 2026
a923b36
Merge #18 (with main 9e391d5) into the C3 + C4 join
frahlg Oct 13, 2026
08557b3
Merge #21 (#19 with main, and the release Noise key) into the C3 + C4…
frahlg Oct 13, 2026
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
4 changes: 4 additions & 0 deletions appleApp/FTW.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
<array>
<string>$(FTW_APP_GROUP)</string>
</array>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.usernotifications.time-sensitive</key>
<true/>
</dict>
</plist>
137 changes: 137 additions & 0 deletions appleApp/FTW.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
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, ); }; };
F7A000000000000000000070 /* FTWShared in Frameworks */ = {isa = PBXBuildFile; productRef = F7A000000000000000000071 /* FTWShared */; };
F7A000000000000000000072 /* FTWNotify.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F7A000000000000000000074 /* FTWNotify.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand All @@ -31,6 +33,13 @@
remoteGlobalIDString = F7A000000000000000000010;
remoteInfo = FTW;
};
F7A000000000000000000073 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F7A000000000000000000001 /* Project object */;
proxyType = 1;
remoteGlobalIDString = F7A000000000000000000078;
remoteInfo = FTWNotify;
};
/* End PBXContainerItemProxy section */

/* Begin PBXCopyFilesBuildPhase section */
Expand All @@ -41,6 +50,7 @@
dstSubfolderSpec = 13;
files = (
F7A000000000000000000034 /* FTWWidgets.appex in Embed Foundation Extensions */,
F7A000000000000000000072 /* FTWNotify.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -72,6 +82,9 @@
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; };
F7A000000000000000000100 /* FTWUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FTWUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F7A000000000000000000074 /* FTWNotify.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FTWNotify.appex; sourceTree = BUILT_PRODUCTS_DIR; };
F7A000000000000000000075 /* FTWNotify.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FTWNotify.entitlements; sourceTree = "<group>"; };
F7A000000000000000000076 /* FTWNotify-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FTWNotify-Info.plist"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand All @@ -95,6 +108,11 @@
path = FTWCharts;
sourceTree = "<group>";
};
F7A000000000000000000077 /* FTWNotify */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = FTWNotify;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -122,6 +140,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F7A00000000000000000007A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F7A000000000000000000070 /* FTWShared in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
Expand All @@ -132,11 +158,14 @@
F7A000000000000000000041 /* FTWWidgets */,
F7A000000000000000000101 /* FTWUITests */,
F7A000000000000000000110 /* FTWCharts */,
F7A000000000000000000077 /* FTWNotify */,
F7A000000000000000000008 /* FTW.xcconfig */,
F7A000000000000000000006 /* FTW.entitlements */,
F7A000000000000000000009 /* FTW-Info.plist */,
F7A000000000000000000007 /* FTWWidgets.entitlements */,
F7A00000000000000000000A /* FTWWidgets-Info.plist */,
F7A000000000000000000075 /* FTWNotify.entitlements */,
F7A000000000000000000076 /* FTWNotify-Info.plist */,
F7A000000000000000000063 /* License texts */,
F7A000000000000000000004 /* Products */,
);
Expand All @@ -158,6 +187,7 @@
F7A000000000000000000005 /* FTW.app */,
F7A000000000000000000040 /* FTWWidgets.appex */,
F7A000000000000000000100 /* FTWUITests.xctest */,
F7A000000000000000000074 /* FTWNotify.appex */,
);
name = Products;
sourceTree = "<group>";
Expand All @@ -180,6 +210,7 @@
);
dependencies = (
F7A000000000000000000037 /* PBXTargetDependency */,
F7A00000000000000000007C /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
F7A000000000000000000003 /* FTW */,
Expand Down Expand Up @@ -239,6 +270,29 @@
productReference = F7A000000000000000000100 /* FTWUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
F7A000000000000000000078 /* FTWNotify */ = {
isa = PBXNativeTarget;
buildConfigurationList = F7A00000000000000000007F /* Build configuration list for PBXNativeTarget "FTWNotify" */;
buildPhases = (
F7A000000000000000000079 /* Sources */,
F7A00000000000000000007A /* Frameworks */,
F7A00000000000000000007B /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
F7A000000000000000000077 /* FTWNotify */,
);
name = FTWNotify;
packageProductDependencies = (
F7A000000000000000000071 /* FTWShared */,
);
productName = FTWNotify;
productReference = F7A000000000000000000074 /* FTWNotify.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
Expand All @@ -259,6 +313,9 @@
CreatedOnToolsVersion = 27.0;
TestTargetID = F7A000000000000000000010;
};
F7A000000000000000000078 = {
CreatedOnToolsVersion = 27.0;
};
};
};
buildConfigurationList = F7A000000000000000000020 /* Build configuration list for PBXProject "FTW" */;
Expand All @@ -282,6 +339,7 @@
F7A000000000000000000010 /* FTW */,
F7A000000000000000000050 /* FTWWidgets */,
F7A000000000000000000102 /* FTWUITests */,
F7A000000000000000000078 /* FTWNotify */,
);
};
/* End PBXProject section */
Expand All @@ -308,6 +366,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F7A00000000000000000007B /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
Expand Down Expand Up @@ -356,6 +421,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F7A000000000000000000079 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */

/* Begin PBXTargetDependency section */
Expand All @@ -369,6 +441,11 @@
target = F7A000000000000000000010 /* FTW */;
targetProxy = F7A000000000000000000106 /* PBXContainerItemProxy */;
};
F7A00000000000000000007C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = F7A000000000000000000078 /* FTWNotify */;
targetProxy = F7A000000000000000000073 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */

/* Begin XCBuildConfiguration section */
Expand Down Expand Up @@ -567,6 +644,52 @@
};
name = Release;
};
F7A00000000000000000007D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = FTWNotify.entitlements;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "FTWNotify-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = FTW;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = energy.ftw.app.notify;
PRODUCT_NAME = FTWNotify;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
F7A00000000000000000007E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = FTWNotify.entitlements;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "FTWNotify-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = FTW;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = energy.ftw.app.notify;
PRODUCT_NAME = FTWNotify;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
Expand Down Expand Up @@ -606,6 +729,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F7A00000000000000000007F /* Build configuration list for PBXNativeTarget "FTWNotify" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F7A00000000000000000007D /* Debug */,
F7A00000000000000000007E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
Expand All @@ -631,6 +763,11 @@
package = F7A000000000000000000015 /* XCLocalSwiftPackageReference "FTWKit" */;
productName = FTWShared;
};
F7A000000000000000000071 /* FTWShared */ = {
isa = XCSwiftPackageProductDependency;
package = F7A000000000000000000015 /* XCLocalSwiftPackageReference "FTWKit" */;
productName = FTWShared;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = F7A000000000000000000001 /* Project object */;
Expand Down
3 changes: 2 additions & 1 deletion appleApp/FTW/About/AboutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ struct AboutView: View {
}

Section {
if !app.isDemo { NavigationLink { CloudSettingsView(app: app) } label: { Label(CloudWords.text("FTW Cloud"), systemImage: "cloud") } }
Link(destination: Origin.privacyURL()) {
Label(AboutWords.text("Privacy policy"), systemImage: "hand.raised")
}
Expand All @@ -90,7 +91,7 @@ struct AboutView: View {
} 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."))
Text(app.site?.cloudOn == true ? CloudWords.text("With FTW Cloud on, Sourceful keeps a copy of your home's readings, signed by your box, to serve this app. Commands travel encrypted between this phone and your box, and Sourceful cannot read them.") : AboutWords.text("FTW collects nothing about you. Readings and commands travel encrypted between this phone and your box, and Sourceful cannot read them."))
}

Section {
Expand Down
73 changes: 73 additions & 0 deletions appleApp/FTW/Cloud/CloudConsentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import FTWKit
import SwiftUI

/// The one screen, at first start: the app uses FTW Cloud, and statistics
/// are a separate choice, off until ticked. An owner's phone turns FTW Cloud
/// on at the box when the person goes on; a viewer is told to ask the owner.
struct CloudConsentView: View {
let app: AppModel
@Bindable var consent: CloudConsent

private var audience: CloudConsent.Audience {
app.site.map(CloudConsent.audience) ?? .unknown
}

var body: some View {
List {
Section {
VStack(alignment: .leading, spacing: 12) {
Image(systemName: "cloud.fill")
.font(.largeTitle)
.foregroundStyle(Theme.accent)
.symbolEffect(.bounce, value: consent.answered)
.accessibilityHidden(true)
Text(CloudWords.text("The FTW app uses FTW Cloud"))
.font(.title2.weight(.bold))
Text(CloudWords.text("FTW Cloud is part of the service. It keeps a copy of your home, signed by your box, so the app opens at once and stays up to date."))
Text(CloudWords.text("Your box still decides. Commands go straight to it, never through FTW Cloud."))
.foregroundStyle(Theme.fgDim)
}
.padding(.vertical, 4)
}

switch audience {
case .viewer:
Section {
Label(CloudWords.text("Ask the owner of this home to turn on FTW Cloud."), systemImage: "person.crop.circle.badge.questionmark")
}
case .owner, .ownerLater, .unknown:
Section {
Toggle(CloudWords.text("Contribute to statistics"), isOn: $consent.contributeStats)
} footer: {
Text(CloudWords.text("Hourly energy totals, without your name or address, help make FTW better. Off unless you turn it on."))
}
if audience == .ownerLater {
Section {
Label(CloudWords.text("Your box turns on FTW Cloud after its next update."), systemImage: "arrow.down.circle")
}
}
}

Section {
Link(destination: Origin.privacyURL()) {
Label(CloudWords.text("Privacy policy"), systemImage: "hand.raised")
}
} footer: {
Text(CloudWords.text("You can turn FTW Cloud off in About."))
}
}
.scrollContentBackground(.hidden)
.background(Theme.surface)
.safeAreaInset(edge: .bottom) {
Button(CloudWords.text("Continue")) {
Task { await consent.accept(app.site) }
}
.buttonStyle(FTWButtonStyle(kind: .primary))
.padding(16)
.background(Theme.surface)
}
.animation(Theme.spring, value: audience)
.sensoryFeedback(.success, trigger: consent.answered)
.interactiveDismissDisabled()
}
}
Loading
Loading