From bafa8c93770108547d55ea3ccf663a416056977b Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sat, 18 Jul 2026 00:17:11 +0530
Subject: [PATCH 01/14] App-store-readiness prep (Phase 1, legal-review-driven)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Non-contested items from the App Store/Play Store legal review: naming
hygiene, disclaimers, privacy policy draft, license attribution, OTA gating.
Does NOT touch the contract/ToS exposure item — that's separately pending
attorney review.
- Rename internal-only WHOOP-referencing identifiers to code-hygiene-neutral
names; the rendered UI text was already "Fitness age"/"FITNESS AGE" and is
unchanged:
- crossday_pipeline.dart / derivation_engine.dart: `whoop_age` bundle key
-> `fitness_age`
- coach_cards.dart: WhoopAgeCard/_WhoopAgeCardState -> FitnessAgeCard/
_FitnessAgeCardState; screens.dart usage site updated to match.
Left pairing_screen.dart / profile_screen.dart's import-source label /
import_screen.dart untouched — those are legitimate nominative/factual
WHOOP references (device name, import source), not branding.
- pubspec.yaml description: dropped "WHOOP 4.0 companion" wording, replaced
with an affiliation-neutral description ending in "Not affiliated with
WHOOP, Inc."
- New AffiliationDisclaimer widget (welcome_screen.dart), attorney-reviewed
wording, shown once on first-run onboarding (right under the welcome hero)
and reused verbatim in the new Settings -> About screen.
- New Settings -> About screen (lib/ui/profile/about_screen.dart), wired in
from profile_screen.dart's new "About" section:
- the affiliation/health-claim disclaimer
- "Privacy policy" link (placeholder hosted URL - see TODO below)
- "Notice & attribution" link (points at NOTICE.md on GitHub)
- "Open-source licenses" -> Flutter's built-in showLicensePage
- app name + version (via package_info_plus, already a dependency)
- PRIVACY.md at repo root: attorney-review-draft privacy policy per the
legal review's exact text. [DATE], [CONTACT EMAIL], and the data-deletion
mechanism are left as explicit placeholders for a human to fill in.
Flagged in-file and in about_screen.dart: both app stores require a
*hosted* URL, not a repo file — a GitHub Pages page (or a page on whatever
domain backs wtf.openstrap.*) still needs to be stood up before store
submission; kPrivacyPolicyUrl is a placeholder pointing nowhere real yet.
- NOTICE.md at repo root: consolidates the affiliation/no-copied-code/
methodology disclaimer already scattered across the three READMEs. No new
claims introduced beyond what protocol/README.md and edge/README.md
already say; deliberately says nothing about decompiling.
- OTA self-update now gated behind a compile-time flag, off by default:
- update_service.dart: new `kSideloadOtaEnabled` const
(`bool.fromEnvironment('ENABLE_SIDELOAD_OTA', defaultValue: false)`),
mirroring the build-time-URL convention already used by
backend_client.dart / companion_client.dart. `UpdateService.supported`
now requires the flag in addition to Platform.isAndroid.
- app_state.dart: `updateAvailable` / `updateMandatory` now also require
the flag, so the "Update available" banner never appears at all on a
store build (not just falls back to a browser link) - a Play Store or
future App Store build gets zero self-update surface unless built with
--dart-define=ENABLE_SIDELOAD_OTA=true.
License check: ran `flutter pub get` + `flutter pub deps --style=compact`,
then scanned every resolved package's LICENSE file (218 packages) for GPL/
AGPL. Zero real hits. The only "GPL" string matches were false positives:
MPL-2.0's boilerplate "Secondary Licenses" clause in bluez/dbus/nm/upower
(Linux-desktop-only D-Bus bindings, transitive via flutter_blue_plus, unused
on iOS/Android) and Flutter's own bundled third-party-notices file
(sky_engine, standard for every Flutter app, not specific to this project).
flutter analyze: 7 pre-existing info/warnings, none introduced by this
change (same deprecated-glassBoxReadiness item already tracked separately).
flutter test: all 457 tests pass.
---
NOTICE.md | 11 +++
PRIVACY.md | 57 +++++++++++++
lib/compute/crossday_pipeline.dart | 6 +-
lib/compute/derivation_engine.dart | 4 +-
lib/state/app_state.dart | 10 ++-
lib/sync/update_service.dart | 37 +++++++--
lib/ui/insights/coach_cards.dart | 14 ++--
lib/ui/onboarding/welcome_screen.dart | 20 +++++
lib/ui/profile/about_screen.dart | 115 ++++++++++++++++++++++++++
lib/ui/profile/profile_screen.dart | 15 ++++
lib/ui/screens/screens.dart | 2 +-
pubspec.yaml | 2 +-
12 files changed, 270 insertions(+), 23 deletions(-)
create mode 100644 NOTICE.md
create mode 100644 PRIVACY.md
create mode 100644 lib/ui/profile/about_screen.dart
diff --git a/NOTICE.md b/NOTICE.md
new file mode 100644
index 00000000..97c3ad5d
--- /dev/null
+++ b/NOTICE.md
@@ -0,0 +1,11 @@
+# Notice
+
+Edge is an independent, open-source project (MIT License — see `LICENSE`). It
+is not affiliated with, sponsored by, or endorsed by WHOOP, Inc. or any of its
+trademarks.
+
+No WHOOP source code, binaries, firmware, or copyrighted assets are included
+in this repository. The Bluetooth protocol support in this project was
+independently developed by observing the band's own Bluetooth communications;
+see [the protocol repo's README](https://github.com/OpenStrap/protocol) for
+methodology notes.
diff --git a/PRIVACY.md b/PRIVACY.md
new file mode 100644
index 00000000..0b6d3377
--- /dev/null
+++ b/PRIVACY.md
@@ -0,0 +1,57 @@
+> **ATTORNEY-REVIEW DRAFT — not final.** This has not been reviewed or approved
+> by a licensed attorney and must not be treated as the live policy until it
+> has been. `[DATE]`, `[CONTACT EMAIL]`, and the data-deletion mechanism below
+> are placeholders a human must fill in before this goes live. Both the App
+> Store and Play Store require a **hosted** URL for the privacy policy (not a
+> link to this repo file) — this still needs a GitHub Pages page (or a page on
+> whatever domain backs `wtf.openstrap.*`) before store submission. See
+> `lib/ui/profile/about_screen.dart`'s `kPrivacyPolicyUrl` TODO for the
+> in-app link that needs updating once that page exists.
+
+# Privacy Policy — Edge / OpenStrap
+
+_Last updated: [DATE]_
+
+Edge ("the App") is an independent, open-source project. It is not affiliated
+with, sponsored by, or endorsed by WHOOP, Inc.
+
+**Data we collect**
+- Biometric/health data from your paired WHOOP 4.0 band over Bluetooth: heart
+ rate, heart rate variability (RR intervals), motion (accelerometer), raw
+ skin-temperature/blood-oxygen sensor channels, and (if you record workouts)
+ GPS location during those workouts.
+- Basic app usage/crash telemetry, only if you opt in.
+
+**Where your data lives**
+By default, all of the above is stored locally on your device only. We do not
+upload it anywhere unless you explicitly opt in to one of the following:
+- **Cloud backup** — a one-time or periodic encrypted upload of your local
+ database to our own backend, only if you turn this on in Settings.
+- **AI Coach** — if you enable this feature and supply your own API key,
+ summaries of your data are sent to the AI provider you configure (by
+ default, OpenAI) to generate coaching responses. This is off by default and
+ requires your own API key.
+- **Health app integration** — if you enable it, the App can write derived
+ daily metrics to Apple Health or Google Health Connect, which are controlled
+ by your device's own OS-level health app, not by us.
+
+**What we don't do**
+We do not sell your data. We do not send your health data to WHOOP, Inc. or
+any advertising network. We do not require a WHOOP account or credentials to
+use the App.
+
+**Your controls**
+You can disable cloud backup, telemetry, or AI Coach at any time in Settings.
+[Describe how a user deletes their local data / requests deletion of any
+uploaded backup — INSERT MECHANISM].
+
+**Children**
+This App is not directed to children under 13 (or the relevant age of digital
+consent in your jurisdiction) and we do not knowingly collect data from them.
+
+**Changes**
+We may update this policy; material changes will be reflected here with an
+updated date.
+
+**Contact**
+Questions about this policy: [CONTACT EMAIL].
diff --git a/lib/compute/crossday_pipeline.dart b/lib/compute/crossday_pipeline.dart
index 5c70e3cb..e3931c95 100644
--- a/lib/compute/crossday_pipeline.dart
+++ b/lib/compute/crossday_pipeline.dart
@@ -222,8 +222,8 @@ Map buildCrossDayBundle(
tsb: ls?.tsb,
);
- // ── VO₂max + WHOOP-Age (physiological age). Resting VO₂max (Uth HRmax:RHR)
- // over the baseline RHR; WHOOP-Age composites VO₂max + RHR + HRV + sleep +
+ // ── VO₂max + Fitness Age (physiological age). Resting VO₂max (Uth HRmax:RHR)
+ // over the baseline RHR; Fitness Age composites VO₂max + RHR + HRV + sleep +
// steps vs age-norms. All ESTIMATE, absent on missing inputs.
final age = _numOrNull(profile['age']);
final sexStr = (profile['sex'] as String?)?.toLowerCase();
@@ -301,7 +301,7 @@ Map buildCrossDayBundle(
},
'strain_coach': strainTgt.toJson((v) => v.toJson()),
'vo2max': vo2.toJson(),
- 'whoop_age': physAge.toJson((v) => v.toJson()),
+ 'fitness_age': physAge.toJson((v) => v.toJson()),
'percentiles': percentiles,
'recent': recent,
};
diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart
index 511e337b..de34b758 100644
--- a/lib/compute/derivation_engine.dart
+++ b/lib/compute/derivation_engine.dart
@@ -101,7 +101,7 @@ import 'substrate.dart';
/// also tunes this estimate. Bumping re-derives non-finalized days so they fill.
// v20: principled nap detection (van Hees immobility + HR-dip) → `naps` block +
// `nap_min` scalar; cross-day Sleep Coach (need/bedtime/cycle-wake/performance),
-// Strain Coach (recovery-gated target), VO₂max + WHOOP-Age, all in the crossday
+// Strain Coach (recovery-gated target), VO₂max + Fitness Age, all in the crossday
// bundle.
// v21: all-day HRV line (`series.hrv_day`, epoch rolling RMSSD over 24/7 RR).
// v22: all-day RESP line (`series.resp_day`, rolling RSA br/min) + relative
@@ -1978,7 +1978,7 @@ class DerivationEngine {
'skin_temp_z': sc('skin_temp_z'),
'trimp': sc('trimp'),
// Headline 0–21 strain, daily steps, nap minutes — feed Sleep/Strain Coach
- // + VO₂max/WHOOP-Age in the cross-day rollup.
+ // + VO₂max/Fitness Age in the cross-day rollup.
'strain': sc('strain'),
'steps': sc('steps'),
'nap_min': sc('nap_min'),
diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart
index 4746fac7..c855fa98 100644
--- a/lib/state/app_state.dart
+++ b/lib/state/app_state.dart
@@ -517,14 +517,22 @@ class AppState extends ChangeNotifier {
UpdateInfo? get _update => appStatus?.update;
/// A newer build is published (we're behind latest_build).
+ ///
+ /// Gated on [kSideloadOtaEnabled] regardless of platform: store builds
+ /// (Play Store / App Store) must never surface a self-update prompt at
+ /// all, not just fall back to a browser link — see update_service.dart.
bool get updateAvailable =>
+ kSideloadOtaEnabled &&
_update != null &&
_currentBuild > 0 &&
_update!.latestBuild > _currentBuild;
/// We're below the mandatory floor — the prompt can't be dismissed.
bool get updateMandatory =>
- _update != null && _currentBuild > 0 && _currentBuild < _update!.minBuild;
+ kSideloadOtaEnabled &&
+ _update != null &&
+ _currentBuild > 0 &&
+ _currentBuild < _update!.minBuild;
UpdateInfo? get update => _update;
diff --git a/lib/sync/update_service.dart b/lib/sync/update_service.dart
index c9ff0e9c..60a063ea 100644
--- a/lib/sync/update_service.dart
+++ b/lib/sync/update_service.dart
@@ -1,10 +1,15 @@
-// UpdateService — the Android OTA mechanics. There's no app store in the loop
-// (the app is sideloaded), so we ARE the update channel: download the signed APK
-// from the backend's update pointer and hand it to the system installer. The new
-// APK must be signed with the same release key or Android refuses the update —
-// which is already true for our CI-built GitHub releases.
+// UpdateService — the Android OTA mechanics. On a direct-distribution/sideload
+// build there's no app store in the loop, so we ARE the update channel:
+// download the signed APK from the backend's update pointer and hand it to the
+// system installer. The new APK must be signed with the same release key or
+// Android refuses the update — which is already true for our CI-built GitHub
+// releases.
//
-// iOS can't sideload-install, so [supported] is false there and the UI hides OTA.
+// iOS can't sideload-install, so [supported] is false there and the UI hides
+// in-app install (falls back to a browser link) regardless of the flag below.
+//
+// Store builds (Play Store, and any future App Store build) must NOT offer
+// self-update at all — see [kSideloadOtaEnabled].
import 'dart:convert';
import 'dart:io';
@@ -16,6 +21,21 @@ import 'package:url_launcher/url_launcher.dart';
import '../cloud/companion_client.dart';
import '../models/app_status.dart';
+/// Compile-time gate for the whole self-update feature (the "Update
+/// available" banner AND the in-app APK download/install flow). Defaults OFF
+/// so the open-source repo — and any store submission built from it without
+/// extra flags — never bakes in self-update: app-store review does not allow
+/// an app to silently install another build of itself outside the store.
+///
+/// A direct-distribution/sideload build opts in explicitly:
+/// flutter build apk --dart-define=ENABLE_SIDELOAD_OTA=true
+///
+/// Mirrors the build-time-URL convention in lib/cloud/backend_client.dart /
+/// lib/cloud/companion_client.dart — the public repo must not bake a specific
+/// distribution channel in by default.
+const bool kSideloadOtaEnabled =
+ bool.fromEnvironment('ENABLE_SIDELOAD_OTA', defaultValue: false);
+
/// A coarse progress event the UI renders.
class OtaProgress {
final String phase; // 'downloading' | 'installing' | 'error'
@@ -25,8 +45,9 @@ class OtaProgress {
}
class UpdateService {
- /// Only Android can install an APK in-app.
- static bool get supported => Platform.isAndroid;
+ /// Only Android can install an APK in-app, and only on a sideload build
+ /// that opted into [kSideloadOtaEnabled].
+ static bool get supported => Platform.isAndroid && kSideloadOtaEnabled;
/// Fetch the OTA update pointer + admin alert banner from the companion
/// backend's public, UNAUTHENTICATED `GET /app/status` — the single URL the app
diff --git a/lib/ui/insights/coach_cards.dart b/lib/ui/insights/coach_cards.dart
index 7add2d61..3053fd83 100644
--- a/lib/ui/insights/coach_cards.dart
+++ b/lib/ui/insights/coach_cards.dart
@@ -1,5 +1,5 @@
// Coach + fitness cards — surface the cross-day coaching/fitness analytics
-// (Sleep Coach, Strain Coach, WHOOP-Age) and the journal correlation engine
+// (Sleep Coach, Strain Coach, Fitness Age) and the journal correlation engine
// ("Performance Assessment"). All read the precomputed `crossday` bundle via
// repo.getInsights() (or getJournalInsights), so they do ZERO heavy compute on
// read. Honest: every value traces to a tested analytics Metric; absent → a
@@ -276,16 +276,16 @@ class _StrainCoachCardState extends State {
}
}
-// ── WHOOP-AGE / fitness ──────────────────────────────────────────────────────
+// ── FITNESS AGE ──────────────────────────────────────────────────────────────
/// Physiological "age" vs chronological + VO₂max estimate.
-class WhoopAgeCard extends StatefulWidget {
- const WhoopAgeCard({super.key});
+class FitnessAgeCard extends StatefulWidget {
+ const FitnessAgeCard({super.key});
@override
- State createState() => _WhoopAgeCardState();
+ State createState() => _FitnessAgeCardState();
}
-class _WhoopAgeCardState extends State {
+class _FitnessAgeCardState extends State {
Map? _age;
num? _vo2;
bool _loading = true;
@@ -301,7 +301,7 @@ class _WhoopAgeCardState extends State {
final cd = await context.read().repo?.getInsights();
if (!mounted) return;
setState(() {
- _age = _val(cd?['whoop_age']);
+ _age = _val(cd?['fitness_age']);
final v = cd?['vo2max'];
_vo2 = v is Map ? v['value'] as num? : null;
_loading = false;
diff --git a/lib/ui/onboarding/welcome_screen.dart b/lib/ui/onboarding/welcome_screen.dart
index 4005cbce..3ba3f51a 100644
--- a/lib/ui/onboarding/welcome_screen.dart
+++ b/lib/ui/onboarding/welcome_screen.dart
@@ -154,6 +154,8 @@ class _WelcomeScreenState extends State {
physics: const BouncingScrollPhysics(),
children: dsStaggered([
const WelcomeHero(),
+ const SizedBox(height: Sp.x4),
+ const AffiliationDisclaimer(),
const SizedBox(height: Sp.x6),
Text('How do you want to start?', style: AppText.h2),
const SizedBox(height: Sp.x4),
@@ -375,6 +377,24 @@ class WelcomeHero extends StatelessWidget {
}
}
+/// Legal/affiliation disclaimer shown once at first run (and mirrored in
+/// Settings → About — see [AboutScreen]). Attorney-reviewed wording; don't
+/// paraphrase without re-running it past the same review.
+class AffiliationDisclaimer extends StatelessWidget {
+ const AffiliationDisclaimer({super.key});
+
+ static const String text =
+ 'Edge is an independent, open-source project. It is not affiliated '
+ 'with, sponsored by, or endorsed by WHOOP, Inc. You must own a genuine '
+ 'WHOOP 4.0 band to use this app. Health metrics are wellness estimates '
+ 'from published research methods — not medical diagnoses.';
+
+ @override
+ Widget build(BuildContext context) {
+ return Text(text, style: AppText.captionMuted);
+ }
+}
+
/// One onboarding choice — a SurfaceCard row: icon-in-tile, title, one quiet
/// line, chevron. [accent] marks the recommended path with the ember tint.
class WelcomeOptionCard extends StatelessWidget {
diff --git a/lib/ui/profile/about_screen.dart b/lib/ui/profile/about_screen.dart
new file mode 100644
index 00000000..ef90841c
--- /dev/null
+++ b/lib/ui/profile/about_screen.dart
@@ -0,0 +1,115 @@
+// about_screen.dart — Settings → About. Surfaces the affiliation/health-claim
+// disclaimer (same attorney-reviewed wording shown once at first run — see
+// AffiliationDisclaimer in lib/ui/onboarding/welcome_screen.dart), links to the
+// Privacy Policy and NOTICE, the open-source license attribution page, and the
+// installed app version. Added as part of app-store-readiness prep; the
+// disclaimer wording must not be edited without re-running it past legal
+// review.
+
+import 'package:flutter/material.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+import '../design/design.dart';
+import '../onboarding/welcome_screen.dart' show AffiliationDisclaimer;
+
+/// TODO(legal): both app stores require a *hosted* URL, not a repo file. Point
+/// this at the GitHub Pages (or wtf.openstrap.* domain) page once PRIVACY.md
+/// is published there — see PRIVACY.md's own TODO for the same gap.
+const String kPrivacyPolicyUrl = 'https://openstrap.github.io/edge/PRIVACY.md';
+
+const String kNoticeUrl =
+ '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/OpenStrap/edge/blob/main/NOTICE.md';
+
+class AboutScreen extends StatefulWidget {
+ const AboutScreen({super.key});
+
+ @override
+ State createState() => _AboutScreenState();
+}
+
+class _AboutScreenState extends State {
+ PackageInfo? _pkg;
+
+ @override
+ void initState() {
+ super.initState();
+ PackageInfo.fromPlatform().then((p) {
+ if (mounted) setState(() => _pkg = p);
+ });
+ }
+
+ static Future _openUrl(String url) async {
+ try {
+ await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
+ } catch (_) {}
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final versionLabel = _pkg == null
+ ? null
+ : '${_pkg!.version}+${_pkg!.buildNumber}';
+
+ return AppScaffold(
+ title: 'About',
+ children: [
+ const SizedBox(height: Sp.x4),
+ SurfaceCard(
+ padding: const EdgeInsets.all(Sp.x4),
+ child: const AffiliationDisclaimer(),
+ ),
+ const SizedBox(height: Sp.x6),
+ const SectionHeader('Legal'),
+ _SettingsCard(rows: [
+ ListRow(
+ icon: OsIcon.activity,
+ title: 'Privacy policy',
+ divider: true,
+ onTap: () => _openUrl(kPrivacyPolicyUrl),
+ ),
+ ListRow(
+ icon: OsIcon.activity,
+ title: 'Notice & attribution',
+ subtitle: 'Trademark + reverse-engineering disclosure',
+ divider: true,
+ onTap: () => _openUrl(kNoticeUrl),
+ ),
+ ListRow(
+ icon: OsIcon.activity,
+ title: 'Open-source licenses',
+ onTap: () => showLicensePage(
+ context: context,
+ applicationName: 'Edge',
+ applicationVersion: versionLabel,
+ ),
+ ),
+ ]),
+ const SizedBox(height: Sp.x6),
+ const SectionHeader('Version'),
+ _SettingsCard(rows: [
+ ListRow(
+ icon: OsIcon.activity,
+ title: 'Edge',
+ value: versionLabel ?? '…',
+ ),
+ ]),
+ const SizedBox(height: Sp.x8),
+ ],
+ );
+ }
+}
+
+/// Local copy of ProfileScreen's private `_SettingsCard` shape (a SurfaceCard
+/// wrapping stacked ListRows) — kept tiny and duplicated rather than exporting
+/// the private widget across files for one screen.
+class _SettingsCard extends StatelessWidget {
+ final List rows;
+ const _SettingsCard({required this.rows});
+
+ @override
+ Widget build(BuildContext context) => SurfaceCard(
+ padding: const EdgeInsets.symmetric(horizontal: Sp.x4, vertical: Sp.x1),
+ child: Column(children: rows),
+ );
+}
diff --git a/lib/ui/profile/profile_screen.dart b/lib/ui/profile/profile_screen.dart
index 0431efe5..6dfe330c 100644
--- a/lib/ui/profile/profile_screen.dart
+++ b/lib/ui/profile/profile_screen.dart
@@ -25,6 +25,7 @@ import '../design/design.dart';
import '../design/gallery_screen.dart';
import '../import/import_screen.dart';
import '../today/step_goal_screen.dart';
+import 'about_screen.dart';
import 'advanced_data_screen.dart';
import 'data_history_screen.dart';
import 'gesture_section.dart';
@@ -422,6 +423,20 @@ class ProfileScreen extends StatelessWidget {
const SizedBox(height: Sp.x6),
+ // ── About ────────────────────────────────────────────────────
+ const SectionHeader('About'),
+ _SettingsCard(rows: [
+ ListRow(
+ icon: OsIcon.activity,
+ title: 'About Edge',
+ subtitle: 'Affiliation, privacy policy, licenses',
+ onTap: () => Navigator.of(context).push(
+ themedRoute((_) => const AboutScreen(), name: 'AboutScreen')),
+ ),
+ ]),
+
+ const SizedBox(height: Sp.x6),
+
// ── Reset ────────────────────────────────────────────────────
const SectionHeader('Reset'),
_SettingsCard(rows: [
diff --git a/lib/ui/screens/screens.dart b/lib/ui/screens/screens.dart
index 362f98fc..93acdeab 100644
--- a/lib/ui/screens/screens.dart
+++ b/lib/ui/screens/screens.dart
@@ -148,7 +148,7 @@ class BodyScreen extends StatelessWidget {
const SizedBox(height: Sp.x3),
StrainDetailScreen(date: todayLabel(), embedded: true),
const SizedBox(height: Sp.x3),
- const WhoopAgeCard(),
+ const FitnessAgeCard(),
const SizedBox(height: Sp.x3),
const PerformanceAssessmentCard(),
const CycleEntryCard(),
diff --git a/pubspec.yaml b/pubspec.yaml
index 8db63312..1114dc96 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: openstrap_edge
-description: "OpenStrap — open-source WHOOP 4.0 companion. BLE drain → raw-first local store → cloud sync."
+description: "Open-source, local-first fitness tracking companion for WHOOP 4.0 hardware. Not affiliated with WHOOP, Inc."
publish_to: 'none'
version: 0.9.14+47
From a6bf39ecb900692e778ebca744b0431c9f582a7e Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sat, 18 Jul 2026 12:21:18 +0530
Subject: [PATCH 02/14] Point kPrivacyPolicyUrl at the live hosted Privacy
Policy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
GitHub Pages is now live for docs/legal-site (maintainer fixed the
Settings -> Pages source branch). Updates the placeholder from Phase 1:
- about_screen.dart: kPrivacyPolicyUrl ->
https://openstrap.github.io/edge/privacy.html (was the placeholder
.../edge/PRIVACY.md, which pointed at a repo file, not a hosted page).
Removed the now-resolved TODO(legal) comment on the constant.
- PRIVACY.md: removed the "still needs a GitHub Pages page" note (resolved)
and pointed at the live URL as the version to keep current going forward.
The DATE/CONTACT EMAIL/data-deletion-mechanism placeholders in THIS repo
copy are untouched and still open — only the hosted-URL gap is resolved
here; the docs/legal-site branch's privacy.html already has those filled
in separately.
---
PRIVACY.md | 12 ++++++------
lib/ui/profile/about_screen.dart | 7 +++----
2 files changed, 9 insertions(+), 10 deletions(-)
diff --git a/PRIVACY.md b/PRIVACY.md
index 0b6d3377..132635ed 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -1,12 +1,12 @@
> **ATTORNEY-REVIEW DRAFT — not final.** This has not been reviewed or approved
> by a licensed attorney and must not be treated as the live policy until it
> has been. `[DATE]`, `[CONTACT EMAIL]`, and the data-deletion mechanism below
-> are placeholders a human must fill in before this goes live. Both the App
-> Store and Play Store require a **hosted** URL for the privacy policy (not a
-> link to this repo file) — this still needs a GitHub Pages page (or a page on
-> whatever domain backs `wtf.openstrap.*`) before store submission. See
-> `lib/ui/profile/about_screen.dart`'s `kPrivacyPolicyUrl` TODO for the
-> in-app link that needs updating once that page exists.
+> are placeholders a human must fill in before this goes live. This is the
+> repo copy for reference; the live, store-linked policy is hosted at
+> https://openstrap.github.io/edge/privacy.html (GitHub Pages, `docs/`
+> folder on the `docs/legal-site` branch) and is the version that should be
+> kept current — `lib/ui/profile/about_screen.dart`'s `kPrivacyPolicyUrl`
+> already points at it.
# Privacy Policy — Edge / OpenStrap
diff --git a/lib/ui/profile/about_screen.dart b/lib/ui/profile/about_screen.dart
index ef90841c..59acfe2a 100644
--- a/lib/ui/profile/about_screen.dart
+++ b/lib/ui/profile/about_screen.dart
@@ -13,10 +13,9 @@ import 'package:url_launcher/url_launcher.dart';
import '../design/design.dart';
import '../onboarding/welcome_screen.dart' show AffiliationDisclaimer;
-/// TODO(legal): both app stores require a *hosted* URL, not a repo file. Point
-/// this at the GitHub Pages (or wtf.openstrap.* domain) page once PRIVACY.md
-/// is published there — see PRIVACY.md's own TODO for the same gap.
-const String kPrivacyPolicyUrl = 'https://openstrap.github.io/edge/PRIVACY.md';
+/// Hosted Privacy Policy (GitHub Pages, docs/legal-site). Both app stores
+/// require a live hosted URL, not a repo file — this is that URL.
+const String kPrivacyPolicyUrl = 'https://openstrap.github.io/edge/privacy.html';
const String kNoticeUrl =
'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/OpenStrap/edge/blob/main/NOTICE.md';
From 9f8b88e591a9fb2b35ce82dc2af6519e242ea502 Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sat, 18 Jul 2026 13:04:57 +0530
Subject: [PATCH 03/14] iOS submission prep: scrub hardcoded Team ID + sync
Widget/Watch versions
Three fixes from the readiness audit:
1. Scrub the hardcoded personal Team ID (2U62X3RF3R) from the "OpenStrapWatch
Watch App" target's Debug/Release/Profile build configs in project.pbxproj
-> replaced with $(APPLE_DEVELOPMENT_TEAM), matching every other target
(Runner/RunnerTests/OpenStrapWidgetExtension already used the variable).
Confirmed via `git show HEAD` this was committed, not a local artifact.
2. AccessorySetupKit entitlement - researched via Apple's current docs + an
independent developer writeup (both converge): NO special entitlement
(e.g. com.apple.developer.accessory-setup-kit) is required for
AccessorySetupKit. It's driven entirely by Info.plist keys
(NSAccessorySetupKitSupports, NSAccessorySetupBluetoothServices), which
Runner/Info.plist already has correctly configured. No entitlement change
made - the existing Runner.entitlements (App Groups + HealthKit only) is
correct as-is.
3. Synced OpenStrapWidget(Extension) and "OpenStrapWatch Watch App" target
versions (previously stuck at 1.0/1 and 0.7.0/17 respectively) to
0.9.14/47, matching the current pubspec.yaml version. These two targets
aren't wired to Generated.xcconfig (only Runner's target-level Debug/
Release configs include it), so $(FLUTTER_BUILD_NAME)/$(FLUTTER_BUILD_NUMBER)
aren't available to them - wiring that in would require adding a new
baseConfigurationReference to targets that also inherit Flutter-runner-
specific settings (arch exclusions, framework search paths) I can't verify
are safe without an actual Xcode build. Took the safer path instead:
hardcode-bumped to match, and documented the manual-sync requirement in
guides/IOS_INSTALLATION.md ("Version Numbers" section) + a NOTE comment
next to pubspec.yaml's version: field, so the next release bump doesn't
silently let these drift again.
RunnerTests' own CURRENT_PROJECT_VERSION/MARKETING_VERSION (1/1.0) intentionally
left untouched - it's a test bundle, never shipped, out of scope here.
---
guides/IOS_INSTALLATION.md | 16 +++++++++++++++
ios/Runner.xcodeproj/project.pbxproj | 30 ++++++++++++++--------------
pubspec.yaml | 5 +++++
3 files changed, 36 insertions(+), 15 deletions(-)
diff --git a/guides/IOS_INSTALLATION.md b/guides/IOS_INSTALLATION.md
index 8bb1ed47..8fccac7f 100644
--- a/guides/IOS_INSTALLATION.md
+++ b/guides/IOS_INSTALLATION.md
@@ -126,6 +126,22 @@ For a signed release-style device install:
flutter run --release -d --dart-define-from-file=.env
```
+## Version Numbers
+
+`Runner`'s `MARKETING_VERSION`/`CURRENT_PROJECT_VERSION` track `pubspec.yaml`'s
+`version:` automatically via `$(FLUTTER_BUILD_NAME)`/`$(FLUTTER_BUILD_NUMBER)`
+(Flutter writes these into `ios/Flutter/Generated.xcconfig` on every build).
+
+The **`OpenStrapWidget`/`OpenStrapWidgetExtension`** and **`OpenStrapWatch Watch
+App`** targets are NOT wired to that mechanism — they don't include
+`Generated.xcconfig`, so those Flutter variables aren't available to them, and
+`MARKETING_VERSION`/`CURRENT_PROJECT_VERSION` are hardcoded per-target in
+`project.pbxproj` instead. **Bump these by hand to match `pubspec.yaml`'s
+`version:` every time it changes** — App Store Connect validates that a host
+app's embedded extensions and companion Watch app carry compatible version
+numbers, so letting these drift is a real upload-time risk, not just cosmetic
+inconsistency.
+
## Debug Builds and Home-Screen Relaunch
Flutter debug builds on iOS must be launched by Flutter tooling or Xcode. If you
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 4329a129..a0bdb4b2 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -833,7 +833,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 47;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -849,7 +849,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 0.9.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
@@ -880,7 +880,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 47;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -896,7 +896,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 0.9.14;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -924,7 +924,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 47;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -940,7 +940,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 0.9.14;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -971,8 +971,8 @@
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 17;
- DEVELOPMENT_TEAM = 2U62X3RF3R;
+ CURRENT_PROJECT_VERSION = 47;
+ DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -987,7 +987,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 0.7.0;
+ MARKETING_VERSION = 0.9.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
@@ -1026,8 +1026,8 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 17;
- DEVELOPMENT_TEAM = 2U62X3RF3R;
+ CURRENT_PROJECT_VERSION = 47;
+ DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1042,7 +1042,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 0.7.0;
+ MARKETING_VERSION = 0.9.14;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1077,8 +1077,8 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 17;
- DEVELOPMENT_TEAM = 2U62X3RF3R;
+ CURRENT_PROJECT_VERSION = 47;
+ DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1093,7 +1093,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 0.7.0;
+ MARKETING_VERSION = 0.9.14;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
PRODUCT_NAME = "$(TARGET_NAME)";
diff --git a/pubspec.yaml b/pubspec.yaml
index 1114dc96..5a5ab5b9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,11 @@
name: openstrap_edge
description: "Open-source, local-first fitness tracking companion for WHOOP 4.0 hardware. Not affiliated with WHOOP, Inc."
publish_to: 'none'
+# NOTE: bumping this also requires manually bumping MARKETING_VERSION/
+# CURRENT_PROJECT_VERSION for the OpenStrapWidget(Extension) and "OpenStrapWatch
+# Watch App" targets in ios/Runner.xcodeproj/project.pbxproj — they aren't wired
+# to FLUTTER_BUILD_NAME/FLUTTER_BUILD_NUMBER. See guides/IOS_INSTALLATION.md
+# "Version Numbers".
version: 0.9.14+47
environment:
From 7c6adc7e1cd15ffba1a907aa687f7f2a80f05470 Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sat, 18 Jul 2026 19:37:52 +0530
Subject: [PATCH 04/14] Offload derive second half to isolate; fix 3 fatal
crashes; readiness bump
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Freeze fix: the entire second half of _derivePreparedDay (all-day HRV/RSA
Timeline curves, naps, workout detection, wake features, steps/energy) ran on
the CALLING isolate — the UI isolate for the foreground light pass fired on
every sync — hanging the main thread for seconds. Confirmed via Crashlytics:
iOS 6.1s jank_watchdog + Android __kernel_rem_pio2/cos main-thread ANRs, fresh
in v0.9.13. Moved it into a second Isolate.run (pure methods made static; DB
reads done first, DB writes/notifications applied after) under _perDayTimeout,
and made it NON-FATAL so a failure persists the headline day (readiness intact)
instead of skipping it — the "readiness randomly goes -" path.
Crash fixes (Crashlytics FATAL triage):
- db.dart: LocalDb.instance reopens when the cached handle isn't open. Android
closes the SQLiteDatabase on background teardown without nulling _db, so every
write threw "attempt to re-open an already-closed object" (91-event burst).
- route_map.dart: drop non-finite GPS coords + only bounds-fit a non-zero-area
box (else fixed-zoom center) — degenerate/NaN routes crashed flutter_map's
tile layer (NaN.toInt in _clampToNativeZoom).
- coach_settings_screen.dart: capture navigator before the await + guard pop
with canPop() — bare Navigator.pop() after await threw "Bad state: No element".
Readiness: bump kAlgoVersion 42->43 for the analytics MAD/SD-z fallback so
previously-blanked days recompute; add a tightly-gated queryable Crashlytics
non-fatal for any residual readiness_absent (non-cold-start) case.
---
lib/compute/derivation_engine.dart | 618 +++++++++++++++---------
lib/data/db.dart | 11 +-
lib/data/local_repository_impl.dart | 146 +++---
lib/ui/coach/coach_settings_screen.dart | 16 +-
lib/ui/kit/route_map.dart | 24 +-
5 files changed, 512 insertions(+), 303 deletions(-)
diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart
index de34b758..466fc7f3 100644
--- a/lib/compute/derivation_engine.dart
+++ b/lib/compute/derivation_engine.dart
@@ -210,7 +210,12 @@ import 'substrate.dart';
// over time while per-night-local always leads. Deep stays a low-confidence
// NREM sub-split (deep_low_confidence). The profile self-seeds across this
// re-derivation sweep; no explicit migration. Bump so every day re-stages.
-const int kAlgoVersion = 42;
+// v43: readinessComposite now falls back to a mean/SD z when the robust (median
+// +MAD) z is degenerate (MAD==0 on a tightly-clustered quantized baseline —
+// whole-bpm RHR / integer skin-temp ADC), which was intermittently blanking the
+// whole readiness score to "—" on nights that had valid sleep. Bump so days that
+// were previously absent-for-that-reason recompute a real score.
+const int kAlgoVersion = 43;
/// Raw is kept this many days past derivation, then pruned (derived stays).
const int rawRetentionDays = 3;
@@ -1397,6 +1402,28 @@ class DerivationEngine {
message: 'readiness_absent',
context: flat,
);
+ // Also surface the SURPRISING case — readiness absent when it should NOT
+ // be (adequate inputs, not the honest cold-start `need_baseline` note) —
+ // as a queryable Crashlytics non-fatal, so a residual "readiness '—' even
+ // with sleep present" is diagnosable from the per-input flags WITHOUT GA4
+ // access. Cold-start (need_baseline) absences stay Analytics-only so this
+ // stays low-noise (and, post the MAD/SD-z fallback, rare).
+ final note = (diag['note'] as String?) ?? '';
+ if (!note.startsWith('need_baseline')) {
+ final summary = StringBuffer('readiness_absent');
+ for (final key in ['hrv', 'rhr', 'resp', 'temp']) {
+ final v = (diag[key] as Map?)?.cast();
+ if (v == null) continue;
+ summary.write(
+ ' $key=${v['value'] == true ? 'Y' : 'n'}/${v['baseline_n']}');
+ }
+ summary.write(' | $note');
+ TelemetryService.instance.recordNonFatal(
+ StateError(summary.toString()),
+ StackTrace.current,
+ reason: 'readiness_absent',
+ );
+ }
}
// Where this day's sleep window came from (auto / auto_fallback / manual /
@@ -1405,142 +1432,94 @@ class DerivationEngine {
bundle['sleep_source'] = day.sleepSource;
final scMap = (bundle['scalars'] as Map?)?.cast();
- final wake = _buildWakeDayFeatures(
- daySub,
- profile,
- sleepOnsetSec: day.sleepOnsetSec,
- sleepOffsetSec: day.sleepOffsetSec,
- restingHr: (scMap?['rhr'] as num?)?.toDouble(),
- );
- _applyWakeDayFeatures(bundle, scMap, wake);
-
- // ── STEPS (real 100 Hz + 1 Hz estimate) + TOTAL DAILY ENERGY (TDEE) ───────
- // Pull the day's 100 Hz coverage windows (device-time sec) + their real step
- // count so _stepsAndEnergy can prefer them and estimate only the rest.
- final dayLo = daySub.length == 0 ? 0 : daySub.tsSec.first;
- final dayHi = daySub.length == 0 ? 0 : daySub.tsSec.last + 60;
- final coverageWindows = await LocalDb.coverageWindowsOverlapping(
- dayLo,
- dayHi,
- );
- final liveStepsReal = await LocalDb.liveStepsForDay(day.date);
- final stepCalib = await LocalDb.getStepCalibration();
- _stepsAndEnergy(
- bundle,
- scMap,
- daySub,
- profile,
- coverageWindows,
- liveStepsReal,
- stepCalib,
- );
- // ── More substrate-derived detail blocks (computed here, where the full
- // sliced substrate lives — same pattern as activeMin; these are fresh
- // blocks so ints are safe, unlike the double? scalars). ─
- bundle['daytime_hrv'] = _daytimeHrv(
- daySub,
- day.sleepOnsetSec,
- day.sleepOffsetSec,
- ); // waking RMSSD
- // ALL-DAY HRV / resp / skin-temp lines for the Timeline graph. Epoch-stamped
- // (rolling RMSSD, rolling RSA, relative skin-temp) over the day-wide 24/7
- // substrate RR/ADC — context lines, movement-confounded, not the recovery
- // values. Computed here where the day substrate lives.
- final sersMap = (bundle['series'] as Map?)?.cast();
- sersMap?['hrv_day'] = _dayHrvCurve(daySub);
- sersMap?['resp_day'] = _dayRespCurve(daySub);
- sersMap?['skin_temp_day'] = _daySkinTempCurve(daySub);
- bundle['restlessness'] = _restlessness(sleepSub); // nocturnal movement
- bundle['sleep_periods'] = _sleepPeriods(
- daySub,
- day.sleepOnsetSec,
- day.sleepOffsetSec,
- ); // naps
- // Principled daytime NAPS (van Hees immobility + HR-dip): rich per-nap block
- // + a `nap_min` scalar feeding the Sleep Coach's nap credit + the Timeline.
- _attachNaps(bundle, scMap, daySub, day.sleepOnsetSec, day.sleepOffsetSec);
-
- // Per-5-min movement-level curve for the "Your day" Movement view ([{t,v}],
- // v = fraction of moving seconds 0..1). Fresh top-level list — no typed-map.
- bundle['activity_curve'] = _activityCurve(daySub);
-
- // ── AUTO-DETECTED WORKOUTS (WorkoutDetector, 1 Hz HR + gravity) ──────────
- // Retroactive bout detection over the day's HR + gravity (lives in daySub,
- // not the bundle input). Per-bout strain/zones/calories via the analytics
- // family. OVERLAP-DEDUP: any manual/live session for this day is passed as a
- // saved span so a detected bout overlapping it is dropped (manual wins). The
- // sport seam stays default ("detected") — OpenStrap's HAR typer can be wired
- // in once high-rate accel features exist.
- bundle['detected_workouts'] = await _detectedWorkouts(daySub, day, profile);
-
- // ── AUTO-WORKOUT SUGGESTIONS + HRR (opt-in detector over day HR+motion) ──
- // One pass of the opt-in detector serves two features: (1) persisted "did you
- // work out?" suggestions + a recent-day notification, and (2) heart-rate
- // recovery (HRR) per bout → `hrr_bpm` scalar. scMap writes through to
- // bundle['scalars'] (CastMap view), so hrr_bpm reaches the series block below.
- await _attachWorkoutSuggestionsAndHrr(bundle, scMap, daySub, day, dataNowSec);
-
- // ── WRIST ORIENTATION during sleep (low-confidence; NOT body position) ───
- _attachWristOrientation(
- bundle, daySub, day.sleepOnsetSec, day.sleepOffsetSec);
-
- // ── ADVANCED SLEEP (4-class Cole–Kripke + DoG/percentile stager) ─────────
- // A richer, AASM-style sleep read (SOL / REM-latency / disturbances + a
- // 4-class hypnogram) computed over the day substrate (needs accel, which
- // lives here, not in the bundle input). ADDITIVE: the canonical single-source
- // `sleep` block (from segmentSleep) is the headline; this is a parallel
- // ESTIMATE detail. Best-effort — never throws.
- bundle['advanced_sleep'] = await _advancedSleep(daySub);
-
- // Feature 6: Restlessness Map (Heatmap of Sleep)
- if (sleepSub.length > 0) {
- final bucketSec = 300; // 5 min
- final moveSum = {};
- final moveCount = {};
- for (var i = 0; i < sleepSub.length; i++) {
- final b = sleepSub.tsSec[i] ~/ bucketSec;
- final ax = sleepSub.ax[i];
- final ay = sleepSub.ay[i];
- final az = sleepSub.az[i];
- final mag = math.sqrt(ax * ax + ay * ay + az * az);
- final enmo = (mag - 1.0).abs();
- moveSum[b] = (moveSum[b] ?? 0.0) + enmo;
- moveCount[b] = (moveCount[b] ?? 0) + 1;
+ // ── SECOND HALF — OFFLOADED to a background isolate ──────────────────────
+ // Everything that turns the isolate-1 bundle into the full day result (wake
+ // features, hybrid steps + TDEE, all-day HRV/RSA/skin-temp Timeline lines,
+ // naps, workout detection + HRR, wrist orientation, restlessness map, fit
+ // quality) used to run on the CALLING isolate — the UI isolate for the
+ // foreground light pass that fires on every sync — hanging the main thread
+ // for seconds (the rolling-RSA Lomb-Scargle over the 24 h day + nap
+ // re-staging + workout detection are the trig/CPU hogs). It is all PURE
+ // compute over the two substrates + a few scalars, so it now runs in
+ // Isolate.run. DB reads that it needs are done HERE (this is the DB-owning
+ // isolate); the DB writes + notification it produces are returned as
+ // descriptors and applied below. Same _perDayTimeout guard as isolate 1.
+ // NON-FATAL: a failure OR timeout anywhere in the offloaded second half must
+ // never skip the whole day. Isolate 1 already computed the headline scalars
+ // (readiness / RHR / RMSSD) into bundle['scalars']; we persist those and just
+ // drop the optional detail blocks. (Previously an exception here threw out of
+ // _derivePreparedDay → the day was marked skipped → readiness rendered "-"
+ // even though it had been computed fine — the "readiness randomly goes -" bug.)
+ try {
+ final dayLo = daySub.length == 0 ? 0 : daySub.tsSec.first;
+ final dayHi = daySub.length == 0 ? 0 : daySub.tsSec.last + 60;
+ final coverageWindows =
+ await LocalDb.coverageWindowsOverlapping(dayLo, dayHi);
+ final liveStepsReal = await LocalDb.liveStepsForDay(day.date);
+ final stepCalib = await LocalDb.getStepCalibration();
+ final savedSessions = await LocalDb.sessionsInRange(dayLo, dayHi);
+
+ // Built on THIS isolate so the Isolate.run closure captures only this plain
+ // sendable object (never `this`, `day`, or `bundle`).
+ final blocksInput = _DayBlocksInput(
+ daySub: daySub,
+ sleepSub: sleepSub,
+ profile: profile,
+ onsetSec: day.sleepOnsetSec,
+ offsetSec: day.sleepOffsetSec,
+ rhr: (scMap?['rhr'] as num?)?.toDouble(),
+ maxHrUsed: (bundle['max_hr_used'] as num?)?.round(),
+ coverageWindows: coverageWindows,
+ liveStepsReal: liveStepsReal,
+ stepCalib: stepCalib,
+ savedSessions: savedSessions,
+ date: day.date,
+ dayEndSec: day.endSec,
+ dataNowSec: dataNowSec,
+ );
+ final blocks = await Isolate.run(() => _computeDayBlocks(blocksInput))
+ .timeout(_perDayTimeout);
+
+ // Merge the computed blocks back into the isolate-1 bundle. scMap is the
+ // CastMap view over bundle['scalars'], so addAll writes through — nap_min /
+ // hrr_bpm reach the persisted series map below.
+ bundle.addAll(blocks.bundlePatch);
+ (bundle['series'] as Map?)?.cast().addAll(
+ blocks.seriesPatch,
+ );
+ scMap?.addAll(blocks.scalarPatch);
+
+ // DB writes + notification the pure compute deferred to us (DB-owning isolate).
+ for (final w in blocks.sessionHrrWrites) {
+ await LocalDb.setSessionHrr(w.$1, w.$2);
}
- final out =
Your controls
- You can disable cloud backup, telemetry, or AI Coach at any time in Settings.
- Uninstalling the App deletes all of its local data from your device — there is
- no separate local-data export left behind. In-app, Settings → "Reset profile"
- clears your local profile (name, age, body metrics) and forgets your paired
- band, but does not delete your previously stored band data on the device; full
- local deletion currently requires uninstalling the App. If you opted in to
- cloud backup, we do not yet have a self-serve deletion flow for that uploaded
- copy — email us at abdulsaheel81@gmail.com to
- request its deletion and we will process it manually.
+ Turn off "Send anonymous diagnostics" at any time in Settings — this stops any
+ further collection immediately. Uninstalling the App deletes all of your
+ locally stored data immediately, since nothing else is retained anywhere except
+ the anonymous Firebase diagnostics described above (governed by Firebase's own
+ practices, linked above, not a system we operate).
Children
This App is not directed to children under 13 (or the relevant age of digital
diff --git a/docs/terms.html b/docs/terms.html
index be56a716..9d548cc4 100644
--- a/docs/terms.html
+++ b/docs/terms.html
@@ -62,10 +62,13 @@
5. Your Responsibilities
6. Third-Party Services
- Certain optional features rely on third-party services you separately
+
The App uses Firebase (Google) for anonymous crash/error and performance
+ monitoring (Crashlytics, Performance Monitoring, Analytics). This is on by
+ default and can be turned off at any time in Settings — see the Privacy
+ Policy for details. It never includes your health data.
+ Certain other, optional features rely on third-party services you separately
configure or consent to, including:
- - An optional cloud backup feature, self-hosted by the App's maintainers.
- An optional AI Coach feature, which sends data you choose to share to a
third-party AI provider (by default, OpenAI) using an API key you
supply.
From 54d51d9587811f46bfd4d02bdf6e8071aab93302 Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sun, 19 Jul 2026 00:40:57 +0530
Subject: [PATCH 08/14] No data collection: gate health-data upload off,
Firebase-only diagnostics
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add kHealthDataContributionEnabled (defaults OFF, same convention as
kSideloadOtaEnabled): compile-time gate for the whole health-data-
contribution feature (HealthUploader's full on-device DB upload + the
"Contribute my health data" toggle in Settings and onboarding). Hidden,
not just disabled, when off. CI no longer opts any distribution channel
into it — we do not collect/receive user health data anywhere.
- Rewrite PRIVACY.md: no bespoke backend collects health data; the only
thing that leaves the device is optional, on-by-default anonymous
diagnostics via Firebase (Crashlytics/Performance/Analytics), governed
by Firebase's own published privacy/security practices (linked directly)
rather than us documenting a backend security posture we don't have.
Drops the now-inapplicable anonymous-vs-pseudonymous/deletion-mechanism
discussion that only applied to the removed upload path.
- Matching rewrite pushed to docs/legal-site (privacy.html + terms.html —
the actual hosted, store-linked copies) in a separate commit there,
since GitHub Pages serves from that branch, not this one.
---
.github/workflows/build.yml | 10 ++++
PRIVACY.md | 70 ++++++++++++----------------
lib/telemetry/health_uploader.dart | 25 ++++++++++
lib/ui/profile/profile_screen.dart | 30 +++++++-----
lib/ui/profile_setup_screen.dart | 41 +++++++++++-----
test/flow_screens_redesign_test.dart | 13 +++++-
6 files changed, 123 insertions(+), 66 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index cf3be87d..ea8158aa 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -36,6 +36,11 @@ jobs:
run: |
echo "COMPANION_URL=${{ secrets.COMPANION_URL }}" > .env
echo "BACKEND_URL=${{ secrets.BACKEND_URL }}" >> .env
+ # Health-data contribution (full on-device DB upload) stays OFF
+ # here too (see kHealthDataContributionEnabled, defaults false) —
+ # we don't collect/receive user health data on ANY distribution
+ # channel, sideload included, matching PRIVACY.md's "we do not
+ # collect your data" — not just the future store build.
# Decode the release keystore from a secret. The gradle config picks these
# env vars up (ANDROID_KEYSTORE_PATH etc.) and signs the release build.
@@ -127,6 +132,11 @@ jobs:
run: |
echo "COMPANION_URL=${{ secrets.COMPANION_URL }}" > .env
echo "BACKEND_URL=${{ secrets.BACKEND_URL }}" >> .env
+ # Health-data contribution (full on-device DB upload) stays OFF
+ # here too (see kHealthDataContributionEnabled, defaults false) —
+ # we don't collect/receive user health data on ANY distribution
+ # channel, sideload included, matching PRIVACY.md's "we do not
+ # collect your data" — not just the future store build.
- name: Install flutterfire (required by Crashlytics Xcode build phase)
run: dart pub global activate flutterfire_cli 1.4.0
diff --git a/PRIVACY.md b/PRIVACY.md
index 15cac4df..5e90963d 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -5,27 +5,31 @@ _Last updated: July 18, 2026_
Edge ("the App") is an independent, open-source project. It is not affiliated
with, sponsored by, or endorsed by WHOOP, Inc.
-**Data we collect**
-- Biometric/health data from your paired WHOOP 4.0 band over Bluetooth: heart
- rate, heart rate variability (RR intervals), motion (accelerometer), raw
- skin-temperature/blood-oxygen sensor channels, and (if you record workouts)
- GPS location during those workouts.
-- Basic app usage/crash telemetry, only if you opt in.
-
-**Where your data lives**
-By default, all of the above is stored locally on your device only. We do not
-upload it anywhere unless you explicitly opt in to one of the following:
-- **Health data contribution** — if you enable "Contribute my health data" in
- Settings, the App periodically uploads a full copy of your on-device
- database (the same biometric/health data listed above) to our backend, to
- help improve the algorithms. This only happens while your phone is on Wi-Fi
- and charging, at most once every 24 hours. We keep only your most recent
- upload — each new one replaces the last — and there is no restore/backup
- feature; it is a one-way contribution, not a backup you can recover from.
- Off by default.
+**We do not collect your data**
+Your biometric/health data from your paired WHOOP 4.0 band — heart rate, heart
+rate variability (RR intervals), motion (accelerometer), raw
+skin-temperature/blood-oxygen sensor channels, and (if you record workouts)
+GPS location during those workouts — is processed and stored entirely on your
+device. We do not upload it, we do not operate a backend that receives it, and
+we never see it.
+
+**Anonymous diagnostics (on by default)**
+The only thing the App sends off your device is basic crash/error and
+performance monitoring, via Firebase (Google) — Crashlytics, Performance
+Monitoring, and Analytics. This is on by default; you can turn it off at any
+time in Settings ("Send anonymous diagnostics"), which stops any further
+collection immediately. It never includes your health data — only crash
+reports, basic device info (OS/model/app version), and coarse performance
+timing. This data is handled under Firebase's own privacy and security
+practices, not a system we built or operate ourselves — see Google's Firebase
+privacy & security documentation: https://firebase.google.com/support/privacy.
+
+**Optional, user-initiated integrations**
+If you choose to enable them, the App can also send data to services *you*
+configure:
- **AI Coach** — if you enable this feature and supply your own API key,
summaries of your data are sent to the AI provider you configure (by
- default, OpenAI) to generate coaching responses. This is off by default and
+ default, OpenAI) to generate coaching responses. Off by default and
requires your own API key.
- **Health app integration** — if you enable it, the App can write derived
daily metrics to Apple Health or Google Health Connect, which are controlled
@@ -34,30 +38,14 @@ upload it anywhere unless you explicitly opt in to one of the following:
**What we don't do**
We do not sell your data. We do not send your health data to WHOOP, Inc. or
any advertising network. We do not require a WHOOP account or credentials to
-use the App.
+use the App. We do not operate a backend that stores your health data.
**Your controls**
-You can disable telemetry, health data contribution, or AI Coach at any time
-in Settings — this stops any further collection or upload immediately.
-
-*Local data:* everything the App stores lives in the App's own private storage
-on your device. Uninstalling the App deletes all of it immediately; nothing
-else needs to happen, since nothing left your device in the first place unless
-you separately opted in to one of the uploads above.
-
-*Previously-sent anonymous diagnostics and health data contributions:* both of
-these are submitted under a randomly-generated install identifier that is
-created on your device, is never linked to your name, email, or any account,
-and is not shown anywhere in the App. Because we have no way to verify that a
-deletion request actually corresponds to a particular person's submissions, we
-are not able to fulfill individual requests to delete previously-sent
-telemetry or a previously-uploaded health data contribution. Turning off the
-"Send anonymous diagnostics" or "Contribute my health data" toggle in Settings
-stops any further collection/upload immediately — it does not retroactively
-remove what was already sent. (For the health data contribution specifically:
-since only your most recent upload is retained and each new one replaces it,
-turning the toggle off also means no further upload will ever replace that
-last one.)
+Turn off "Send anonymous diagnostics" at any time in Settings — this stops any
+further collection immediately. Uninstalling the App deletes all of your
+locally stored data immediately, since nothing else is retained anywhere
+except the anonymous Firebase diagnostics described above (governed by
+Firebase's own practices, linked above, not a system we operate).
**Children**
This App is not directed to children under 13 (or the relevant age of digital
diff --git a/lib/telemetry/health_uploader.dart b/lib/telemetry/health_uploader.dart
index dd28972f..00710a01 100644
--- a/lib/telemetry/health_uploader.dart
+++ b/lib/telemetry/health_uploader.dart
@@ -16,6 +16,24 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../cloud/companion_client.dart';
import '../data/db.dart';
+/// Compile-time gate for the whole health-data-contribution feature (this
+/// upload path + the "Contribute my health data" toggle in Settings and
+/// onboarding). Defaults OFF, same convention as [kSideloadOtaEnabled] in
+/// update_service.dart — the open-source repo, and any App Store/Play Store
+/// submission built from it without extra flags, never offers uploading a
+/// full copy of the local health database anywhere. Uploading someone's
+/// entire raw + derived health history to a self-hosted backend is by far
+/// the biggest privacy/compliance surface this app has; store-review-facing
+/// builds should not carry it at all, only Firebase-based crash/diagnostics
+/// telemetry (gated separately by the user's telemetry consent).
+///
+/// A direct-distribution/sideload build opts in explicitly:
+/// flutter build apk --dart-define=ENABLE_HEALTH_DATA_CONTRIBUTION=true
+const bool kHealthDataContributionEnabled = bool.fromEnvironment(
+ 'ENABLE_HEALTH_DATA_CONTRIBUTION',
+ defaultValue: false,
+);
+
class HealthUploader {
HealthUploader._();
static final HealthUploader instance = HealthUploader._();
@@ -32,7 +50,14 @@ class HealthUploader {
/// Attempt an upload if [consented] AND on Wi-Fi AND charging AND >24h since the
/// last one. Returns true if an upload actually happened.
+ ///
+ /// Defense in depth: even though the Settings/onboarding toggle that sets
+ /// [consented] is itself hidden when [kHealthDataContributionEnabled] is
+ /// false, gate the actual network call on the same flag directly — no
+ /// build without the flag should ever attempt this upload regardless of
+ /// how `consented` ended up true (e.g. a stale local pref).
Future maybeUpload({required bool consented}) async {
+ if (!kHealthDataContributionEnabled) return false;
if (!consented || !CompanionClient.configured || _running) return false;
if (deviceId == null) return false;
_running = true;
diff --git a/lib/ui/profile/profile_screen.dart b/lib/ui/profile/profile_screen.dart
index 6dfe330c..a6343000 100644
--- a/lib/ui/profile/profile_screen.dart
+++ b/lib/ui/profile/profile_screen.dart
@@ -19,6 +19,7 @@ import '../../health/health_export.dart';
import '../../state/app_state.dart';
import '../../state/units_controller.dart';
import '../../debug/debug_mode.dart';
+import '../../telemetry/health_uploader.dart' show kHealthDataContributionEnabled;
import '../../theme/theme_switcher.dart';
import '../ai/ai_settings_screen.dart';
import '../design/design.dart';
@@ -324,17 +325,24 @@ class ProfileScreen extends StatelessWidget {
value: app.telemetryConsent,
onChanged: (v) => app.setTelemetryConsent(v),
),
- const SizedBox(height: Sp.x3),
- _ToggleCard(
- icon: OsIcon.activity,
- title: 'Contribute my health data',
- subtitle:
- 'Periodically upload your on-device database (over Wi-Fi, '
- 'while charging) to improve the algorithms. On by default — '
- 'switch off anytime.',
- value: app.healthShareConsent,
- onChanged: (v) => app.setHealthShareConsent(v),
- ),
+ // Health-data contribution (full on-device DB upload) is a
+ // compile-time-gated feature (see kHealthDataContributionEnabled) —
+ // never offered in a store-review-facing build, only in a
+ // sideload/direct-distribution build that opted in. Hide the
+ // toggle entirely rather than leave it visible-but-inert.
+ if (kHealthDataContributionEnabled) ...[
+ const SizedBox(height: Sp.x3),
+ _ToggleCard(
+ icon: OsIcon.activity,
+ title: 'Contribute my health data',
+ subtitle:
+ 'Periodically upload your on-device database (over Wi-Fi, '
+ 'while charging) to improve the algorithms. On by default — '
+ 'switch off anytime.',
+ value: app.healthShareConsent,
+ onChanged: (v) => app.setHealthShareConsent(v),
+ ),
+ ],
const SizedBox(height: Sp.x6),
diff --git a/lib/ui/profile_setup_screen.dart b/lib/ui/profile_setup_screen.dart
index 1037099e..105a4025 100644
--- a/lib/ui/profile_setup_screen.dart
+++ b/lib/ui/profile_setup_screen.dart
@@ -16,6 +16,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../state/app_state.dart';
+import '../telemetry/health_uploader.dart' show kHealthDataContributionEnabled;
import 'design/design.dart';
class ProfileSetupScreen extends StatefulWidget {
@@ -30,7 +31,10 @@ class _ProfileSetupScreenState extends State {
// either off here). Recorded + sent on Continue. When re-editing an existing
// profile we reflect the saved choice instead.
bool _telemetry = true;
- bool _healthShare = true;
+ // Health-data contribution is a compile-time-gated feature (see
+ // kHealthDataContributionEnabled) — never pre-enabled (or even offered) in
+ // a build that doesn't carry it.
+ bool _healthShare = kHealthDataContributionEnabled;
@override
void initState() {
@@ -43,6 +47,10 @@ class _ProfileSetupScreenState extends State {
_telemetry = app.telemetryConsent;
_healthShare = app.healthShareConsent;
}
+ // Never true in a build without the feature — even a stale saved `true`
+ // (e.g. carried over from a different build's local prefs) must not
+ // resubmit consent for an upload this build will never attempt.
+ if (!kHealthDataContributionEnabled) _healthShare = false;
}
Future _submit(
@@ -84,7 +92,9 @@ class ProfileSetupForm extends StatefulWidget {
super.key,
this.initial = const {},
this.telemetryInitial = true,
- this.healthShareInitial = true,
+ // Gated default: never pre-enabled (or shown) in a build compiled
+ // without kHealthDataContributionEnabled.
+ this.healthShareInitial = kHealthDataContributionEnabled,
required this.onSubmit,
});
@@ -207,18 +217,25 @@ class _ProfileSetupFormState extends State {
value: _telemetry,
onChanged: (v) => setState(() => _telemetry = v),
),
- const SizedBox(height: Sp.x3),
- ConsentTile(
- title: 'Contribute my health data',
- subtitle:
- 'Uploads your on-device database over Wi-Fi while charging, to '
- 'improve the algorithms.',
- value: _healthShare,
- onChanged: (v) => setState(() => _healthShare = v),
- ),
+ // Health-data contribution (full on-device DB upload) is a
+ // compile-time-gated feature (see kHealthDataContributionEnabled) —
+ // never offered in a store-review-facing build.
+ if (kHealthDataContributionEnabled) ...[
+ const SizedBox(height: Sp.x3),
+ ConsentTile(
+ title: 'Contribute my health data',
+ subtitle:
+ 'Uploads your on-device database over Wi-Fi while charging, '
+ 'to improve the algorithms.',
+ value: _healthShare,
+ onChanged: (v) => setState(() => _healthShare = v),
+ ),
+ ],
const SizedBox(height: Sp.x2),
Text(
- 'Switch either off here or anytime in Settings.',
+ kHealthDataContributionEnabled
+ ? 'Switch either off here or anytime in Settings.'
+ : 'Switch it off here or anytime in Settings.',
style: AppText.caption.copyWith(color: AppColors.inkMuted),
),
const SizedBox(height: Sp.x7),
diff --git a/test/flow_screens_redesign_test.dart b/test/flow_screens_redesign_test.dart
index f4ca135f..9c0ebf08 100644
--- a/test/flow_screens_redesign_test.dart
+++ b/test/flow_screens_redesign_test.dart
@@ -14,6 +14,8 @@ import 'package:openstrap_edge/ui/onboarding/welcome_screen.dart'
show WelcomeHero, WelcomeOptionCard;
import 'package:openstrap_edge/ui/pairing_screen.dart'
show PairPhase, PairingInstructionContent, PairingStateView;
+import 'package:openstrap_edge/telemetry/health_uploader.dart'
+ show kHealthDataContributionEnabled;
import 'package:openstrap_edge/ui/profile/profile_screen.dart' show DeviceTile;
import 'package:openstrap_edge/ui/profile_setup_screen.dart'
show ConsentTile, ProfileSetupForm;
@@ -166,7 +168,12 @@ void main() {
expect(find.text('About you'), findsOneWidget);
expect(find.text('Male'), findsOneWidget);
expect(find.text('Female'), findsOneWidget);
- expect(find.byType(ConsentTile), findsNWidgets(2));
+ // The health-data-contribution tile only renders in a build compiled
+ // with kHealthDataContributionEnabled (never in CI/default test runs).
+ expect(
+ find.byType(ConsentTile),
+ findsNWidgets(kHealthDataContributionEnabled ? 2 : 1),
+ );
// No sex chosen yet → Continue disabled (onSubmit not called on tap).
await t.ensureVisible(find.text('Continue'));
@@ -186,7 +193,9 @@ void main() {
expect(submitted!['age'], 30);
expect(submitted!['sex'], 'f');
expect(telemetry, isTrue);
- expect(healthShare, isTrue);
+ // Gated default: only pre-enabled in a build compiled with
+ // kHealthDataContributionEnabled.
+ expect(healthShare, kHealthDataContributionEnabled);
});
}
From 2f156e9c696b8fad6136f3ca4295cbe5e8ff1888 Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sun, 19 Jul 2026 00:49:50 +0530
Subject: [PATCH 09/14] Scope the "no data collection" claim to builds we
actually distribute
An unqualified "we do not collect your data" is an overbroad claim for
open-source code: the flag that gates health-data upload exists in source
and could be enabled by an independent developer's own separately-built
copy. Scope the claim explicitly to every build we ourselves publish
(GitHub Releases APK/IPA, any future store submission) and disclose the
capability + who's responsible for a self-built variant that enables it.
Same scoping applied to the Firebase-only diagnostics claim.
---
docs/privacy.html | 35 ++++++++++++++++++++++++-----------
1 file changed, 24 insertions(+), 11 deletions(-)
diff --git a/docs/privacy.html b/docs/privacy.html
index f07bfa38..abf1d9bb 100644
--- a/docs/privacy.html
+++ b/docs/privacy.html
@@ -27,19 +27,32 @@ Privacy Policy — Edge / OpenStrap
with, sponsored by, or endorsed by WHOOP, Inc.
We do not collect your data
- Your biometric/health data from your paired WHOOP 4.0 band — heart rate, heart
- rate variability (RR intervals), motion (accelerometer), raw
- skin-temperature/blood-oxygen sensor channels, and (if you record workouts) GPS
- location during those workouts — is processed and stored entirely on your
- device. We do not upload it, we do not operate a backend that receives it, and
- we never see it.
+ This section describes every build we ourselves distribute — the signed
+ Android APK and iOS IPA published on GitHub Releases, and any future App
+ Store/Play Store submission. Your biometric/health data from your paired
+ WHOOP 4.0 band — heart rate, heart rate variability (RR intervals), motion
+ (accelerometer), raw skin-temperature/blood-oxygen sensor channels, and (if
+ you record workouts) GPS location during those workouts — is processed and
+ stored entirely on your device in every one of those builds. We do not
+ upload it, we do not operate a backend that receives it, and we never see
+ it.
+
+ Edge is open source. The underlying code contains an off-by-default,
+ compile-time flag (kHealthDataContributionEnabled) that an
+ independent developer could enable in their own, separately-built
+ and separately-distributed copy of the app, pointed at a backend of their
+ own choosing. No build we publish enables it — our CI explicitly forces it
+ off for every release rather than relying on the flag's default. A
+ self-built copy compiled with that flag on is that builder's own software
+ and their own responsibility; it is not covered by this policy.
Anonymous diagnostics (on by default)
- The only thing the App sends off your device is basic crash/error and
- performance monitoring, via Firebase (Google) — Crashlytics, Performance
- Monitoring, and Analytics. This is on by default; you can turn it off at any
- time in Settings ("Send anonymous diagnostics"), which stops any further
- collection immediately. It never includes your health data — only crash
+
In every build we distribute, the only thing the App sends off your device
+ is basic crash/error and performance monitoring, via Firebase (Google) —
+ Crashlytics, Performance Monitoring, and Analytics. This is on by default;
+ you can turn it off at any time in Settings ("Send anonymous diagnostics"),
+ which stops any further collection immediately. It never includes your
+ health data — only crash
reports, basic device info (OS/model/app version), and coarse performance
timing. This data is handled under Firebase's own privacy and security
practices, not a system we built or operate ourselves — see Google's Firebase
From 7c074fe59369a119fa6f63c3ddc6485b9c5a6226 Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sun, 19 Jul 2026 00:50:35 +0530
Subject: [PATCH 10/14] Address CodeRabbit: make the off-by-default health-data
gate explicit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- CI now explicitly passes ENABLE_HEALTH_DATA_CONTRIBUTION=false for both
the Android and iOS release builds, instead of relying on
kHealthDataContributionEnabled's Dart-side default — true by
construction of the CI config, not contingent on the default never
changing later.
- Updated the flag's doc comment: no build we distribute opts in anymore
(the sideload channel no longer does either); it exists solely for an
independent developer's own separately-built copy.
- PRIVACY.md: scoped the "we do not collect your data" and "only Firebase"
claims explicitly to builds we ourselves distribute, and disclosed the
open-source capability + who's responsible if someone else compiles
their own build with it enabled.
- Matching scoping pushed to docs/legal-site's privacy.html in a separate
commit there.
---
.github/workflows/build.yml | 24 +++++++++++++----------
PRIVACY.md | 31 +++++++++++++++++++++---------
lib/telemetry/health_uploader.dart | 22 +++++++++++++--------
3 files changed, 50 insertions(+), 27 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ea8158aa..ea0a240a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -36,11 +36,13 @@ jobs:
run: |
echo "COMPANION_URL=${{ secrets.COMPANION_URL }}" > .env
echo "BACKEND_URL=${{ secrets.BACKEND_URL }}" >> .env
- # Health-data contribution (full on-device DB upload) stays OFF
- # here too (see kHealthDataContributionEnabled, defaults false) —
- # we don't collect/receive user health data on ANY distribution
- # channel, sideload included, matching PRIVACY.md's "we do not
- # collect your data" — not just the future store build.
+ # Health-data contribution (full on-device DB upload) is OFF for
+ # every build we distribute, sideload included — matching
+ # PRIVACY.md's "we do not collect your data". Passed explicitly
+ # (not just relying on kHealthDataContributionEnabled's default)
+ # so this is true by construction of the CI config itself, not
+ # contingent on nobody ever changing that default later.
+ echo "ENABLE_HEALTH_DATA_CONTRIBUTION=false" >> .env
# Decode the release keystore from a secret. The gradle config picks these
# env vars up (ANDROID_KEYSTORE_PATH etc.) and signs the release build.
@@ -132,11 +134,13 @@ jobs:
run: |
echo "COMPANION_URL=${{ secrets.COMPANION_URL }}" > .env
echo "BACKEND_URL=${{ secrets.BACKEND_URL }}" >> .env
- # Health-data contribution (full on-device DB upload) stays OFF
- # here too (see kHealthDataContributionEnabled, defaults false) —
- # we don't collect/receive user health data on ANY distribution
- # channel, sideload included, matching PRIVACY.md's "we do not
- # collect your data" — not just the future store build.
+ # Health-data contribution (full on-device DB upload) is OFF for
+ # every build we distribute, sideload included — matching
+ # PRIVACY.md's "we do not collect your data". Passed explicitly
+ # (not just relying on kHealthDataContributionEnabled's default)
+ # so this is true by construction of the CI config itself, not
+ # contingent on nobody ever changing that default later.
+ echo "ENABLE_HEALTH_DATA_CONTRIBUTION=false" >> .env
- name: Install flutterfire (required by Crashlytics Xcode build phase)
run: dart pub global activate flutterfire_cli 1.4.0
diff --git a/PRIVACY.md b/PRIVACY.md
index 5e90963d..80dcf91a 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -6,17 +6,30 @@ Edge ("the App") is an independent, open-source project. It is not affiliated
with, sponsored by, or endorsed by WHOOP, Inc.
**We do not collect your data**
-Your biometric/health data from your paired WHOOP 4.0 band — heart rate, heart
-rate variability (RR intervals), motion (accelerometer), raw
-skin-temperature/blood-oxygen sensor channels, and (if you record workouts)
-GPS location during those workouts — is processed and stored entirely on your
-device. We do not upload it, we do not operate a backend that receives it, and
-we never see it.
+This section describes every build we ourselves distribute — the signed
+Android APK and iOS IPA published on GitHub Releases, and any future App
+Store/Play Store submission. Your biometric/health data from your paired
+WHOOP 4.0 band — heart rate, heart rate variability (RR intervals), motion
+(accelerometer), raw skin-temperature/blood-oxygen sensor channels, and (if
+you record workouts) GPS location during those workouts — is processed and
+stored entirely on your device in every one of those builds. We do not
+upload it, we do not operate a backend that receives it, and we never see it.
+
+Edge is open source. The underlying code contains an off-by-default,
+compile-time flag (`kHealthDataContributionEnabled`, see
+`lib/telemetry/health_uploader.dart`) that an independent developer could
+enable in their *own*, separately-built and separately-distributed copy of
+the app, pointed at a backend of their own choosing. No build we publish
+enables it — our CI explicitly forces it off for every release (see
+`.github/workflows/build.yml`) rather than relying on the flag's default. A
+self-built copy compiled with that flag on is that builder's own software
+and their own responsibility; it is not covered by this policy.
**Anonymous diagnostics (on by default)**
-The only thing the App sends off your device is basic crash/error and
-performance monitoring, via Firebase (Google) — Crashlytics, Performance
-Monitoring, and Analytics. This is on by default; you can turn it off at any
+In every build we distribute, the only thing the App sends off your device is
+basic crash/error and performance monitoring, via Firebase (Google) —
+Crashlytics, Performance Monitoring, and Analytics. This is on by default; you
+can turn it off at any
time in Settings ("Send anonymous diagnostics"), which stops any further
collection immediately. It never includes your health data — only crash
reports, basic device info (OS/model/app version), and coarse performance
diff --git a/lib/telemetry/health_uploader.dart b/lib/telemetry/health_uploader.dart
index 00710a01..89aca362 100644
--- a/lib/telemetry/health_uploader.dart
+++ b/lib/telemetry/health_uploader.dart
@@ -19,15 +19,21 @@ import '../data/db.dart';
/// Compile-time gate for the whole health-data-contribution feature (this
/// upload path + the "Contribute my health data" toggle in Settings and
/// onboarding). Defaults OFF, same convention as [kSideloadOtaEnabled] in
-/// update_service.dart — the open-source repo, and any App Store/Play Store
-/// submission built from it without extra flags, never offers uploading a
-/// full copy of the local health database anywhere. Uploading someone's
-/// entire raw + derived health history to a self-hosted backend is by far
-/// the biggest privacy/compliance surface this app has; store-review-facing
-/// builds should not carry it at all, only Firebase-based crash/diagnostics
-/// telemetry (gated separately by the user's telemetry consent).
+/// update_service.dart.
///
-/// A direct-distribution/sideload build opts in explicitly:
+/// NOT enabled by ANY build we distribute — not the GitHub Releases sideload
+/// APK/IPA, and not any future App Store/Play Store submission (CI passes
+/// `ENABLE_HEALTH_DATA_CONTRIBUTION=false` explicitly for both; see
+/// .github/workflows/build.yml). Uploading someone's entire raw + derived
+/// health history to a backend is by far the biggest privacy/compliance
+/// surface this app could have, so PRIVACY.md's "we do not collect your
+/// data" claim is scoped to — and must remain true of — every build we
+/// ourselves ship.
+///
+/// This flag exists purely because the code is open source: an independent
+/// developer compiling their OWN build from this repo can opt in and point
+/// it at a backend of their own choosing. That is their build and their
+/// responsibility, not something our privacy policy governs.
/// flutter build apk --dart-define=ENABLE_HEALTH_DATA_CONTRIBUTION=true
const bool kHealthDataContributionEnabled = bool.fromEnvironment(
'ENABLE_HEALTH_DATA_CONTRIBUTION',
From 6f773592a74a73076006d997094ee1f2a5a01fcb Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sun, 19 Jul 2026 07:44:35 +0530
Subject: [PATCH 11/14] Carve out AI Coach / Health app integration from the
absolute claims
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
"We do not collect your data", the Firebase-only diagnostics claim, and
"nothing else is retained" all read as unqualified, but AI Coach and
Health app integration do send data off-device when a user opts into
them. Add explicit carve-outs, and clarify that uninstalling only removes
app-local data — third-party retention/deletion for those two opt-in
integrations follows each service's own policies, not ours.
---
docs/privacy.html | 34 ++++++++++++++++++++++++++--------
1 file changed, 26 insertions(+), 8 deletions(-)
diff --git a/docs/privacy.html b/docs/privacy.html
index abf1d9bb..2196718c 100644
--- a/docs/privacy.html
+++ b/docs/privacy.html
@@ -35,7 +35,9 @@ We do not collect your data
you record workouts) GPS location during those workouts — is processed and
stored entirely on your device in every one of those builds. We do not
upload it, we do not operate a backend that receives it, and we never see
- it.
+ it — unless you separately choose to enable AI Coach or Health app
+ integration, described below, which send specific data to services you
+ configure.
Edge is open source. The underlying code contains an off-by-default,
compile-time flag (kHealthDataContributionEnabled) that an
@@ -48,9 +50,11 @@
We do not collect your data
Anonymous diagnostics (on by default)
In every build we distribute, the only thing the App sends off your device
- is basic crash/error and performance monitoring, via Firebase (Google) —
- Crashlytics, Performance Monitoring, and Analytics. This is on by default;
- you can turn it off at any time in Settings ("Send anonymous diagnostics"),
+ automatically — aside from the optional, user-initiated
+ integrations described next — is basic crash/error and performance
+ monitoring, via Firebase (Google) — Crashlytics, Performance Monitoring, and
+ Analytics. This is on by default; you can turn it off at any time in
+ Settings ("Send anonymous diagnostics"),
which stops any further collection immediately. It never includes your
health data — only crash
reports, basic device info (OS/model/app version), and coarse performance
@@ -79,10 +83,24 @@
What we don't do
Your controls
Turn off "Send anonymous diagnostics" at any time in Settings — this stops any
- further collection immediately. Uninstalling the App deletes all of your
- locally stored data immediately, since nothing else is retained anywhere except
- the anonymous Firebase diagnostics described above (governed by Firebase's own
- practices, linked above, not a system we operate).
+ further collection immediately. You can also disable AI Coach or Health app
+ integration at any time in Settings if you'd previously turned them on.
+
+ Uninstalling the App deletes all of your locally stored data immediately.
+ That's the whole picture unless you had separately enabled one of
+ the optional integrations above — in that case, uninstalling stops the App
+ from sending anything further, but does not reach back and delete data
+ already sent:
+
+ - Anonymous diagnostics already sent to Firebase are retained and
+ governed by Firebase's own practices (linked above), not by us.
+ - Data already sent to your configured AI Coach provider (e.g. OpenAI)
+ is retained and governed by that provider's own policies, not by
+ us.
+ - Metrics already written to Apple Health or Google Health Connect are
+ retained and governed by that platform's own data controls, not by us
+ — manage or delete them from that app directly.
+
Children
This App is not directed to children under 13 (or the relevant age of digital
From 1c13e21a8ea6b67adf31a72587e59ce76b3a667e Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sun, 19 Jul 2026 07:44:57 +0530
Subject: [PATCH 12/14] Carve out AI Coach / Health app integration from the
absolute claims
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
"We do not collect your data", the Firebase-only diagnostics claim, and
"nothing else is retained" all read as unqualified, but AI Coach and
Health app integration do send data off-device when a user opts into
them. Add explicit carve-outs, and clarify that uninstalling only removes
app-local data — third-party retention/deletion for those two opt-in
integrations follows each service's own policies, not ours.
Matching fix pushed to docs/legal-site's privacy.html in a separate
commit there.
---
PRIVACY.md | 33 ++++++++++++++++++++++++---------
1 file changed, 24 insertions(+), 9 deletions(-)
diff --git a/PRIVACY.md b/PRIVACY.md
index 80dcf91a..f1430749 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -13,7 +13,10 @@ WHOOP 4.0 band — heart rate, heart rate variability (RR intervals), motion
(accelerometer), raw skin-temperature/blood-oxygen sensor channels, and (if
you record workouts) GPS location during those workouts — is processed and
stored entirely on your device in every one of those builds. We do not
-upload it, we do not operate a backend that receives it, and we never see it.
+upload it, we do not operate a backend that receives it, and we never see it
+— *unless you separately choose to enable AI Coach or Health app
+integration, described below, which send specific data to services you
+configure.*
Edge is open source. The underlying code contains an off-by-default,
compile-time flag (`kHealthDataContributionEnabled`, see
@@ -26,10 +29,11 @@ self-built copy compiled with that flag on is that builder's own software
and their own responsibility; it is not covered by this policy.
**Anonymous diagnostics (on by default)**
-In every build we distribute, the only thing the App sends off your device is
-basic crash/error and performance monitoring, via Firebase (Google) —
-Crashlytics, Performance Monitoring, and Analytics. This is on by default; you
-can turn it off at any
+In every build we distribute, the only thing the App sends off your device
+*automatically* — aside from the optional, user-initiated integrations
+described next — is basic crash/error and performance monitoring, via
+Firebase (Google) — Crashlytics, Performance Monitoring, and Analytics. This
+is on by default; you can turn it off at any
time in Settings ("Send anonymous diagnostics"), which stops any further
collection immediately. It never includes your health data — only crash
reports, basic device info (OS/model/app version), and coarse performance
@@ -55,10 +59,21 @@ use the App. We do not operate a backend that stores your health data.
**Your controls**
Turn off "Send anonymous diagnostics" at any time in Settings — this stops any
-further collection immediately. Uninstalling the App deletes all of your
-locally stored data immediately, since nothing else is retained anywhere
-except the anonymous Firebase diagnostics described above (governed by
-Firebase's own practices, linked above, not a system we operate).
+further collection immediately. You can also disable AI Coach or Health app
+integration at any time in Settings if you'd previously turned them on.
+
+Uninstalling the App deletes all of your locally stored data immediately.
+That's the whole picture *unless* you had separately enabled one of the
+optional integrations above — in that case, uninstalling stops the App from
+sending anything further, but does not reach back and delete data already
+sent:
+- Anonymous diagnostics already sent to Firebase are retained and governed by
+ Firebase's own practices (linked above), not by us.
+- Data already sent to your configured AI Coach provider (e.g. OpenAI) is
+ retained and governed by that provider's own policies, not by us.
+- Metrics already written to Apple Health or Google Health Connect are
+ retained and governed by that platform's own data controls, not by us —
+ manage or delete them from that app directly.
**Children**
This App is not directed to children under 13 (or the relevant age of digital
From 1ec89b9f39c6bb40a5cbfcc32cfcaa875931143a Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sun, 19 Jul 2026 08:33:32 +0530
Subject: [PATCH 13/14] Bump kAlgoVersion 43 -> 44 for the partial-day +
wake-steps fixes
Out of caution: neither fix changes a scalar a previously-finalized
day_result already had right, but bumping forces non-finalized days to
re-derive with both fixes (partial-day pruning guard, corrected
early-read wake steps/calories) in effect going forward.
---
lib/compute/derivation_engine.dart | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart
index c5b1dd70..c8549ac4 100644
--- a/lib/compute/derivation_engine.dart
+++ b/lib/compute/derivation_engine.dart
@@ -215,7 +215,23 @@ import 'substrate.dart';
// whole-bpm RHR / integer skin-temp ADC), which was intermittently blanking the
// whole readiness score to "—" on nights that had valid sleep. Bump so days that
// were previously absent-for-that-reason recompute a real score.
-const int kAlgoVersion = 43;
+// v44: two consistency fixes; neither changes a scalar that a previously
+// FINALIZED day_result already had right, but both affect data availability/
+// consistency going forward. (1) A day whose offloaded second-half compute
+// (naps/workouts/HRR/wear/curves/wake-features) failed or timed out — but
+// whose headline scalars (readiness/RHR/RMSSD) already succeeded — could get
+// marked finalized and treated as fully "derived" by the raw-pruning guard,
+// permanently losing the raw substrate needed to ever fill in those missing
+// fields on retry. Now tracked via a new `partial` day_result column and
+// excluded from both age-based finalization and the pruning guard until the
+// second half actually completes. (2) The wake_day_features early-read
+// artifact (what the Today repo shows before the full day result is ready)
+// was copying the pre-hybrid-correction 1Hz-only step/calorie estimate
+// instead of the corrected real-100Hz+1Hz hybrid value computed moments
+// later in the same pass — the final day_result was always correct, only
+// this transient early read was stale. Bump so any day currently sitting
+// non-finalized re-derives with both fixes in effect.
+const int kAlgoVersion = 44;
/// Raw is kept this many days past derivation, then pruned (derived stays).
const int rawRetentionDays = 3;
From c0df0616fc2ecd7839dc3cf5fd573a95290262fd Mon Sep 17 00:00:00 2001
From: abdulsaheel
Date: Sun, 19 Jul 2026 08:37:56 +0530
Subject: [PATCH 14/14] iOS App Store prep: usage-description strings + real
launch screen
- Info.plist: add NSLocationAlwaysAndWhenInUseUsageDescription and
NSPhotoLibraryUsageDescription. Neither permission is requested by app
code (gps_source.dart is WhenInUse-only; import_screen.dart's FilePicker
only ever uses FileType.any, never .image/.media) but geolocator_apple
and file_picker's iOS podspecs unconditionally link the corresponding
APIs, which trips App Store Connect's static binary scan (ITMS-90683)
at submission without these strings present.
- Launch screen: replace the literal 1x1 placeholder LaunchImage PNGs
(1x/2x/3x) with a real 96x96 logo, and the hardcoded white background
with a new LaunchBackground color asset (light/dark-mode aware).
- project.pbxproj: cosmetic alphabetical reorder of two build-file
entries only; the Crashlytics symbol-upload script's `|| true` (kept
non-fatal so a Crashlytics upload hiccup never fails the build for a
contributor without matching Firebase/signing setup) is unchanged.
---
ios/Runner.xcodeproj/project.pbxproj | 4 +-
.../LaunchBackground.colorset/Contents.json | 38 ++++++++++++++++++
.../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 12359 bytes
.../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 43205 bytes
.../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 92976 bytes
ios/Runner/Base.lproj/LaunchScreen.storyboard | 4 +-
ios/Runner/Info.plist | 15 +++++++
7 files changed, 57 insertions(+), 4 deletions(-)
create mode 100644 ios/Runner/Assets.xcassets/LaunchBackground.colorset/Contents.json
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index a0bdb4b2..bee5ce53 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -18,7 +18,6 @@
5348973F2FDC19C80033A4D9 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5348973E2FDC19C80033A4D9 /* SwiftUI.framework */; };
5348974E2FDC19C90033A4D9 /* OpenStrapWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5348973B2FDC19C80033A4D9 /* OpenStrapWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
534897A72FDC2B310033A4D9 /* LiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 534897A62FDC2B310033A4D9 /* LiveActivityBridge.swift */; };
- BEEF00000000000000000002 /* BreathingLiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEEF00000000000000000001 /* BreathingLiveActivityBridge.swift */; };
53962E962FF6EE120061A61B /* WatchBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53962E952FF6EE120061A61B /* WatchBridge.swift */; };
53962E972FF6EE120061A61B /* OpenStrapIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53962E942FF6EE120061A61B /* OpenStrapIntents.swift */; };
630172CC9317145AD5F8F3B7 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C721EA0C8D31A3834E66203 /* Pods_RunnerTests.framework */; };
@@ -28,6 +27,7 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
ACCE55E7000000000000B11D /* AccessorySetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACCE55E7000000000000F11E /* AccessorySetup.swift */; };
+ BEEF00000000000000000002 /* BreathingLiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEEF00000000000000000001 /* BreathingLiveActivityBridge.swift */; };
FADE0001FADE0001FADE0001 /* OpenStrapWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 53962EBE2FF6EF790061A61B /* OpenStrapWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
@@ -105,7 +105,6 @@
534897562FDC1A450033A4D9 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
534897572FDC1A610033A4D9 /* OpenStrapWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenStrapWidgetExtension.entitlements; sourceTree = ""; };
534897A62FDC2B310033A4D9 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = ""; };
- BEEF00000000000000000001 /* BreathingLiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreathingLiveActivityBridge.swift; sourceTree = ""; };
53962E942FF6EE120061A61B /* OpenStrapIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenStrapIntents.swift; sourceTree = ""; };
53962E952FF6EE120061A61B /* WatchBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchBridge.swift; sourceTree = ""; };
53962EBE2FF6EF790061A61B /* OpenStrapWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "OpenStrapWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -128,6 +127,7 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
ACCE55E7000000000000F11E /* AccessorySetup.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccessorySetup.swift; sourceTree = ""; };
B0B95B9F07C8083A2DB92078 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ BEEF00000000000000000001 /* BreathingLiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreathingLiveActivityBridge.swift; sourceTree = ""; };
DE1829A4D85DB1C3222C2CB5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
EC2DC396945E3B2DAA52EB3F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
FE8EE10565E740E87BE6E423 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.colorset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.colorset/Contents.json
new file mode 100644
index 00000000..19292716
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchBackground.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xEC",
+ "green" : "0xF1",
+ "red" : "0xF4"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x0D",
+ "green" : "0x11",
+ "red" : "0x14"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..bf1b93a1c0b52301c2644efc0e2931d3073a7439 100644
GIT binary patch
literal 12359
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
Q1PU{Fy85}Sb4q9e0B4a5jsO4v
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..664f151ccb79e83b18390acd0ee3b38ba5e6d600 100644
GIT binary patch
literal 43205
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 +(}x3nkQhxvs;(SuYQ4H
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
-pzdF|
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