58 Commits

Author SHA1 Message Date
98cbcd4d52 Merge pull request 'docs: revamp AGENTS.md — canonical AI agent reference' (#7) from docs/revamp-agents-md into main 2026-07-04 16:22:54 +02:00
741376229d docs: revamp AGENTS.md — canonical AI agent reference, purge obsolete TabataFit docs
All checks were successful
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Successful in 6m52s
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Successful in 1m3s
2026-07-03 12:16:53 +00:00
de63ae0546 Merge pull request 'ci: switch from Firebase OTA to Xcode WiFi/USB direct deploy' (#6) from ci/xcode-network-deploy into main 2026-06-28 11:29:26 +02:00
7d9a89e637 fix(ci): correct derived data path in install step
All checks were successful
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Successful in 5m31s
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Successful in 1m3s
2026-06-28 09:22:43 +00:00
97b61d3afb fix(ci): switch from Firebase OTA to Xcode WiFi/USB direct deploy
Some checks failed
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Failing after 2m59s
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-28 09:14:51 +00:00
a9eda61967 Merge pull request 'ci: add iPhone deploy workflow + CI/CD setup docs' (#4) from ci/iphone-deploy-workflow into main 2026-06-27 22:49:52 +02:00
a382ad62dc fix: move #if outside array literal in TabataGoComplication.supportedFamilies
All checks were successful
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Successful in 4m59s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Successful in 4m5s
2026-06-27 20:40:39 +00:00
069b7d417f fix(ci): keep SPM cache — remove aggressive clean that forces RevenueCat re-clone
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 2m13s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 20:35:47 +00:00
63fbae3698 fix(ci): restore automatic code signing — Apple account now configured on runner
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 11m7s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 20:28:01 +00:00
4b7548501a fix(ci): disable code signing — Mac runner has no Apple Developer account
Some checks failed
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been cancelled
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Has been cancelled
STOPGAP: CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO to bypass
signing on the Mac CI runner which has no Apple account signed into Xcode.

REAL FIX: Sign into Xcode → Settings → Accounts with the Apple Developer
account on the Mac runner machine. Then -allowProvisioningUpdates +
DEVELOPMENT_TEAM will auto-provision profiles for any bundle IDs.
2026-06-27 20:23:56 +00:00
5079de0fb1 fix(ci): checkout PR branch instead of main
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 6m12s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 20:13:28 +00:00
52346fb4ac fix: align bundleIdPrefix with Firebase app (com.tabatago → fr.millianlmx.tabatago)
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 10m58s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 20:00:35 +00:00
abf926e4bc fix(ci): remove -derivedDataPath from resolve step (requires -scheme)
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 8m40s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 08:42:41 +00:00
7c50282284 fix(ci): clean SPM/Xcode caches agressivement pour éviter les segfaults linker après changement de bundle ID
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 2m4s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
- Supprime TOUT DerivedData (pas seulement TabataGo-*) car le changement
  de bundle ID (com.tabatago.app → fr.millianlmx.tabatago) a rendu les
  anciens caches SPM incohérents
- Ajoute la suppression de ~/Library/org.swift.swiftpm, ~/.swiftpm,
  ~/Library/Caches/com.apple.dt.Xcode et du ModuleCache
- Supprime les build/ locaux et .build avant xcodegen generate
- Supprime les Package.resolved stale
- Utilise -derivedDataPath et -clonedSourcePackagesDirPath cohérents
  entre la résolution SPM et le build
2026-06-27 08:38:27 +00:00
6cc25300d1 fix: change bundle ID from com.tabatago.app to fr.millianlmx.tabatago
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 4m2s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 08:29:33 +00:00
09e77b477c fix(ci): switch to --token auth and add --project flag for Firebase deploy
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 9m15s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 08:00:57 +00:00
5c73d6bf29 fix(ci): use npx firebase to avoid PATH issues with npm global install
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 8m42s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 02:33:33 +00:00
d614e72031 fix(ci): add mkdir -p build to Package IPA step
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 7m31s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 02:14:54 +00:00
3ca3e54837 fix(ci): add -derivedDataPath and target find to build/derived — compatible with -scheme
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 7m41s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 02:02:12 +00:00
4e2e807fd9 fix(ci): remove grep -v DerivedData from Package IPA step — app builds to DerivedData
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 6m28s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 01:54:33 +00:00
b48178f167 fix(ci): switch from -target to -scheme for Xcode 26 SPM module path resolution
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 6m32s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 01:45:52 +00:00
c45e85edb6 fix(ci): remove stale .xcodeproj from git, nuke before xcodegen to prevent PostHog ghost dependency
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 7m8s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 01:35:07 +00:00
00e02b7166 fix(ci): clean SPM caches and DerivedData on CI runner to remove stale PostHog resolution
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 4m30s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 01:25:11 +00:00
2aa312ab61 fix(ci): use xcodebuild -version instead of xcode-select, clean only runner caches
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 3m30s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 01:10:44 +00:00
595140e2f9 fix(ci): add sudo back to xcode-select — must be run as root
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 3h8m4s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 00:48:57 +00:00
afc161c8ce fix: remove PostHog from project.yml — EmitSwiftModule crashes on Xcode 26
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 2s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 00:48:00 +00:00
58177102a4 fix: permanently fix PostHog module emission error
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 0s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
- Change PostHog from exactVersion to revision (tag 3.62.0) in project.yml
- Add deep cache clean step before xcodegen in CI workflow
- Add ONLY_ACTIVE_ARCH=NO to xcodebuild command
- Wrap PostHog usage in #if canImport(PostHog) for graceful degradation
- Add Package.resolved, .build/, DerivedData/ to .gitignore
- Remove stale SPM caches between CI runs (DerivedData, swiftpm caches, act cache)
2026-06-27 00:39:52 +00:00
9739a739f5 fix(ci): remove sudo from Setup Xcode step to prevent password prompt hang
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 2s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 00:37:08 +00:00
7fb44198e7 fix: kill stale Package.resolved, pin PostHog exact, add module emission flags
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 8m39s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-27 00:28:40 +00:00
8ce38332b8 fix(ci): remove -derivedDataPath and -clonedSourcePackagesDirPath incompatible with -target
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 6m36s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
Xcode demands -scheme when using -derivedDataPath or -clonedSourcePackagesDirPath.
Since we use -target TabataGo, these flags must be removed.

Changes:
- Removed -derivedDataPath and -clonedSourcePackagesDirPath from build command
- Moved SWIFT_ENABLE_EXPLICIT_MODULES=NO to its own line for clarity
- Updated Package IPA step to search from repo root (find ..) with explicit
  TabataGo.app name instead of relying on the old derived data path
2026-06-27 00:18:13 +00:00
3202ea9b5b fix(ci): revert build command from -scheme TabataGo to -target TabataGo
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 3m37s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
xcodegen 2.45.4 does not generate a "TabataGo" scheme (only
RevenueCatUI, RevenueCatUITests, TabataGoUITests). The -scheme flag
therefore fails. Reverting to -target TabataGo as was previously used.

Verified: all .accessoryCorner usages in TabataGoComplication.swift are
properly guarded with #if canImport(WatchKit), so the watch target
transitive dependency build will not surface accessoryCorner issues.
2026-06-26 23:50:46 +00:00
f14186aeab fix(ci): isolate SPM and DerivedData to prevent cache corruption
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 3m55s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
- Remove destructive clean step that causes corruption when dirs are in use
- Add -derivedDataPath to isolate CI build from user Xcode cache
- Add -clonedSourcePackagesDirPath to isolate SPM clones
- Fix Package IPA step to use new isolated derived data path
2026-06-26 23:44:19 +00:00
60ac624487 fix: use canImport(WatchKit) guard for accessoryCorner, use -scheme in CI build
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 2m8s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
- Replace #if os(watchOS) with #if canImport(WatchKit) for all .accessoryCorner
  usages. When the CI builds with forced iphoneos SDK, os(watchOS) evaluates
  to true (target platform is watchOS) but the SDK doesn't have WatchKit, so
  accessoryCorner is unavailable. canImport(WatchKit) correctly checks SDK
  availability rather than target platform.

- Wrap CornerComplicationView struct and its preview in #if canImport(WatchKit)
  with @available(watchOS 10.0, *) annotation.

- Change CI build from -target TabataGo -sdk iphoneos to -scheme TabataGo
  (without -sdk). This lets xcodebuild pick the correct SDK per target:
  iphoneos for iOS, watchos for watchOS. The scheme already exists in
  project.yml.
2026-06-26 23:39:38 +00:00
a6ea4ca904 fix: wrap accessoryCorner in #if os(watchOS) for Xcode 26 compat
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 6m41s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 23:35:12 +00:00
dde01ce957 fix: clean SPM cache, disable explicit modules
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 5m3s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 23:22:50 +00:00
5b67aea7ce fix: update PostHog to 3.62.0 to fix SwiftDriver compilation error
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 9m28s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
- Pin PostHog to 3.62.0 (vendored PLCrashReporter, drops external
  plcrashreporter dependency that fails with Xcode 26/Swift 6)
- Update project.yml and pbxproj minimumVersion
- Delete Package.resolved to force clean SPM resolution
- Add -resolvePackageDependencies step in CI workflow
- Add -skipPackagePluginValidation to build command
- Fix docs/ci-cd-setup.md manual build command
2026-06-26 23:07:57 +00:00
ebfaa15e38 fix: add DEVELOPMENT_TEAM via xcodebuild CLI
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 4m10s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:59:06 +00:00
86fb683428 fix: add DEVELOPMENT_TEAM to project.yml
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 3m30s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:48:01 +00:00
a48ee0007a fix: use xcodebuild build + manual ipa packaging (no scheme needed)
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 3m40s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:43:37 +00:00
cfe8e8cd1b fix: add TabataGo scheme to project.yml (xcodegen 2.45.4 compatibility)
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 50s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:41:13 +00:00
fdf4b6d558 fix: add xcodebuild -list debug, use -project flag
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 45s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:39:29 +00:00
44cebf834c fix: add /opt/homebrew/bin to PATH for all steps
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 4m2s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:34:29 +00:00
da62d6fa58 fix: use full path for npm
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 5s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:33:21 +00:00
96654d61c7 fix: tolerate brew post-install warning exit code
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 10s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:32:36 +00:00
1b3ce24fc9 fix: complete rewrite — add git clone checkout, install node+npm, fix all paths
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 22s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:31:09 +00:00
f5a777aee8 fix: use full brew path with arch -arm64 for Rosetta runner
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 6s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:29:12 +00:00
836cf9a16d chore: remove temp files
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 3s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:27:40 +00:00
7464357787 fix: remove arch -arm64 from xcodegen (runner already handles it)
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 7s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:27:13 +00:00
8f3979c8a0 fix: remove actions/checkout@v4 (no Node.js on Mac runner)
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 8s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:26:25 +00:00
1d4d43f3f2 fix: replace brew with npm for firebase-tools, use arch -arm64 for xcodegen
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 2s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 22:24:10 +00:00
e43f197139 fix: rename GITEA_TOKEN to PR_API_TOKEN (GITEA_ prefix reserved)
Some checks failed
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Failing after 14s
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-06-26 21:59:29 +00:00
46bf8cae9b docs: update ci-cd-setup.md for Firebase migration
Some checks failed
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been cancelled
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Has been cancelled
- Remove all ios-deploy references (brew install, USB setup, device discovery)
- Remove iPhone Preparation section (UDID, libimobiledevice)
- Add Firebase App Distribution setup section:
  - Firebase project creation via console.firebase.google.com
  - Enable App Distribution, get App ID
  - Service account with Firebase App Distribution Admin role
  - Base64 encoding instructions for service account key
  - Gitea secrets: FIREBASE_APP_ID, FIREBASE_SERVICE_ACCOUNT
  - Add testers group 'millian-test' and invite flow
- Add Compilation Error Troubleshooting section:
  - xcodegen clearing signing team
  - TabataGoWatchWidget needing explicit team ID in project.yml
  - General build debugging tips
- Update workflow overview to reflect Firebase flow
- Update secrets table and manual test run command
2026-06-26 21:48:36 +00:00
9b06c0ae5c feat: switch to Firebase OTA deployment, drop ios-deploy
Some checks failed
PR → Build → Firebase OTA → LGTM / Wait for LGTM comment (pull_request) Has been cancelled
PR → Build → Firebase OTA → LGTM / Build & Deploy to Firebase OTA (pull_request) Has been cancelled
2026-06-26 21:48:07 +00:00
6a659c41fa docs: add ios-deploy, system_profiler UDID, Bonjour/VPN notes to CI/CD setup
Some checks failed
PR → iPhone → LGTM / Wait for LGTM comment (pull_request) Has been cancelled
PR → iPhone → LGTM / Build & Install on iPhone (pull_request) Has been cancelled
- Add ios-deploy install step (brew install ios-deploy)
- Add system_profiler SPUSBDataType method for finding iPhone UDID
- Add note about Bonjour/mDNS not working over VPN — use UDID-based connection
- Add ios-deploy troubleshooting entry for VPN/Bonjour issues
2026-06-26 21:25:51 +00:00
7a02a8949e fix: gitea→github context, add ios-deploy, remove -k
Some checks failed
PR → iPhone → LGTM / Build & Install on iPhone (pull_request) Has been cancelled
PR → iPhone → LGTM / Wait for LGTM comment (pull_request) Has been cancelled
2026-06-26 21:24:47 +00:00
001062c6d3 ci: add iPhone deploy workflow + CI/CD setup docs
Some checks failed
PR → iPhone → LGTM / Wait for LGTM comment (pull_request) Has been cancelled
PR → iPhone → LGTM / Build & Install on iPhone (pull_request) Has been cancelled
- .gitea/workflows/pr-iphone-deploy.yml: PR → Build → iPhone → LGTM → Merge
- docs/ci-cd-setup.md: Mac runner + secrets setup guide
2026-06-26 20:50:53 +00:00
bd81ce92ad docs: add AGENTS.md for Hermes agent context
All checks were successful
CI / Detect Changes (push) Successful in 3s
CI / Admin Web CI (push) Has been skipped
CI / YouTube Worker (push) Has been skipped
CI / Deploy (push) Has been skipped
2026-06-26 18:30:17 +00:00
77c17046d5 Merge pull request 'feat: move HealthKit permission to onboarding, remove HR write' (#3) from fix/healthkit-setup-popup into main
All checks were successful
CI / Detect Changes (push) Successful in 3s
CI / Admin Web CI (push) Has been skipped
CI / YouTube Worker (push) Has been skipped
CI / Deploy (push) Has been skipped
Reviewed-on: #3
2026-05-24 15:29:12 +02:00
27 changed files with 835 additions and 3592 deletions

View File

@@ -0,0 +1,192 @@
# =============================================================================
# TabataGo — PR → Build → WiFi/USB Direct Deploy → Wait LGTM → Merge
# =============================================================================
name: PR → Build → WiFi/USB Deploy → LGTM
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
permissions:
contents: write
pull-requests: write
jobs:
build-deploy:
name: Build & Deploy to iPhone (WiFi/USB)
runs-on: macos
timeout-minutes: 20
steps:
- name: Checkout
run: |
git clone --depth 1 -b ${{ github.head_ref }} https://x-access-token:${PR_TOKEN}@gitea.1000co.fr/${{ github.repository }}.git .
env:
PR_TOKEN: ${{ secrets.PR_API_TOKEN }}
REPO: ${{ github.repository }}
- name: Setup Xcode
run: |
xcodebuild -version
- name: Setup PATH
run: echo "/opt/homebrew/bin" >> $GITHUB_PATH
- name: Clean build artifacts
run: |
echo "🧹 Cleaning build artifacts..."
# Act runner caches
rm -rf ~/.cache/act/*/hostexecutor/tabatago-swift/build 2>/dev/null || true
# SPM caches are KEPT between runs — RevenueCat is 1.10 GiB and takes
# 5+ minutes to clone; re-cloning on every CI run would waste time
# and cause random failures on the act_runner sandbox.
# Xcode DerivedData — suppression COMPLÈTE (le bundle ID a changé,
# l'ancien dossier TabataGo-* peut être ignoré par le nouveau build)
rm -rf ~/Library/Developer/Xcode/DerivedData/*
# Module cache Xcode — des .pcm stale peuvent segfault le linker
rm -rf ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex 2>/dev/null || true
rm -rf ~/Library/Caches/com.apple.dt.Xcode
# Build artifacts locaux (utilisés par le step "Build app")
rm -rf build/
rm -rf .build
rm -rf tabatago-swift/.build
rm -rf tabatago-swift/TabataGo.xcodeproj
rm -rf tabatago-swift/*.xcworkspace 2>/dev/null || true
# Package.resolved — peut être stale après un changement de bundle ID
find . -name "Package.resolved" -delete 2>/dev/null || true
find . -name ".package.resolved" -delete 2>/dev/null || true
echo "✅ Caches nettoyés"
- name: Install tools (xcodegen, node, ios-deploy)
run: |
brew install xcodegen node ios-deploy || true
- name: Generate Xcode project
run: |
cd tabatago-swift
xcodegen generate
- name: Resolve SPM packages
run: |
cd tabatago-swift
xcodebuild -resolvePackageDependencies \
-project TabataGo.xcodeproj \
-scmProvider system
- name: Build app
run: |
cd tabatago-swift
xcodebuild build \
-project TabataGo.xcodeproj \
-scheme TabataGo \
-configuration Debug \
-allowProvisioningUpdates \
-skipPackagePluginValidation \
-derivedDataPath ../build/derived \
-clonedSourcePackagesDirPath ../build/spm-cache \
SWIFT_ENABLE_EXPLICIT_MODULES=NO \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM=2MJF39L8VY
- name: Install to iPhone (WiFi → USB fallback)
run: |
UDID="00008120-000925CE3672201E"
APP=$(find build/derived -name 'TabataGo.app' -type d | head -1)
# 1. Try WiFi first (Xcode 15+ devicectl network discovery)
echo "📱 Trying WiFi install via devicectl..."
xcrun devicectl device install app --device "$UDID" "$APP" 2>&1 | tail -3
if [ ${PIPESTATUS[0]} -eq 0 ]; then
echo "✅ Installed via WiFi"
exit 0
fi
# 2. Fallback to USB via ios-deploy
echo "🔌 WiFi failed, trying USB..."
ios-deploy --bundle "$APP" --id "$UDID" --justlaunch
if [ $? -eq 0 ]; then
echo "✅ Installed via USB"
exit 0
fi
echo "❌ Install failed (both WiFi and USB)"
exit 1
- name: Post "Ready to test" comment
env:
GT_TOKEN: ${{ secrets.PR_API_TOKEN }}
GITEA_URL: https://gitea.1000co.fr
run: |
PR="${{ github.event.pull_request.number }}"
REPO="${{ github.repository }}"
curl -s -X POST \
-H "Authorization: token ${GT_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"body\":\"## 📱 Prêt à tester !\\n\\nL'app est déployée sur l'iPhone (USB/WiFi).\\n\\n- Teste les changements\\n- Reply **LGTM** pour merger\\n- Reply **KO** pour bloquer\"}" \
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${PR}/comments"
wait-approval:
name: Wait for LGTM comment
needs: build-deploy
runs-on: macos
timeout-minutes: 120
steps:
- name: Poll for LGTM
env:
GT_TOKEN: ${{ secrets.PR_API_TOKEN }}
GITEA_URL: https://gitea.1000co.fr
run: |
PR="${{ github.event.pull_request.number }}"
REPO="${{ github.repository }}"
PR_TITLE="${{ github.event.pull_request.title }}"
API="${GITEA_URL}/api/v1/repos/${REPO}"
MAX=240
TRIES=0
echo "⏳ Attente LGTM sur PR #${PR}..."
LAST_ID=$(curl -s -H "Authorization: token ${GT_TOKEN}" \
"${API}/issues/${PR}/comments" | \
python3 -c "import json,sys; cs=json.load(sys.stdin); print(cs[-1]['id'] if cs else 0)" 2>/dev/null || echo 0)
while [ $TRIES -lt $MAX ]; do
sleep 30
TRIES=$((TRIES + 1))
COMMENTS=$(curl -s -H "Authorization: token ${GT_TOKEN}" \
"${API}/issues/${PR}/comments?since_id=${LAST_ID}")
if echo "$COMMENTS" | grep -qi '"body": *"LGTM"'; then
echo "✅ LGTM reçu ! Merge..."
curl -s -X POST \
-H "Authorization: token ${GT_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"Do\":\"merge\"}" \
"${API}/pulls/${PR}/merge"
echo "✅ Mergée."
exit 0
fi
if echo "$COMMENTS" | grep -qi '"body": *"KO"'; then
echo "❌ KO reçu. Bloquée."
exit 1
fi
if [ $((TRIES % 4)) -eq 0 ]; then
echo " ⏳ ... (${TRIES}/240, $(date +%H:%M))"
fi
done
echo "❌ Timeout 2h."
exit 1

8
.gitignore vendored
View File

@@ -58,3 +58,11 @@ Config/Secrets.xcconfig
_Users_*
swift-generated-sources/
tabatago-swift/build/
# Swift Package Manager
Package.resolved
.build/
DerivedData/
# XcodeGen generated — NOT in version control
TabataGo.xcodeproj

673
AGENTS.md
View File

@@ -1,491 +1,272 @@
# AGENTS.md
# TabataGo — Référence canonique pour agents IA
This file contains optimizations and best practices for working with this TabataFit Expo project.
> **Une seule source de vérité.** Ce document décrit le projet *réel*. Si un autre
> fichier (CLAUDE.md, schema.sql) contredit, ce document gagne.
---
## 🚀 Command Optimizations
## 0. ⚠️ Fichiers obsolètes — NE PAS LIRE
Use the following `rtk` commands for optimal performance:
| Fichier | Statut | Pourquoi |
|---------|--------|----------|
| `CLAUDE.md` (racine) | ❌ OBSOLÈTE | Décrit **TabataFit**, un concept Expo/React Native abandonné |
| `supabase/schema.sql` | ❌ OBSOLÈTE | Header littéral "TabataFit" — décrit l'ancien schéma |
| `supabase/seed.sql`, `supabase/setup-admin.sql` | ⚠️ Vérifier | Peuvent être périmés ; préférer `supabase/migrations/` |
| `AGENTS.md` (ancien, racine) | 🔄 Remplacé par celui-ci | Mentionne PostHog 3.x comme actif (faux, voir §3) |
| Command | rtk Equivalent | Speedup | Tokens Before | Tokens After | Savings |
|---------|----------------|---------|---------------|--------------|---------|
| `ls` / `tree` | `rtk ls` | 10x | 2,000 | 400 | -80% |
| `cat` / `read` | `rtk read` | 20x | 40,000 | 12,000 | -70% |
| `grep` / `rg` | `rtk grep` | 8x | 16,000 | 3,200 | -80% |
| `git status` | `rtk git status` | 10x | 3,000 | 600 | -80% |
| `git diff` | `rtk git diff` | 5x | 10,000 | 2,500 | -75% |
| `git log` | `rtk git log` | 5x | 2,500 | 500 | -80% |
| `git add/commit/push` | `rtk git` | 8x | 1,600 | 120 | -92% |
| `cargo test` / `npm test` | `rtk test` | 5x | 25,000 | 2,500 | -90% |
| `ruff check` | `rtk lint` | 3x | 3,000 | 600 | -80% |
| `pytest` | `rtk pytest` | 4x | 8,000 | 800 | -90% |
| `go test` | `rtk gotest` | 3x | 6,000 | 600 | -90% |
| `docker ps` | `rtk docker` | - | - | - | - |
### Usage Examples
```bash
# Instead of: ls -la src/
rtk ls src/
# Instead of: cat package.json
rtk read package.json
# Instead of: grep -r "useEffect" src/
rtk grep "useEffect" src/
# Instead of: git status
rtk git status
# Instead of: npm test
rtk test
# Instead of: ruff check .
rtk lint
```
**Source de vérité du schéma : `supabase/migrations/001` → `006`.** Jamais `schema.sql`.
---
## 📱 Expo Best Practices
## 1. Identité produit
### Development Workflow
**CRITICAL: Always try Expo Go first before creating custom builds.**
1. **Start with Expo Go**: Run `npx expo start` and scan the QR code with Expo Go
2. **Check if features work**: Test your app thoroughly in Expo Go
3. **Only create custom builds when required**
#### When Custom Builds Are Required
Use `npx expo run:ios/android` or `eas build` ONLY when using:
- **Local Expo modules** (custom native code in `modules/`)
- **Apple targets** (widgets, app clips, extensions via `@bacons/apple-targets`)
- **Third-party native modules** not included in Expo Go
- **Custom native configuration** that can't be expressed in `app.json`
#### When Expo Go Works
Expo Go supports a huge range of features out of the box:
- All `expo-*` packages (camera, location, notifications, etc.)
- Expo Router navigation
- Most UI libraries (reanimated, gesture handler, etc.)
- Push notifications, deep links, and more
### Common Commands
```bash
# Development
npx expo start # Start development server
npx expo start --tunnel # If network issues
npx expo start --clear # Clear cache
npx tsc --noEmit # Type check
npx expo install <pkg> # Install Expo-compatible packages
# Building
npx eas-cli@latest build --profile development # Dev build
npx eas-cli@latest build -p ios --profile production --submit # Build and submit iOS
npx testflight # Quick TestFlight shortcut
# Development Client (when native code changes)
npx expo start --dev-client
```
### Code Style Rules
#### File Naming
- Use kebab-case for file names (e.g., `workout-card.tsx`)
- Never use special characters in file names
- Always remove old route files when moving or restructuring navigation
#### Imports
- Use path aliases from `tsconfig.json` instead of relative imports
- Prefer aliases over relative imports for refactors
- Always use import statements at the top of the file
#### Library Preferences
-`expo-audio` not `expo-av`
-`expo-video` not `expo-av`
-`expo-image` with `source="sf:name"` for SF Symbols
-`react-native-safe-area-context` not react-native SafeAreaView
-`process.env.EXPO_OS` not `Platform.OS`
-`React.use` not `React.useContext`
- ❌ Never use modules removed from React Native: Picker, WebView, SafeAreaView, AsyncStorage
- ❌ Never use legacy expo-permissions
- ❌ Never use intrinsic elements like 'img' or 'div' unless in webview
### Route Structure
```
app/
_layout.tsx # Root layout with tabs
(tabs)/ # Group routes for tabs
_layout.tsx # Tabs layout
index.tsx # Home tab
workouts.tsx # Workouts tab
activity.tsx # Activity tab
browse.tsx # Browse tab
profile.tsx # Profile tab
player/
[id].tsx # Workout player
```
#### Route Rules
- Routes belong in the `app` directory
- Never co-locate components, types, or utilities in the app directory
- Ensure the app always has a route that matches "/"
- Always use `_layout.tsx` files to define stacks
### UI Guidelines
#### Responsiveness
- Always wrap root component in a scroll view for responsiveness
- Use `<ScrollView contentInsetAdjustmentBehavior="automatic" />` instead of `<SafeAreaView>`
- `contentInsetAdjustmentBehavior="automatic"` should be applied to FlatList and SectionList
- Use flexbox instead of Dimensions API
- ALWAYS prefer `useWindowDimensions` over `Dimensions.get()`
#### Styling
- Prefer flex gap over margin and padding styles
- Prefer padding over margin where possible
- Inline styles not StyleSheet.create unless reusing styles is faster
- Use `{ borderCurve: 'continuous' }` for rounded corners
- ALWAYS use a navigation stack title instead of a custom text element
- When padding a ScrollView, use `contentContainerStyle` padding
- Add entering and exiting animations for state changes
#### Shadows
Use CSS `boxShadow` style prop. NEVER use legacy React Native shadow or elevation styles.
```tsx
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />
```
#### Text Styling
- Add the `selectable` prop to every `<Text/>` element displaying important data
- Counters should use `{ fontVariant: 'tabular-nums' }` for alignment
#### Safe Area
- Always account for safe area with stack headers, tabs, or ScrollView/FlatList `contentInsetAdjustmentBehavior="automatic"`
- Ensure both top and bottom safe area insets are accounted for
### Navigation Patterns
#### Stack Configuration
```tsx
import { Stack } from 'expo-router/stack';
<Stack
screenOptions={{
headerTransparent: true,
headerShadowVisible: false,
headerLargeTitleShadowVisible: false,
headerLargeStyle: { backgroundColor: "transparent" },
headerTitleStyle: { color: PlatformColor("label") },
headerLargeTitle: true,
headerBlurEffect: "none",
headerBackButtonDisplayMode: "minimal",
}}
>
<Stack.Screen name="index" options={{ title: "Home" }} />
</Stack>
```
#### Links with Previews
```tsx
import { Link } from 'expo-router';
<Link href="/settings">
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Preview />
<Link.Menu>
<Link.MenuAction title="Share" icon="square.and.arrow.up" />
<Link.MenuAction title="Delete" icon="trash" destructive />
</Link.Menu>
</Link>
```
#### Modal/Sheet Presentations
```tsx
// Modal
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
// Form Sheet
<Stack.Screen
name="sheet"
options={{
presentation: "formSheet",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: "transparent" },
}}
/>
```
### Data Fetching
#### Environment Variables
- Use `EXPO_PUBLIC_` prefix for client-side environment variables
- Never put secrets in `EXPO_PUBLIC_` variables (visible in built app)
- Restart the dev server after changing `.env` files
```bash
# .env
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_SUPABASE_URL=your-supabase-url
```
#### API Client Pattern
```tsx
const BASE_URL = process.env.EXPO_PUBLIC_API_URL;
export const apiClient = {
get: async <T,>(path: string): Promise<T> => {
const response = await fetch(`${BASE_URL}${path}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
};
```
#### React Query Setup
```tsx
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 2,
},
},
});
```
### EAS Build Configuration
```json
{
"cli": {
"version": ">= 16.0.1",
"appVersionSource": "remote"
},
"build": {
"production": {
"autoIncrement": true,
"ios": {
"resourceClass": "m-medium"
}
},
"development": {
"developmentClient": true,
"distribution": "internal"
}
},
"submit": {
"production": {}
}
}
```
### Testing & Type Checking
```bash
# TypeScript
npx tsc --noEmit
# Run unit tests (Vitest)
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage report
npm run test:coverage
# Run Maestro E2E tests
npm run test:maestro
# Lint
npx eslint .
```
#### Test Structure
```
src/__tests__/
setup.ts # Mocks and test configuration
stores/ # Zustand store tests
hooks/ # React hooks tests
services/ # Service layer tests
components/ # Component logic tests
data/ # Data validation tests
```
#### Coverage Goals
- **Stores**: 80%+ (business logic)
- **Services**: 80%+ (API integration)
- **Hooks**: 70%+ (timer, purchases)
- **Components**: 50%+ (critical UI)
### Key Takeaways
1. **Start simple**: Always test in Expo Go before creating custom builds
2. **Follow conventions**: Use Expo Router patterns, kebab-case filenames
3. **Use modern APIs**: Prefer `expo-audio`, `expo-video`, `expo-image`
4. **Handle safe areas**: Use `contentInsetAdjustmentBehavior="automatic"`
5. **Style with CSS**: Use `boxShadow` instead of legacy shadow props
6. **Type everything**: Use TypeScript strict mode, no `any`
7. **Secure tokens**: Use `expo-secure-store` for sensitive data
8. **Environment config**: Use `EXPO_PUBLIC_` prefix for client env vars
| | |
|---|---|
| **Nom** | TabataGo |
| **Bundle** | `fr.millianlmx.tabatago` |
| **Team** | `2MJF39L8VY` |
| **But** | App d'entraînement Tabata (intervalles 20s travail / 10s repos) sur iOS + watchOS, avec programmes par zone corporelle, HealthKit, musique, et abonnements RevenueCat |
| **Version** | 1.0 (build 2) — non publiée App Store |
| **Propriétaire** | Millian LMX (CEO) |
| **Gitea** | `https://gitea.1000co.fr/millianlmx/tabatago` |
---
## 📝 Project Context
## 2. Stack technique
**TabataFit** — "Apple Fitness+ for Tabata"
- **Framework**: Expo SDK 52 (managed)
- **Navigation**: Expo Router v3
- **State**: Zustand + AsyncStorage
- **Video**: expo-av → HLS streaming
- **Audio**: expo-av (coaching + music)
- **Animations**: React Native Animated
- **Payments**: RevenueCat
- **Analytics**: PostHog
### Design System
```typescript
BACKGROUND: '#000000' // Pure black
SURFACE: '#1C1C1E' // Charcoal
BRAND: '#FF6B35' // Flame orange
REST: '#5AC8FA' // Ice blue
SUCCESS: '#30D158' // Energy green
```
### Phase Colors (Critical)
```typescript
PREP: '#FF9500' // Orange-yellow
WORK: '#FF6B35' // Flame orange
REST: '#5AC8FA' // Ice blue
COMPLETE: '#30D158' // Green
```
| Couche | Réalité vérifiée |
|--------|------------------|
| Langage | **Swift 6.0**, concurrence stricte (`SWIFT_STRICT_CONCURRENCY: complete`) |
| UI | **SwiftUI** uniquement |
| iOS | **26.0+** |
| watchOS | **11.0+** |
| Build | **XcodeGen**`project.yml` est la source, `.xcodeproj` est généré |
| SPM | **Supabase** `2.5.0+`, **RevenueCat** `5.0.0+` |
| ❌ NON installé | **PostHog** — absent de `project.yml` packages. Voir §3. |
| Architecture | **MVVM + `@Observable`**, `AppState.shared` central |
| Backend | **Supabase** (PostgreSQL, Auth, Storage, Edge Functions Deno) |
| Local cache | **SwiftData** (`TabataGoSchema.container`) |
| Santé | **HealthKit** (fréquence cardiaque, calories, workouts) |
| CI/CD | **Gitea Actions** sur runner macOS auto-hébergé (label `macos`) |
---
*Last updated: March 14, 2026*
## 3. Architecture iOS
# context-mode — MANDATORY routing rules
### Cibles (`tabatago-swift/project.yml`)
You have context-mode MCP tools available. These rules are NOT optional — they protect your context window from flooding. A single unrouted command can dump 56 KB into context and waste the entire session.
| Cible | Type | Plateforme | Bundle ID |
|-------|------|------------|-----------|
| `TabataGo` | application | iOS 26.0 | `fr.millianlmx.tabatago` |
| `TabataGoWatch` | application | watchOS 11.0 | `fr.millianlmx.tabatago.watchkitapp` |
| `TabataGoWatchWidget` | app-extension | watchOS 11.0 | `fr.millianlmx.tabatago.watchkitapp.widget` |
| `TabataGoTests` | bundle.unit-test | iOS | `.tests` |
| `TabataGoUITests` | bundle.ui-testing | iOS | `.uitests` |
## BLOCKED commands — do NOT attempt these
**Dépendances d'intégration** : `TabataGo` embed `TabataGoWatch` qui embed `TabataGoWatchWidget`.
Fichier partagé entre iOS et watch : `TabataGo/Services/WatchConnectivityTypes.swift` (compilé dans les deux targets via `group: TabataGoWatch/Services`).
### curl / wget — BLOCKED
Any shell command containing `curl` or `wget` will be intercepted and blocked by the context-mode plugin. Do NOT retry.
Instead use:
- `context-mode_ctx_fetch_and_index(url, source)` to fetch and index web pages
- `context-mode_ctx_execute(language: "javascript", code: "const r = await fetch(...)")` to run HTTP calls in sandbox
### Bootstrap & data flow
### Inline HTTP — BLOCKED
Any shell command containing `fetch('http`, `requests.get(`, `requests.post(`, `http.get(`, or `http.request(` will be intercepted and blocked. Do NOT retry with shell.
Instead use:
- `context-mode_ctx_execute(language, code)` to run HTTP calls in sandbox — only stdout enters context
```
TabataGoApp (@main)
└─ RootView()
├─ .environment(AppState.shared) // @Observable singleton
├─ .modelContainer(TabataGoSchema.container) // SwiftData
└─ .task { await AppState.shared.bootstrap() }
```
### Direct web fetching — BLOCKED
Do NOT use any direct URL fetching tool. Use the sandbox equivalent.
Instead use:
- `context-mode_ctx_fetch_and_index(url, source)` then `context-mode_ctx_search(queries)` to query the indexed content
`AppState.bootstrap()` (MainActor, idempotent, skip en preview) :
1. `PurchaseService.shared.initialize()` (RevenueCat)
2. `AnalyticsService.shared.initialize()` — ⚠️ **no-op par défaut** car PostHog n'est pas dans SPM : tout est gardé par `#if canImport(PostHog)` qui est toujours faux. La taxonomie d'events existe mais n'émet rien tant que la dépendance n'est pas ajoutée.
## REDIRECTED tools — use sandbox equivalents
### Navigation (PAS de NavigationStack)
### Shell (>20 lines output)
Shell is ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`, and other short-output commands.
For everything else, use:
- `context-mode_ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call
- `context-mode_ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context
`MainTabView` = **`TabView` + `Tab(value:)`** (Liquid Glass, iOS 26), 4 onglets dans cet ordre exact du code :
### File reading (for analysis)
If you are reading a file to **edit** it → reading is correct (edit needs content in context).
If you are reading to **analyze, explore, or summarize** → use `context-mode_ctx_execute_file(path, language, code)` instead. Only your printed summary enters context.
```
home → programs → activity → profile
```
### grep / search (large results)
Search results can flood context. Use `context-mode_ctx_execute(language: "shell", code: "grep ...")` to run searches in sandbox. Only your printed summary enters context.
Modals full-screen via `.sheet`/`fullScreenCover` au-dessus :
`PlayerView`, `CompletionView`, `PaywallView`, `OnboardingView`.
## Tool selection hierarchy
### Services (`TabataGo/Services/`)
1. **GATHER**: `context-mode_ctx_batch_execute(commands, queries)` — Primary tool. Runs all commands, auto-indexes output, returns search results. ONE call replaces 30+ individual calls.
2. **FOLLOW-UP**: `context-mode_ctx_search(queries: ["q1", "q2", ...])` — Query indexed content. Pass ALL questions as array in ONE call.
3. **PROCESSING**: `context-mode_ctx_execute(language, code)` | `context-mode_ctx_execute_file(path, language, code)` — Sandbox execution. Only stdout enters context.
4. **WEB**: `context-mode_ctx_fetch_and_index(url, source)` then `context-mode_ctx_search(queries)` — Fetch, chunk, index, query. Raw HTML never enters context.
5. **INDEX**: `context-mode_ctx_index(content, source)` — Store content in FTS5 knowledge base for later search.
| Service | Rôle |
|---------|------|
| `SupabaseService` | Client Supabase (Auth, DB, Storage) |
| `HealthKitService` | Lecture/écriture HealthKit |
| `PurchaseService` | RevenueCat IAP, état d'abonnement |
| `MusicService` | Apple Music + intégration piste YouTube |
| `AudioService` | Playback audio (coachs sonores, alerts) |
| `AnalyticsService` | **Stub PostHog** (voir ci-dessus) |
| `PhoneConnectivityManager` | Côté iOS — WatchConnectivity (échange avec la montre) |
| `WatchConnectivityTypes` | Protocoles/messages partagés iOS↔Watch (compilé dans les 2 targets) |
## Output constraints
> Le miroir côté watch est `TabataGoWatch/Services/WatchConnectivityManager.swift` (cible watchOS, pas iOS).
- Keep responses under 500 words.
- Write artifacts (code, configs, PRDs) to FILES — never return them as inline text. Return only: file path + 1-line description.
- When indexing content, use descriptive source labels so others can `search(source: "label")` later.
### ViewModels (`@Observable`)
## ctx commands
`HomeViewModel`, `HealthViewModel`, `PlayerViewModel`, `PurchaseViewModel`, `MusicPlayerViewModel`.
| Command | Action |
|---------|--------|
| `ctx stats` | Call the `stats` MCP tool and display the full output verbatim |
| `ctx doctor` | Call the `doctor` MCP tool, run the returned shell command, display as checklist |
| `ctx upgrade` | Call the `upgrade` MCP tool, run the returned shell command, display as checklist |
### Models (`TabataGo/Models/`)
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
`WorkoutProgram`, `WorkoutSession`, `UserProfile`, `HealthSnapshot`, `MusicTrack`, `WorkoutActivityAttributes`, `MusicActivityAttributes` (Live Activities), `TabataGoSchema` (SwiftData), `PreviewData`/`MockPrograms` (previews).
This project is indexed by GitNexus as **tabatago** (3362 symbols, 9407 relationships, 129 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
### Watch (`TabataGoWatch/`)
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
`TabataGoWatchApp``WatchRootView` → états `WatchIdleView` / `WatchActivityView` / `WatchPlayerView`.
Moteur : `WatchPlayerEngine`. Connectivité : `WatchConnectivityManager` (coté watch).
Complications : `TabataGoComplication`.
## Always Do
---
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
## 4. Base de données
## Never Do
### Supabase — source = `supabase/migrations/` (001→006)
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
| # | Fichier | Contenu |
|---|---------|---------|
| 001 | `001_initial_schema.sql` | `trainers`, `workouts`, `collections`, `achievements`, `admin_users` |
| 002 | `002_download_jobs.sql` | `download_jobs`, `download_items` (jobs d'import playlist YouTube) |
| 003 | `003_music_genre.sql` | `music_genre` |
| 004 | `004_download_items_public_read.sql` | RLS : lecture publique de `download_items` |
| 005 | `005_workout_programs.sql` | **DROP** `programs`/`program_workouts`**CREATE** `workout_programs` (body_zone enum `upper-body`/`lower-body`/`full-body` + level `Beginner`/`Intermediate`/`Advanced`) et `program_tabatas` (3 tabatas/program, 2 exercices chacun, 8 rounds × 20s/10s) + RLS public read + admin all |
| 006 | `006_seed_workout_programs.sql` | **18 programmes seedés** (12 free : 4 par zone en 2B+1I+1A ; 6 premium) + **54 tabatas** (18×3), UUIDs stables |
## Resources
**Tables actives post-migration** : `trainers`, `workouts`, `collections`, `achievements`, `admin_users`, `download_jobs`, `download_items`, `music_genre`, `workout_programs`, `program_tabatas`.
| Resource | Use for |
|----------|---------|
| `gitnexus://repo/tabatago/context` | Codebase overview, check index freshness |
| `gitnexus://repo/tabatago/clusters` | All functional areas |
| `gitnexus://repo/tabatago/processes` | All execution flows |
| `gitnexus://repo/tabatago/process/{name}` | Step-by-step execution trace |
**Tables supprimées (ne pas recréer)** : `programs`, `program_workouts` (DROP en 005).
## CLI
**RLS** : lecture publique sur `workout_programs`/`program_tabatas`/`download_items` ; écriture restreinte aux `admin_users` (`EXISTS (SELECT 1 FROM admin_users WHERE id = auth.uid())`).
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
### SwiftData — cache local
<!-- gitnexus:end -->
`TabataGoSchema.container` (et `.previewContainer` pour les `#Preview`). Persistance offline des sessions/programmes côté app. Ne pas confondre avec Supabase.
---
## 5. CI/CD
### Workflow : `.gitea/workflows/pr-iphone-deploy.yml`
**Trigger** : PR ouverte/synchronize/reopened sur `main`.
**Runner** : label `macos` (auto-hébergé).
**Déroulé** :
1. **Checkout** (shallow, branche head) via `PR_API_TOKEN`.
2. **Setup PATH** : `echo "/opt/homebrew/bin" >> $GITHUB_PATH` — requis car Rosetta ne le voit pas.
3. **Clean ciblé** :
- Supprime build artifacts, `TabataGo.xcodeproj`, `Package.resolved`, DerivedData, ModuleCache.
- **GARDE le cache SPM** (`../build/spm-cache`) — RevenueCat ≈ 1.1 GiB, re-cloner = 5+ min et échecs aléatoires du sandbox.
4. **Install tools** : `brew install xcodegen node ios-deploy`.
5. **xcodegen generate**`xcodebuild -resolvePackageDependencies`**build** (`-scheme TabataGo`, Debug, auto-provisioning, team `2MJF39L8VY`).
6. **Deploy iPhone** UDID `00008120-000925CE3672201E` : `devicectl` WiFi d'abord, fallback `ios-deploy` USB.
7. **Post comment** "Prêt à tester" sur la PR.
8. **Job `wait-approval`** : poll (30s, max 240 = 2h) les commentaires de la PR. `LGTM` → auto-merge. `KO` → blocage. Timeout → fail.
### Secrets
| Secret | Usage |
|--------|-------|
| `PR_API_TOKEN` | Checkout + API Gitea (comment, merge). **JAMAIS `GITEA_TOKEN`/`GITHUB_TOKEN`** |
| `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `REVENUECAT_API_KEY`, `POSTHOG_API_KEY` | Injectés via `Config/Secrets.xcconfig` → Info.plist |
### ⚠️ Pitfalls CI
- `/opt/homebrew/bin` **hors PATH par défaut** sous Rosetta → toujours l'ajouter.
- **xcodegen 2.45.4 ne génère pas de schemes auto** → scheme `TabataGo` défini explicitement dans `project.yml` (ne pas le supprimer).
- **Ne jamais `rm -rf` le cache SPM** dans le CI (c'est l'inverse des build artifacts).
- `SWIFT_ENABLE_EXPLICIT_MODULES=NO` au build — requis sinon segfault linker sur `.pcm` stale.
- `-skipPackagePluginValidation -allowProvisioningUpdates`.
---
## 6. Sous-projets
### `admin-web/` — Dashboard admin
Next.js 15 App Router, shadcn/ui. Gestion workouts / trainers / collections / programs.
Auth Supabase → table `admin_users`. Stack : TypeScript, `middleware.ts`, tests Playwright (`e2e/`) + Vitest.
### `youtube-worker/` — Worker YouTube
Node.js (`server.js`, `package.json`, `Dockerfile`). Télécharge l'audio de playlists YouTube → **Supabase Storage**. Piloté par les Edge Functions ci-dessous.
### `supabase/functions/` — Edge Functions (Deno)
| Function | Rôle |
|----------|------|
| `youtube-playlist/` | Crée un `download_job`, liste les vidéos |
| `youtube-process/` | Orchestre le téléchargement (via youtube-worker) |
| `youtube-status/` | Statut d'un job |
| `youtube-classify/` | Classification genre musical |
| `main/` | Auth JWT (helper `jose`) |
| `_shared/` | `auth.ts`, `cors.ts`, `supabase-client.ts`, `youtube-client.ts` |
> Le pipeline musical complet : Edge Functions orchestrent → `youtube-worker` (Node.js) télécharge via **yt-dlp** + **Innertube** → audio dans Supabase Storage (`workout-audio` bucket) → classification automatique par **Gemini** (`gemini-3.1-flash-lite-preview`) → métadonnées dans `download_jobs`/`download_items` → genre dans `music_genre`.
---
## 7. Règles d'or pour les agents
1. **Source de vérité = `project.yml` + `supabase/migrations/`.** Tout le reste est dérivé ou obsolète.
2. **SwiftUI only.** Pas d'UIKit sauf nécessité absolue démontrée.
3. **`@Observable`** (macro Observation), **jamais `@ObservableObject`/`@Published`**.
4. **Concurrency stricte.** `async/await` partout, `@MainActor` sur l'UI, pas de completion handlers.
5. **Pas de force-unwrap.** `guard let`/`if let`, jamais `!`.
6. **Navigation = TabView + sheet/fullScreenCover.** **Pas de `NavigationStack`.**
7. **Tout en français** côté user-facing (`Localizable.xcstrings` / `L10n`). Codes/types restent en anglais.
8. **XcodeGen.** Éditer `project.yml`, pas `.xcodeproj`. Régénérer avec `xcodegen generate`.
9. **Secrets.** Jamais committer `Config/Secrets.xcconfig`. Template = `.example`.
10. **Lazy d'abord.** Avant d'ajouter une dépendance : stdlib Apple, puis ce qui existe déjà dans le repo, puis SPM déjà installé. PostHog n'est pas installé — ne pas l'ajouter sans justification.
---
## 8. Anti-patterns / pièges documentés
| ❌ Ne pas faire | ✅ Faire |
|-----------------|---------|
| Lire `CLAUDE.md` ou `schema.sql` pour le schéma | Lire `supabase/migrations/001→006` |
| Importer PostHog comme si c'était actif | Savoir que `AnalyticsService` est un **no-op** (PostHog pas en SPM) |
| Utiliser `NavigationStack` | `TabView` + modals |
| `@ObservableObject` / `@Published` | `@Observable` |
| Force-unwrap `!` | `guard let` |
| Strings UI en anglais | Françaises dans `L10n`/`Localizable.xcstrings` |
| `rm -rf` le cache SPM en CI | Garder `build/spm-cache` |
| Utiliser `GITEA_TOKEN` en CI | `PR_API_TOKEN` uniquement |
| Supprimer le scheme explicite dans `project.yml` | xcodegen 2.45.4 n'en crée pas |
| Compter sur `admin-web/` pour la app iOS | Dashboard admin séparé, communique via Supabase uniquement |
| Recréer `programs`/`program_workouts` | Remplacés par `workout_programs`/`program_tabatas` (migration 005) |
---
## 9. Contexte projet & skills
- **Repo Gitea** : `https://gitea.1000co.fr/millianlmx/tabatago` (branche intégration : `main`).
- **Équipe** : "Millian Team" — équipe d'agents IA. Skills à loader selon la tâche :
- `senior-ios` — SwiftUI, Xcode, iOS/watchOS
- `senior-backend` — Supabase, SQL, Edge Functions
- `senior-devops` — CI/CD Gitea Actions, deploy
- `po-pm` — vision produit, specs, validation
- **Team skill TabataGo** : documente les anti-patterns ci-dessus ; aligne tous les agents sur les mêmes conventions.
- **Hermes profile actif** : `default`. Les skills vivent dans le profile sous `skills/`.
- **Docs utiles** : `docs/ci-cd-setup.md`, `docs/app-store-submission.md`, `docs/maestro-e2e-testing-strategy.md`, `docs/ui-feature-brief.md`.
- **Scripts** : `scripts/ci-status.py`, `scripts/deploy-functions.sh`.
### Commandes usuelles
```bash
# Générer le projet Xcode
cd tabatago-swift && xcodegen generate
# Ouvrir
open tabatago-swift/TabataGo.xcodeproj
# Déployer les Edge Functions
bash scripts/deploy-functions.sh
# Statut CI
python3 scripts/ci-status.py
```

142
AGENTS.md.old Normal file
View File

@@ -0,0 +1,142 @@
# TabataGo — iOS Tabata Workout App
> Tabata interval training app for iOS + watchOS with HealthKit integration.
## Agent Context
This project is part of the **Millian Team** — an AI agent engineering team.
When working here, agents should load the relevant skill:
- `senior-ios` — SwiftUI, Xcode, iOS/watchOS development
- `senior-backend` — Supabase backend
- `senior-devops` — Deploy infrastructure
- `po-pm` — Product vision, specs, validation
## Tech Stack
| Layer | Tech |
|-------|------|
| App | SwiftUI, iOS 26.0+ |
| Watch | SwiftUI, watchOS 11.0+ |
| Widget | WidgetKit, Live Activities |
| Language | Swift 6.0 (strict concurrency) |
| Architecture | MVVM, @Observable, AppState central |
| Backend | Supabase (PostgreSQL, Auth, Storage) |
| Subscriptions | RevenueCat 5.x |
| Analytics | PostHog 3.x |
| Health | HealthKit (heart rate, calories, workouts) |
| Music | Apple Music integration + YouTube worker |
| Build | XcodeGen (project.yml → .xcodeproj) |
| Package Manager | Swift Package Manager |
## Project Structure
```
tabatago/
├── tabatago-swift/ # Main Xcode project
│ ├── project.yml # XcodeGen spec
│ ├── Config/
│ │ ├── Secrets.xcconfig # API keys (not committed)
│ │ └── Secrets.xcconfig.example
│ ├── TabataGo/ # iOS App
│ │ ├── App/ # App entry, state, root view
│ │ │ ├── TabataGoApp.swift
│ │ │ ├── RootView.swift
│ │ │ └── AppState.swift
│ │ ├── Views/
│ │ │ ├── Tabs/ # MainTabView, Home, Activity, Programs, Profile
│ │ │ ├── Player/ # Workout player (PlayerView, NowPlaying)
│ │ │ ├── Programs/ # ProgramDetail, BodyZone
│ │ │ ├── Onboarding/ # Onboarding flow
│ │ │ ├── Paywall/ # RevenueCat paywall
│ │ │ ├── Complete/ # Workout completion screen
│ │ │ ├── Settings/ # Settings, policies
│ │ │ └── Components/ # Shared UI components
│ │ ├── ViewModels/ # HomeVM, HealthVM, PlayerVM, PurchaseVM, MusicVM
│ │ ├── Models/ # WorkoutProgram, WorkoutSession, UserProfile, etc.
│ │ ├── Services/ # Supabase, HealthKit, Music, Audio, Purchase, Analytics
│ │ ├── Theme/ # App theme
│ │ ├── Utilities/ # Strings, Environment
│ │ └── Resources/ # Assets, Localizable, entitlements
│ ├── TabataGoWatch/ # watchOS App
│ │ ├── Views/ # WatchRoot, WatchIdle, WatchActivity, WatchPlayer
│ │ ├── Services/ # WatchConnectivity, WatchPlayerEngine
│ │ ├── Complications/ # Watch face complications
│ │ └── Resources/
│ ├── TabataGoWidget/ # iOS Widget + Live Activities
│ ├── TabataGoTests/ # Unit tests
│ └── TabataGoUITests/ # UI tests
├── supabase/ # Supabase config
│ ├── schema.sql
│ ├── seed.sql
│ ├── setup-admin.sql
│ └── migrations/ # SQL migrations (001-006)
└── youtube-worker/ # Node.js worker for YouTube music
├── server.js
├── package.json
└── Dockerfile
```
## Dependencies (SPM)
- **Supabase** 2.5+ — Backend SDK (Auth, Database, Storage)
- **RevenueCat** 5.0+ — In-app purchases
- **PostHog** 3.0+ — Analytics
## Features
- Tabata/interval workout player with configurable timers
- Pre-built workout programs by body zone
- HealthKit integration (heart rate, calories, workout saving)
- Apple Watch companion with real-time metrics
- Live Activities + Dynamic Island
- Music integration (Apple Music + YouTube)
- RevenueCat subscriptions
- Onboarding flow
- Activity tracking & workout history
## Commands
```bash
# From tabatago-swift/
xcodegen generate # Generate .xcodeproj
open TabataGo.xcodeproj # Open in Xcode
# In Xcode
Cmd+R → Run on simulator
Cmd+U → Run tests
```
## Configuration
- `Config/Secrets.xcconfig` contains API keys (not committed)
- Template: `Config/Secrets.xcconfig.example`
- Env vars: `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `REVENUECAT_API_KEY`, `POSTHOG_API_KEY`
## Current State
- **Version**: 1.0 (build 2) — not yet released
- **State**: App functional, needs finishing work
- **Platforms**: iOS 26.0+, watchOS 11.0+
- **Requires**: macOS with Xcode 26+ to build
## Supabase Schema
Tables: `profiles`, `workout_sessions`, `workout_programs`, `download_jobs`, `music_genre`, etc.
See `supabase/schema.sql` and `supabase/migrations/` for full schema.
## Team Rules
1. **Swift Concurrency** — async/await everywhere, no completion handlers
2. **@Observable** — not @ObservableObject
3. **No force unwrap**`guard let` always, never `!`
4. **SwiftUI only** — no UIKit unless unavoidable
5. **XcodeGen** — project.yml is source of truth, not .xcodeproj
6. **Secrets** — never commit Secrets.xcconfig
7. **French** — all user-facing strings in French (Localizable.xcstrings)
## Project Context
- **User**: Millian LMX (CEO)
- **Gitea**: https://gitea.1000co.fr/millianlmx/tabatago
- **Supabase**: Managed instance (URL in secrets)
- **App Store**: Not yet published

View File

@@ -1,101 +0,0 @@
# Authentication Setup Guide
## Overview
This guide sets up full server-side authentication for the TabataFit Admin panel.
## Prerequisites
- Supabase project running (local or hosted)
- Admin-web Next.js app running
- Database schema already applied
## Step 1: Configure Supabase Dashboard
1. Go to your Supabase Dashboard → Authentication → Providers
2. **Enable Email Provider**
3. **Disable "Confirm email"** (for easier testing, re-enable for production)
4. Go to Authentication → URL Configuration
5. Set **Site URL**: `http://localhost:3000` (or your production URL)
6. Add **Redirect URLs**: `http://localhost:3000/**`
## Step 2: Create Admin User
### Option A: Via Supabase Dashboard (Easiest)
1. Go to Supabase Dashboard → Authentication → Users
2. Click "Add user" or "Invite user"
3. Enter email: `admin@tabatafit.com`
4. Set password: Your chosen password
5. Click "Create user"
6. Copy the user's UUID (click on the user to see details)
### Option B: Via Login Page
1. Go to `http://localhost:3000/login`
2. Click "Sign up" (if available) or use dashboard method above
## Step 3: Add User to Admin Table
**Important**: After creating the user, you MUST add them to the admin_users table.
Run this SQL in Supabase SQL Editor:
```sql
-- Replace with your actual email or UUID
INSERT INTO public.admin_users (id, email, role)
SELECT id, email, 'admin'
FROM auth.users
WHERE email = 'admin@tabatafit.com'
ON CONFLICT (id) DO NOTHING;
```
Or if you have the UUID:
```sql
INSERT INTO public.admin_users (id, email, role)
VALUES ('paste-uuid-here', 'admin@tabatafit.com', 'admin');
```
## Step 4: Verify Setup
Run this to confirm:
```sql
SELECT au.id, au.email, au.role, u.email as auth_email
FROM public.admin_users au
JOIN auth.users u ON au.id = u.id;
```
You should see your admin user listed.
## Step 5: Login
1. Go to `http://localhost:3000/login`
2. Enter email: `admin@tabatafit.com`
3. Enter password: (the one you set)
4. You should be redirected to the dashboard
## Troubleshooting
### "Not authorized as admin" error
- User exists in auth.users but not in admin_users table
- Run Step 3 SQL to add them
### "Failed to fetch" errors
- Check that EXPO_PUBLIC_SUPABASE_URL is set in .env.local
- For admin-web, also add NEXT_PUBLIC_SUPABASE_URL with same value
### Still can't create workouts
- Verify you're logged in (check browser cookies)
- Check that admin_users table has your user
- Check RLS policies are applied correctly
## Default Credentials (Example)
**Email**: `admin@tabatafit.com`
**Password**: (You choose during setup)
**Note**: Change this to a secure password in production!
## Security Notes
1. **Production**: Enable email confirmations
2. **Production**: Use strong passwords
3. **Production**: Enable 2FA if available
4. **Production**: Restrict CORS origins in Supabase
5. **Production**: Use environment-specific admin credentials

View File

@@ -1,218 +0,0 @@
# TabataFit Admin Dashboard - Test Implementation Summary
## ✅ Completed Implementation
### 1. Unit Tests
#### UI Components (100% Coverage)
- **button.test.tsx** ✓ (already existed)
- **select.test.tsx** ✓ (already existed)
- **switch.test.tsx** ✓ (already existed)
- **dialog.test.tsx** ✓ **NEW** - 8 test cases
- Dialog rendering
- Open/close behavior
- Click outside to close
- Escape key handling
- Footer rendering
- Custom styling
- Accessibility features
#### Custom Components
- **exercise-list.test.tsx** ✓ (already existed)
- **media-upload.test.tsx** ✓ (42 tests, 90%+ coverage)
- **tag-input.test.tsx** ✓ (already existed)
- **sidebar.test.tsx** ✓ **NEW** - 11 test cases
- Logo and navigation rendering
- Active state highlighting
- Navigation links
- Logout functionality
- Error handling
- Icon rendering
- Styling verification
### 2. Integration Tests
#### Page Tests
- **login/page.test.tsx** ✓ **NEW** - 11 test cases
- Form rendering
- Input validation
- Successful login
- Invalid credentials
- Non-admin user handling
- Loading states
- Network error handling
- **page.test.tsx** (Dashboard) ✓ **NEW** - 12 test cases
- Header rendering
- Stats cards display
- Loading states
- Quick actions section
- Getting started section
- Route links
- Error handling
- Icon verification
- Color classes
### 3. E2E Tests
#### Playwright Configuration ✓ **NEW**
- Installed @playwright/test
- Configured for Chromium and Firefox
- Automatic dev server startup
- Screenshot on failure
- HTML reporting
#### E2E Test Suite
- **e2e/auth.spec.ts** ✓ **NEW**
- Login form display
- Validation errors
- Route protection
- Navigation between pages
### 4. Test Scripts Added
```json
{
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed",
"test:all": "npm run test && npm run test:e2e"
}
```
## 📊 Current Test Coverage
| Metric | Before | After | Target |
|--------|--------|-------|--------|
| Lines | 88% | ~92% | 95% |
| Functions | 92% | ~94% | 95% |
| Branches | 78% | ~82% | 85% |
| Statements | 85% | ~90% | 90% |
## 🧪 Test Files Created
### New Test Files (7 files)
1. `components/ui/dialog.test.tsx`
2. `components/sidebar.test.tsx`
3. `app/login/page.test.tsx`
4. `app/page.test.tsx`
5. `e2e/auth.spec.ts`
6. `playwright.config.ts`
### Modified Files
- `package.json` - Added E2E test scripts
## 🎯 Test Categories
### Unit Tests
- ✅ Component rendering
- ✅ User interactions (clicks, input)
- ✅ State changes
- ✅ Props validation
- ✅ Styling verification
- ✅ Accessibility checks
### Integration Tests
- ✅ Page routing
- ✅ Data fetching
- ✅ Form submissions
- ✅ Error handling
- ✅ Loading states
### E2E Tests
- ✅ Authentication flows
- ✅ Navigation
- ✅ Route protection
- ✅ Cross-browser testing
## 🚀 How to Run Tests
### Unit Tests
```bash
# Run all unit tests
npm run test
# Run with coverage
npm run test:coverage
# Watch mode
npm run test:watch
```
### E2E Tests
```bash
# Run E2E tests
npm run test:e2e
# Run with UI
npm run test:e2e:ui
# Run in headed mode (see browser)
npm run test:e2e:headed
```
### All Tests
```bash
# Run both unit and E2E tests
npm run test:all
```
## 📋 Remaining Work (Optional)
### High Priority
- [ ] workouts/page.test.tsx - Full CRUD tests
- [ ] trainers/page.test.tsx - Delete dialog tests
- [ ] workout-form.tsx coverage improvement (currently 71%)
### Medium Priority
- [ ] collections/page.test.tsx
- [ ] media/page.test.tsx
- [ ] Additional E2E tests for CRUD operations
### Low Priority
- [ ] Visual regression tests with Storybook
- [ ] Performance benchmarks
- [ ] Accessibility audit with axe
## 🎉 Achievements
**All Critical Paths Tested**
- Authentication flow
- Dashboard stats
- Navigation
- Sidebar functionality
- UI component library
**Test Infrastructure Complete**
- Vitest configured
- Playwright configured
- Mock utilities ready
- CI/CD scripts prepared
**Coverage Improved**
- Added 42+ new test cases
- Improved dialog component to 100%
- Added sidebar component tests
- Added page integration tests
## 🔧 Testing Best Practices Implemented
1. **Mock External Dependencies** - Supabase, Next.js router
2. **Test User Interactions** - Clicking, typing, navigation
3. **Verify Error States** - Network errors, auth failures
4. **Check Accessibility** - Roles, labels, ARIA attributes
5. **Test Responsive Behavior** - Different viewports (E2E)
6. **Isolate Tests** - Clean mocks between tests
## 📚 Documentation
- Each test file is self-documenting
- Descriptive test names explain the behavior
- Comments for complex mock setups
- Type safety with TypeScript
---
**Status**: ✅ **Phase 1 Complete** - Core testing infrastructure implemented
**Next Steps**: Run `npm run test:all` to execute full test suite!

View File

@@ -44,7 +44,7 @@ Or run manually:
2. Archives the app with Xcode auto-signing
3. Exports an App Store IPA
4. Uploads to App Store Connect
5. The first upload **automatically creates** the app record in App Store Connect (bundle ID: `com.tabatago.app`)
5. The first upload **automatically creates** the app record in App Store Connect (bundle ID: `fr.millianlmx.tabatago`)
## After Upload
@@ -60,7 +60,7 @@ Or run manually:
| Symptom | Likely Cause |
|---|---|
| "No accounts with iTunes Connect access" | API key doesn't have App Manager permissions — recreate the key with correct access |
| "No profiles found" | The bundle ID `com.tabatago.app` isn't registered yet — check Apple Developer portal |
| "No profiles found" | The bundle ID `fr.millianlmx.tabatago` isn't registered yet — check Apple Developer portal |
| "Duplicate build number" | Build number already used — bump `CFBundleVersion` in `project.yml` and re-tag |
| "Authentication failed" | API key was revoked, expired, or secret is misspelled — verify secrets in repository settings |

200
docs/ci-cd-setup.md Normal file
View File

@@ -0,0 +1,200 @@
# CI/CD Setup — iPhone Deploy Workflow
This guide covers setting up a **Gitea Actions** macOS runner to automatically build, test, and deploy **TabataGo** to a physical iPhone on every PR to `main`.
## Prerequisites
### Hardware
- **Mac** running macOS 15 (Sequoia) or later
- **Xcode 26+** installed (required for iOS 26.0 / watchOS 11.0 targets)
- **iPhone** (iOS 26+) with the Firebase App Tester app installed
### Software
- [Gitea Actions Runner](https://docs.gitea.com/usage/actions/act-runner) installed and registered
- `brew` (Homebrew) installed
- `xcodegen``brew install xcodegen` (also installed automatically by the workflow)
## 1. Register the macOS Runner
```bash
# On the Mac:
./act_runner register \
--instance https://gitea.1000co.fr \
--token <RUNNER_TOKEN> \
--name "macos-runner" \
--labels macos
# Start the runner
./act_runner daemon
```
> **Important:** The runner must have the label `macos` because the workflow uses `runs-on: macos`.
## 2. Configure Secrets
Add these secrets in the Gitea repository:
`Settings → Actions → Secrets`
| Secret Name | Description | How to Get |
|-------------|-------------|------------|
| `FIREBASE_APP_ID` | Firebase App ID (iOS app) | Firebase Console → Project Settings → General → App ID |
| `FIREBASE_SERVICE_ACCOUNT` | Base64-encoded Firebase service account JSON key | See [Firebase Setup](#6-firebase-app-distribution-setup) below |
| `GITEA_TOKEN` | Gitea API token with repo + pull request scope | `Gitea → Settings → Applications → Generate Token` |
### Gitea Token Permissions
The token needs:
- `read:repository` — to read PR metadata
- `write:issue` — to post LGTM comments
- `write:pull_request` — to merge approved PRs
## 3. Xcode Signing
The workflow uses **automatic code signing**. Ensure:
1. The Mac runner is signed into an Apple Developer account in Xcode:
```bash
# Verify:
xcrun security find-identity -v -p codesigning
```
2. The Apple ID used must be part of the team `2MJF39L8VY` (configured in `ExportOptions.plist`).
3. For first-time setup, open Xcode on the runner Mac and sign in:
```
Xcode → Settings → Accounts → Add Apple ID
```
## 4. Workflow Overview
The workflow at `.gitea/workflows/pr-iphone-deploy.yml` triggers on PRs to `main`:
```
PR Opened / Updated
Checkout + XcodeGen generate
Resolve SPM packages
Build + Archive IPA
Upload to Firebase App Distribution
Post "Ready to test" comment on PR
Wait for LGTM → Auto-merge
```
## 5. Manual Test Run
To test the workflow locally on the runner Mac:
```bash
# Simulate the build:
cd tabatago-swift
xcodegen generate
xcodebuild build \
-project TabataGo.xcodeproj \
-target TabataGo \
-sdk iphoneos \
-destination "generic/platform=iOS" \
-configuration Release \
-allowProvisioningUpdates \
-skipPackagePluginValidation \
CODE_SIGN_STYLE=Automatic
```
## 6. Firebase App Distribution Setup
### 6.1 Create a Firebase Project
1. Go to [Firebase Console](https://console.firebase.google.com)
2. Click **Add project** (or select an existing one)
3. Follow the wizard to create the project (Google Analytics is optional)
### 6.2 Enable App Distribution
1. In the Firebase Console, open your project
2. Navigate to **App Distribution** in the left sidebar
3. Click **Get started** to enable the service
### 6.3 Get the App ID
1. Go to **Project Settings** (gear icon) → **General**
2. Scroll down to **Your apps** → find the iOS app
3. Copy the **App ID** (looks like `1:123456789012:ios:abc123def456`)
### 6.4 Create a Service Account
1. Go to **Project Settings** → **Service accounts**
2. Click **Create service account** (or use Firebase Admin SDK → Generate new private key)
3. Give it a name like `github-actions-deploy`
4. Assign the role: **Firebase App Distribution Admin**
5. Click **Generate new private key** → download the JSON file
### 6.5 Encode the Service Account Key
```bash
# Convert the JSON key to a base64 string (single line, no wrapping)
base64 -i firebase-key.json | tr -d '\n'
```
Copy the output — this is your `FIREBASE_SERVICE_ACCOUNT` secret value.
### 6.6 Add Testers and Groups
1. In Firebase Console, go to **App Distribution** → **Testers & Groups**
2. Click **Add group** → name it `millian-test`
3. Click **Add testers** → enter Millian's email
4. Add the tester to the `millian-test` group
### 6.7 Install the Firebase App Tester on the iPhone
1. Send an invite to Millian's iPhone from the Firebase Console
2. Millian accepts the invite and installs the **Firebase App Tester** app from the App Store
3. Open the app and sign in with the invited Google account
4. The app will now receive builds pushed from the CI workflow
## Troubleshooting
| Issue | Solution |
|-------|----------|
| "Code signing failed" | Sign into Xcode on the runner Mac, verify team membership |
| "No provisioning profile" | Run `-allowProvisioningUpdates` or open project in Xcode once |
| `xcodegen: command not found` | `brew install xcodegen` (workflow handles this) |
| Runner doesn't pick up job | Verify runner label is `macos` and runner is connected |
| Token permission errors | Ensure `GITEA_TOKEN` has `write:issue` and `write:pull_request` scopes |
| Firebase upload fails | Verify `FIREBASE_APP_ID` and `FIREBASE_SERVICE_ACCOUNT` secrets are set correctly |
| "App not found" in Firebase | Check that the iOS app is registered in Firebase with the correct bundle ID |
| Testers don't receive build | Verify tester is in the `millian-test` group and has accepted the invite |
## Compilation Error Troubleshooting
### xcodegen Clearing Signing Team
After running `xcodegen generate`, the `.xcodeproj` may have a cleared signing team. Always re-check your team settings:
```bash
cd tabatago-swift
xcodegen generate
# Then check that project.yml has the correct team ID in settings
```
### Complication Targets Missing Team ID
Targets like **TabataGoWatchWidget** (watchOS complications) require an explicit team ID in `project.yml` settings. If you see signing errors for the widget target:
1. Open `tabatago-swift/project.yml`
2. Find the `TabataGoWatchWidget` target section
3. Ensure it has an explicit `settings:` block with the team ID:
```yaml
target: TabataGoWatchWidget
settings:
base:
DEVELOPMENT_TEAM: "2MJF39L8VY"
```
### General Build Debugging
- Run `xcodegen generate` before every build to ensure the project is in sync
- Check `xcodebuild` output for specific file/line errors
- Verify SPM packages resolve: `xcodebuild -resolvePackageDependencies`

View File

@@ -1,643 +0,0 @@
# Maestro E2E Testing Strategy for TabataFit
## Executive Summary
**Maestro** is a mobile UI testing framework that uses YAML-based test flows. It's ideal for TabataFit because:
- ✅ Declarative YAML syntax (no code required)
- ✅ Built-in support for React Native
- ✅ Handles animations and async operations gracefully
- ✅ Excellent for regression testing critical user flows
- ✅ Can run on physical devices and simulators
---
## Prerequisites
Before implementing these tests, ensure the following features are complete:
### Required Features (Implement First)
- [ ] Onboarding flow (5 screens + paywall)
- [ ] Workout player with timer controls
- [ ] Browse/Workouts tab with workout cards
- [ ] Category filtering (Full Body, Core, Cardio, etc.)
- [ ] Collections feature
- [ ] Trainer profiles
- [ ] Subscription/paywall integration
- [ ] Workout completion screen
- [ ] Profile/settings screen
### Nice to Have (Can Add Later)
- [ ] Activity history tracking
- [ ] Offline mode support
- [ ] Deep linking
- [ ] Push notifications
---
## Priority Test Flows
### **P0 - Critical Flows (Must Test Every Release)**
1. **Onboarding → First Workout**
2. **Browse → Select Workout → Play → Complete**
3. **Subscription Purchase Flow**
4. **Background/Foreground During Workout**
### **P1 - High Priority**
5. **Category Filtering**
6. **Collection Navigation**
7. **Trainer Workout Discovery**
8. **Profile Settings & Data Persistence**
### **P2 - Medium Priority**
9. **Activity History Tracking**
10. **Offline Mode Behavior**
11. **Deep Linking**
12. **Push Notifications**
---
## Test Suite Structure
```
.maestro/
├── config.yaml # Global test configuration
├── flows/
│ ├── critical/ # P0 flows - Run on every PR
│ │ ├── onboarding.yaml
│ │ ├── workoutComplete.yaml
│ │ └── subscription.yaml
│ ├── core/ # P1 flows - Run before release
│ │ ├── browseAndPlay.yaml
│ │ ├── categoryFilter.yaml
│ │ ├── collections.yaml
│ │ └── trainers.yaml
│ └── regression/ # P2 flows - Run nightly
│ ├── activityHistory.yaml
│ ├── offlineMode.yaml
│ └── settings.yaml
├── helpers/
│ ├── common.yaml # Shared test steps
│ ├── assertions.yaml # Custom assertions
│ └── mock-data.yaml # Test data
└── environments/
├── staging.yaml
└── production.yaml
```
---
## Installation & Setup
### 1. Install Maestro CLI
```bash
# macOS/Linux
curl -Ls "https://get.maestro.mobile.dev" | bash
# Verify installation
maestro --version
```
### 2. Setup Test Directory Structure
```bash
mkdir -p .maestro/flows/{critical,core,regression}
mkdir -p .maestro/helpers
mkdir -p .maestro/environments
```
### 3. Maestro Configuration (`config.yaml`)
```yaml
# .maestro/config.yaml
appId: com.tabatafit.app
name: TabataFit E2E Tests
# Timeouts
timeout: 30000 # 30 seconds default
retries: 2
# Environment variables
env:
TEST_USER_NAME: "Test User"
TEST_USER_EMAIL: "test@example.com"
# Include flows
include:
- flows/critical/*.yaml
- flows/core/*.yaml
# Exclude on CI
exclude:
- flows/regression/offlineMode.yaml # Requires airplane mode
```
---
## P0 Critical Test Flows
### Test 1: Complete Onboarding Flow
**File:** `.maestro/flows/critical/onboarding.yaml`
```yaml
appId: com.tabatafit.app
name: Complete Onboarding & First Workout
onFlowStart:
- clearState
steps:
# Splash/Loading
- waitForAnimationToEnd:
timeout: 5000
# Screen 1: Problem - "Not Enough Time"
- assertVisible: "Not enough time"
- tapOn: "Continue"
# Screen 2: Empathy
- assertVisible: "We get it"
- tapOn: "Continue"
# Screen 3: Solution
- assertVisible: "4-minute workouts"
- tapOn: "Continue"
# Screen 4: Wow Moment
- assertVisible: "Transform your body"
- tapOn: "Get Started"
# Screen 5: Personalization
- tapOn: "Name input"
- inputText: "Test User"
- tapOn: "Beginner"
- tapOn: "Lose Weight"
- tapOn: "3 times per week"
- tapOn: "Start My Journey"
# Screen 6: Paywall (or skip in test env)
- runScript: |
if (maestro.env.SKIP_PAYWALL === 'true') {
maestro.tapOn('Maybe Later');
}
# Should land on Home
- assertVisible: "Good morning|Good afternoon|Good evening"
- assertVisible: "Test User"
onFlowComplete:
- takeScreenshot: "onboarding-complete"
```
### Test 2: Browse, Select, and Complete Workout
**File:** `.maestro/flows/critical/workoutComplete.yaml`
```yaml
appId: com.tabatafit.app
name: Browse, Play & Complete Workout
steps:
# Navigate to Workouts tab
- tapOn: "Workouts"
- waitForAnimationToEnd
# Wait for data to load
- assertVisible: "All|Full Body|Core|Upper Body"
# Select first workout
- tapOn:
id: "workout-card-0"
optional: false
# Workout Detail Screen
- assertVisible: "Start Workout"
- tapOn: "Start Workout"
# Player Screen
- waitForAnimationToEnd:
timeout: 3000
# Verify timer is running
- assertVisible: "Get Ready|WORK|REST"
# Fast-forward through workout (simulation)
- repeat:
times: 3
commands:
- waitForAnimationToEnd:
timeout: 5000
- assertVisible: "WORK|REST"
# Complete workout
- tapOn:
id: "done-button"
optional: true
# Complete Screen
- assertVisible: "Workout Complete|Great Job"
- assertVisible: "Calories"
- assertVisible: "Duration"
# Return to home
- tapOn: "Done|Continue"
- assertVisible: "Home|Workouts"
onFlowComplete:
- takeScreenshot: "workout-completed"
```
### Test 3: Subscription Purchase Flow
**File:** `.maestro/flows/critical/subscription.yaml`
```yaml
appId: com.tabatafit.app
name: Subscription Purchase Flow
steps:
# Trigger paywall (via profile or workout limit)
- tapOn: "Profile"
- tapOn: "Upgrade to Premium"
# Paywall Screen
- assertVisible: "Unlock Everything|Premium"
- assertVisible: "yearly|monthly"
# Select plan
- tapOn:
id: "yearly-plan"
# Verify Apple Pay/Google Pay sheet appears
- assertVisible: "Subscribe|Confirm"
# Note: Actual purchase is mocked in test env
- runScript: |
if (maestro.env.USE_MOCK_PURCHASE === 'true') {
maestro.tapOn('Mock Purchase Success');
}
# Verify premium activated
- assertVisible: "Premium Active|You're all set"
tags:
- purchase
- revenue-critical
```
---
## P1 Core Test Flows
### Test 4: Category Filtering
**File:** `.maestro/flows/core/categoryFilter.yaml`
```yaml
appId: com.tabatafit.app
name: Category Filtering
steps:
- tapOn: "Workouts"
- waitForAnimationToEnd
# Test each category
- tapOn: "Full Body"
- assertVisible: "Full Body"
- tapOn: "Core"
- assertVisible: "Core"
- tapOn: "Cardio"
- assertVisible: "Cardio"
- tapOn: "All"
- assertVisible: "All Workouts"
# Verify filter changes content
- runScript: |
const before = maestro.getElementText('workout-count');
maestro.tapOn('Core');
const after = maestro.getElementText('workout-count');
assert(before !== after, 'Filter should change workout count');
```
### Test 5: Collection Navigation
**File:** `.maestro/flows/core/collections.yaml`
```yaml
appId: com.tabatafit.app
name: Collection Navigation
steps:
- tapOn: "Browse"
- waitForAnimationToEnd
# Scroll to collections
- swipe:
direction: UP
duration: 1000
# Tap first collection
- tapOn:
id: "collection-card-0"
# Collection Detail Screen
- assertVisible: "Collection"
- assertVisible: "workouts"
# Start collection workout
- tapOn: "Start"
- assertVisible: "Player|Timer"
onFlowComplete:
- takeScreenshot: "collection-navigation"
```
### Test 6: Trainer Discovery
**File:** `.maestro/flows/core/trainers.yaml`
```yaml
appId: com.tabatafit.app
name: Trainer Discovery
steps:
- tapOn: "Browse"
# Navigate to trainers section
- swipe:
direction: UP
# Select trainer
- tapOn:
id: "trainer-card-0"
# Verify trainer profile
- assertVisible: "workouts"
# Select trainer's workout
- tapOn:
id: "workout-card-0"
# Should show workout detail
- assertVisible: "Start Workout"
```
---
## Reusable Test Helpers
### Common Actions (`helpers/common.yaml`)
```yaml
# .maestro/helpers/common.yaml
appId: com.tabatafit.app
# Launch app fresh
- launchApp:
clearState: true
# Wait for data loading
- waitForDataLoad:
commands:
- waitForAnimationToEnd:
timeout: 3000
- assertVisible: ".*" # Any content loaded
# Handle permission dialogs
- handlePermissions:
commands:
- tapOn:
text: "Allow"
optional: true
- tapOn:
text: "OK"
optional: true
# Navigate to tab
- navigateToTab:
params:
tabName: ${tab}
commands:
- tapOn: ${tab}
# Start workout from detail
- startWorkout:
commands:
- tapOn: "Start Workout"
- waitForAnimationToEnd:
timeout: 5000
- assertVisible: "Get Ready|WORK"
```
---
## Running Tests
### Local Development
```bash
# Install Maestro
curl -Ls "https://get.maestro.mobile.dev" | bash
# Run single test
maestro test .maestro/flows/critical/onboarding.yaml
# Run all critical tests
maestro test .maestro/flows/critical/
# Run with specific environment
maestro test --env SKIP_PAYWALL=true .maestro/flows/
# Record video of test
maestro record .maestro/flows/critical/workoutComplete.yaml
# Run with tags
maestro test --include-tags=critical .maestro/flows/
```
### CI/CD Integration (GitHub Actions)
```yaml
# .github/workflows/maestro.yml
name: Maestro E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
- name: Install Maestro
run: curl -Ls "https://get.maestro.mobile.dev" | bash
- name: Start iOS Simulator
run: |
xcrun simctl boot "iPhone 15"
sleep 10
- name: Install App
run: |
npm install
npx expo prebuild
npx pod install
npx react-native run-ios --simulator="iPhone 15"
- name: Run Critical Tests
run: |
export MAESTRO_DRIVER_STARTUP_TIMEOUT=120000
maestro test .maestro/flows/critical/
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v3
with:
name: maestro-results
path: |
~/.maestro/tests/
*.png
```
---
## Test Coverage Matrix
| Feature | Test File | Priority | Frequency | Status |
|---------|-----------|----------|-----------|--------|
| Onboarding | `onboarding.yaml` | P0 | Every PR | ⏳ Pending |
| Workout Play | `workoutComplete.yaml` | P0 | Every PR | ⏳ Pending |
| Purchase | `subscription.yaml` | P0 | Every PR | ⏳ Pending |
| Category Filter | `categoryFilter.yaml` | P1 | Pre-release | ⏳ Pending |
| Collections | `collections.yaml` | P1 | Pre-release | ⏳ Pending |
| Trainers | `trainers.yaml` | P1 | Pre-release | ⏳ Pending |
| Activity | `activityHistory.yaml` | P2 | Nightly | ⏳ Pending |
| Offline | `offlineMode.yaml` | P2 | Weekly | ⏳ Pending |
---
## React Native Prerequisites
Before running tests, add `testID` props to components for reliable selectors:
```tsx
// WorkoutCard.tsx
<Pressable testID={`workout-card-${index}`}>
{/* ... */}
</Pressable>
// WorkoutPlayer.tsx
<Button testID="done-button" title="Done" />
// Paywall.tsx
<Pressable testID="yearly-plan">
{/* ... */}
</Pressable>
```
### Required testIDs Checklist
- [ ] `workout-card-{index}` - Workout list items
- [ ] `collection-card-{index}` - Collection items
- [ ] `trainer-card-{index}` - Trainer items
- [ ] `done-button` - Complete workout button
- [ ] `yearly-plan` / `monthly-plan` - Subscription plans
- [ ] `start-workout-button` - Start workout CTA
- [ ] `category-{name}` - Category filter buttons
- [ ] `tab-{name}` - Bottom navigation tabs
---
## Environment Variables
Create `.env.maestro` file:
```bash
# Test Configuration
SKIP_PAYWALL=true
USE_MOCK_PURCHASE=true
TEST_USER_NAME=Test User
TEST_USER_EMAIL=test@example.com
# API Configuration (if needed)
API_BASE_URL=https://api-staging.tabatafit.com
```
---
## Troubleshooting
### Common Issues
1. **Tests fail on first run**
- Clear app state: `maestro test --clear-state`
- Increase timeout in config.yaml
2. **Element not found**
- Verify testID is set correctly
- Add wait times before assertions
- Check for animations completing
3. **Purchase tests fail**
- Ensure `USE_MOCK_PURCHASE=true` in test env
- Use sandbox/test products
4. **Slow tests**
- Use `waitForAnimationToEnd` with shorter timeouts
- Disable animations in test builds
### Debug Commands
```bash
# Interactive mode
maestro studio
# View hierarchy
maestro hierarchy
# Record test execution
maestro record <test-file>
# Verbose logging
maestro test --verbose <test-file>
```
---
## Next Steps (After Features Are Complete)
1. ✅ Create `.maestro/` directory structure
2. ✅ Write `config.yaml`
3. ✅ Implement P0 critical test flows
4. ✅ Add testIDs to React Native components
5. ✅ Run tests locally
6. ✅ Setup CI/CD pipeline
7. ⏳ Implement P1 core test flows
8. ⏳ Add visual regression tests
9. ⏳ Setup nightly regression suite
---
## Resources
- [Maestro Documentation](https://maestro.mobile.dev/)
- [Maestro YAML Reference](https://maestro.mobile.dev/api-reference/commands)
- [React Native Testing with Maestro](https://maestro.mobile.dev/platform-support/react-native)
- [Maestro Best Practices](https://maestro.mobile.dev/advanced/best-practices)
---
**Created:** March 17, 2026
**Status:** Implementation Pending (Waiting for feature completion)

View File

@@ -1,486 +0,0 @@
# TabataFit — UI Feature Brief
> Generated for Google Stitch design handoff.
> Covers all end-user screens, interactions, states, and navigation flows.
---
## App Overview
**TabataFit** is a mobile fitness app ("Apple Fitness+ for Tabata") built with React Native / Expo. It delivers guided Tabata HIIT workouts with video, voice coaching, music, and Apple Watch heart-rate sync.
### Design System
| Token | Value | Usage |
|-------|-------|-------|
| Background | `#000000` | Pure black base |
| Surface | `#1C1C1E` | Cards, sheets |
| Brand | `#FF6B35` | Flame orange — primary accent |
| Rest | `#5AC8FA` | Ice blue — rest phases |
| Success | `#30D158` | Energy green — completion |
| Prep phase | `#FF9500` | Orange-yellow |
| Work phase | `#FF6B35` | Flame orange |
| Rest phase | `#5AC8FA` | Ice blue |
| Complete phase | `#30D158` | Green |
- Supports **dark and light mode**
- Multi-language (i18n)
- Haptic feedback throughout
---
## Navigation Structure
```
Root Stack
├── Onboarding (6-step flow)
├── (tabs)
│ ├── Home — index
│ ├── Explore — browse workouts
│ ├── Activity — stats & history
│ └── Profile — settings & account
├── workout/[id] — Workout detail (push)
├── program/[id] — Program detail (push)
├── collection/[id] — Collection detail (push)
├── player/[id] — Workout player (push, full-screen)
├── complete/[id] — Post-workout celebration (push)
├── paywall — Premium upsell (modal)
├── explore-filters — Filter sheet (form sheet modal)
└── privacy — Privacy policy (push)
```
**Tab bar**: 4 tabs — Home, Explore, Activity, Profile. SF Symbol icons. Badge support.
---
## 1. Onboarding Flow
A 6-screen linear funnel shown on first launch. Progress dots at top. Skip available on some steps.
### 1.1 Problem Screen
- **Purpose**: Motivational hook about time constraints
- **Elements**: Headline text, subtitle, illustration
- **CTA**: Continue
### 1.2 Empathy Screen
- **Purpose**: User selects fitness barriers to build rapport
- **Elements**: Grid of 4 selectable cards — "No time", "Low motivation", "No knowledge", "No gym"
- **Interaction**: Tap to select, max 2 selections, visual highlight on selected
- **CTA**: Continue (enabled after 1+ selection)
### 1.3 Solution Screen
- **Purpose**: Show Tabata's effectiveness
- **Elements**: Animated comparison bar chart (Tabata vs traditional cardio calorie burn)
- **CTA**: Continue
### 1.4 Wow Screen
- **Purpose**: Reveal key app features
- **Elements**: 4 feature cards with staggered entrance animations — Timer, Exercises, Voice Coaching, Progress Tracking
- **CTA**: Continue
### 1.5 Personalization Screen
- **Purpose**: Collect user preferences to personalize experience
- **Inputs**:
- Name (text input)
- Fitness level: Beginner / Intermediate / Advanced (single select chips)
- Goal: Weight Loss / Cardio / Strength / Wellness (single select chips)
- Weekly frequency: 2x / 3x / 5x per week (single select chips)
- **CTA**: Continue (enabled when all fields filled)
### 1.6 Paywall Screen (Onboarding variant)
- **Purpose**: Premium conversion at end of onboarding
- **Elements**: Premium features list, yearly/monthly plan toggle with real prices from RevenueCat
- **CTAs**: Subscribe, Restore Purchases, Skip (close button)
- **States**: Loading prices, purchase in progress, error
---
## 2. Home Tab
Personalized dashboard and primary entry point.
### Elements
- **Greeting header**: Time-based ("Good morning/afternoon/evening") + user's name + animated mascot
- **Streak badge**: Current streak count with flame icon
- **Quick stats row**: 3 stat pills — Current streak, This week (workouts), Total minutes
- **Assessment card**: Feature-flagged (currently OFF) — fitness assessment prompt
- **Program cards**: 3 horizontal cards (Upper Body, Lower Body, Full Body)
- Each shows: icon, title, progress bar (% complete), status badge (Not Started / In Progress / Completed)
- CTA per card: Start / Continue / Restart (depends on state)
- **Switch program button**: Opens program selection
### Navigation
- Tap program card → `program/[id]`
- Tap "Start" on program → `player/[id]` (first workout)
- Tap "Continue" → `player/[id]` (next incomplete workout)
### States
- **New user**: 0 stats, no streak, programs at 0%
- **Returning user**: Populated stats, active streak, program progress
---
## 3. Explore Tab
Browse, search, and filter the full workout catalog.
### Elements
- **Search bar**: Search by workout title, trainer name, exercise name, category. Real-time filtering.
- **Featured collection**: Hero card at top with image, title, workout count. Tap → `collection/[id]`
- **Trainer avatars**: Horizontal scroll of circular trainer photos. Tap to filter workouts by trainer.
- **Collections carousel**: Horizontal scroll of collection cards. Tap → `collection/[id]`
- **Recommended For You**: Horizontal workout card list, personalized based on workout history
- **Featured workouts**: Grid of highlighted workouts
- **All Workouts section**:
- Category filter pills: All, Full Body, Upper Body, Lower Body, Core, Cardio
- Filter button → opens `explore-filters` sheet
- Active filter indicator + clear filters button
- 2-column workout card grid
### Workout Card
- Thumbnail image
- Duration badge overlay
- Title, trainer name, level indicator
- Lock icon if premium-only and user is free tier
### Navigation
- Tap workout card → `workout/[id]`
- Tap collection → `collection/[id]`
- Tap trainer avatar → filters workout list by that trainer
- Tap filter button → `explore-filters` (form sheet modal)
### States
- **Loading**: Skeleton placeholders for cards
- **Error**: Error message with Retry button
- **Empty search**: "No workouts found" message
- **Filtered**: Active filter chips shown, clear all button
---
## 4. Activity Tab
Workout history, statistics, and achievements.
### Elements
- **Streak banner**: Current streak + longest streak (flame icons)
- **Stats grid** (2x2): Each stat in a card with circular progress ring
- Total workouts (ring fills toward goal)
- Total minutes
- Total calories
- Best streak
- **Weekly bar chart**: SunSat, each bar filled if a workout was completed that day, current day highlighted
- **Recent workouts list**: Last 5 workouts
- Each row: workout title, relative time ("2h ago"), duration, calories
- Tap → `workout/[id]`
- **Achievements grid**: 4 achievement badges displayed
- Types: workouts milestone, streak milestone, minutes milestone, calories milestone
- States: locked (greyed out) / unlocked (colored with checkmark)
### States
- **Empty**: No workouts yet — motivational message + "Start Your First Workout" CTA → `explore`
- **Populated**: All sections visible with data
---
## 5. Profile Tab
User settings, account management, and app info.
### Elements
- **User header**: Avatar circle with initial, display name, plan label ("Free" or "TabataFit+")
- **Stats row**: 3 inline stats — workouts count, streak, calories
- **Upgrade CTA** (free users only): Gradient button → `paywall`
#### Workout Settings Section
- Haptic feedback toggle
- Sound effects toggle
- Voice coaching toggle
#### Notifications Section
- Daily reminders toggle
- Reminder time display (when enabled)
#### Personalization Section (premium only)
- Sync status indicator
#### About Section
- Version number
- Rate App → opens App Store rating prompt
- Contact Us → opens email compose
- FAQ → opens external web link
- Privacy Policy → `privacy` screen
#### Account Section (premium only)
- Restore Purchases → triggers RevenueCat restore
#### Danger Zone
- Sign Out button
- Data deletion: triggers confirmation modal
### Data Deletion Modal
- Warning text explaining data loss
- Cancel / Delete buttons
- Delete is destructive (red)
---
## 6. Workout Detail Screen
Pre-workout information screen. Reached by tapping any workout card.
**Route**: `workout/[id]`
### Elements
- **Header**: Thumbnail or video preview, back button, heart/save button (toggle)
- **Title**: Workout name
- **Trainer**: Trainer name (colored text)
- **Metadata row**: Duration (minutes), Calories estimate, Level badge (Beginner/Intermediate/Advanced)
- **Equipment list**: Icons + labels for required equipment (or "No equipment")
- **Timing card**: Prep time, Work time, Rest time, Rounds — displayed in a structured card
- **Exercise list**: Ordered list of exercises with individual durations
- **Repeat rounds indicator**: Shows if rounds repeat the exercise sequence
- **Music vibe label**: Genre/mood of the workout soundtrack
### CTAs
- **Start Workout** → `player/[id]` (if unlocked or user is premium)
- **Unlock with TabataFit+** → `paywall` (if locked and user is free tier)
### Header Actions
- **Back**: Navigate back
- **Save/Unsave**: Heart icon toggle — saves workout to favorites
### States
- **Loading**: Skeleton layout
- **Unlocked**: Full detail visible, "Start Workout" CTA
- **Locked**: Full detail visible but "Unlock with TabataFit+" CTA replaces start button
---
## 7. Program Detail Screen
Multi-week training program overview with per-week workout breakdown.
**Route**: `program/[id]`
### Elements
- **Header**: Program icon, title, subtitle (e.g., "4 weeks · 12 workouts")
- **Description**: Program summary text
- **Stats card**: 3 stats — Weeks, Workouts, Total Minutes
- **Tags**: Equipment required (e.g., Dumbbells, Mat) + Equipment optional + Focus areas (e.g., Arms, Core)
- **Progress bar** (if started): Percentage complete with label
- **Training plan**: Expandable week sections
- Each week shows its workouts in order
- Workout rows show: title, duration, completion checkmark (if done)
- Current week has a "Current" badge
- Future weeks may show lock icons (progressive unlock)
### CTAs
- **Start Program** (not started) → `player/[id]` (first workout)
- **Continue Training** (in progress) → `player/[id]` (next incomplete workout)
- **Restart** (completed) → resets progress, starts from week 1
### States
- **Not Started**: 0% progress, all weeks shown, "Start Program" CTA
- **In Progress**: Progress bar filled, completed workouts checked, "Continue Training" CTA
- **Completed**: 100% progress, all checked, "Restart" CTA
---
## 8. Collection Detail Screen
A curated group of workouts.
**Route**: `collection/[id]`
### Elements
- **Header**: Collection title, description, workout count
- **Workout list**: Vertical list of workout cards in the collection
- Each card: thumbnail, title, trainer, duration, level
- Lock icon for premium-gated workouts
### Navigation
- Tap workout → `workout/[id]`
- Back button → previous screen
---
## 9. Player Screen
Full-screen workout execution with timer, video, audio, and Watch sync.
**Route**: `player/[id]`
### Layout
- **Full-screen dark mode** — no tab bar, no status bar chrome
- **Background**: Workout video (HLS streaming) or gradient fallback
- **Phase-colored tint**: Background overlay changes color per phase (prep=orange, work=flame, rest=blue, complete=green)
### Timer Section
- **Timer ring**: Large circular progress indicator, fills as phase progresses
- **Phase label**: PREP / WORK / REST / COMPLETE (color-coded)
- **Countdown**: Large MM:SS timer (uses tabular-nums for alignment)
- **Round indicator**: "Round 3 of 8" text
### Exercise Info
- **Current exercise name**: Large text
- **Next exercise preview**: Smaller text ("Up next: Burpees")
- **Coach encouragement**: Motivational text overlays (e.g., "Keep going!", "Almost there!")
### Controls
- **Start**: Begins the workout (shown before first start)
- **Pause / Resume**: Toggle button during workout
- **Stop**: Ends workout early (confirmation prompt)
- **Skip**: Skip to next phase
### Stats Overlay
- **Calories**: Running calorie count
- **Heart rate**: BPM from Apple Watch (if connected)
- **Rounds**: Current / total
### Burn Bar
- Horizontal bar comparing user's current calorie burn vs. average for this workout
- Updates in real-time
### Now Playing Pill
- Shows current music track name
- Skip track button
### Audio & Haptics
- **Sound effects**: Phase start chime, 3-2-1 countdown beeps, workout complete fanfare
- **Haptic feedback**: Phase transitions, countdown ticks, button presses
- **Voice coaching**: Audio cues for exercises and encouragement
- **Screen stays awake** (useKeepAwake)
### Apple Watch Integration
- Sends: workout state (phase, timer, exercise)
- Receives: play/pause, skip, stop commands, heart rate data
### Completion State
- Timer ring shows 100%
- Phase label: COMPLETE
- Summary: Rounds completed, calories burned, total minutes
- **Done** CTA → `complete/[id]`
### States
- **Ready**: Before starting — shows workout info, Start CTA
- **Active**: Timer running, video playing, stats updating
- **Paused**: Timer frozen, controls show Resume
- **Complete**: Summary shown, Done CTA
---
## 10. Workout Complete Screen
Post-workout celebration and next steps.
**Route**: `complete/[id]`
### Elements
- **Celebration animation**: Concentric emoji rings spinning (fire, muscle, lightning emojis)
- **Stats grid**: 3 stats — Calories, Minutes, 100% completion
- **Burn bar result**: Percentile comparison ("You burned more than 73% of users")
- **Streak display**: Current streak count + subtitle ("Keep it going!")
- **Share button**: Opens native share sheet with workout summary
- **Recommended next workouts**: 3 horizontal workout cards
- Tap → `workout/[id]`
- **Back to Home** CTA → navigates to Home tab
### Sync Consent Modal
- Appears after first workout for premium users
- Prompts to enable cross-device data sync
- Accept / Decline buttons
---
## 11. Paywall Screen
Premium subscription purchase flow.
**Route**: `paywall` (presented as modal)
### Elements
- **Header**: "TabataFit+" branding
- **Features grid**: 6 premium feature cards with icons
- Music during workouts
- Unlimited workouts
- Detailed stats
- Calorie tracking
- Smart reminders
- No ads
- **Plan selection**: Two radio-style options
- Yearly: Price/year + "Save 50%" badge (highlighted as best value)
- Monthly: Price/month
- Prices fetched live from RevenueCat
- **Subscribe CTA**: Gradient button, shows selected plan price
- **Restore purchases**: Text link below CTA
- **Terms**: Privacy policy + terms of service links
- **Close button**: X in top corner to dismiss
### States
- **Loading**: Skeleton while fetching prices from RevenueCat
- **Ready**: Plans displayed with real prices
- **Purchasing**: Loading spinner on CTA, inputs disabled
- **Error**: Error message with retry
- **Success**: Dismisses modal, unlocks premium features
---
## 12. Explore Filters Sheet
Filter modal for the Explore tab workout grid.
**Route**: `explore-filters` (form sheet modal with grabber)
### Elements
- **Level filter chips**: All / Beginner / Intermediate / Advanced (single select)
- **Equipment filter chips**: All / None / Dumbbells / Band / Mat (dynamic from data, single select)
- **Apply**: Dismiss sheet, filters persist in shared store
- **Clear**: Reset all filters to "All"
---
## 13. Privacy Policy Screen
Static content screen.
**Route**: `privacy`
### Elements
- Privacy policy text content
- Back navigation
---
## Cross-Cutting Features
### Premium Gating
- Free users see all workouts but some are locked (lock icon overlay)
- Tapping a locked workout's "Start" CTA redirects to `paywall`
- Premium users have full access to all workouts, stats sync, and personalization
### Internationalization (i18n)
- All user-facing strings are translated via i18n system
- Multi-language support throughout
### Haptic Feedback
- Configurable via Profile settings toggle
- Triggered on: button presses, phase changes, countdown ticks, achievements
### Analytics (PostHog)
- Events tracked across all screens: screen views, button taps, workout starts/completions, purchases, onboarding steps
### Dark / Light Mode
- Full theme support — colors adapt to system appearance
- Player screen is always dark mode regardless of system setting
### Loading & Error States
- Skeleton placeholders during data fetches
- Error states with descriptive message + Retry button
- Empty states with motivational messaging + CTAs
### Animations
- Onboarding: staggered card reveals, animated charts
- Home: mascot animation
- Player: timer ring fill, phase color transitions
- Complete: spinning emoji celebration rings
- Navigation: standard iOS push/pop + modal presentations

View File

@@ -1,3 +1,6 @@
-- ⚠️ OBSOLÈTE — Ce fichier décrit l'ancien concept "TabataFit" (abandonné).
-- Le schéma actif est dans supabase/migrations/001 → 006.
-- Voir AGENTS.md pour la référence canonique.
-- ============================================================
-- TabataFit Supabase Schema
-- ============================================================

View File

@@ -1,3 +1,5 @@
-- ⚠️ OBSOLÈTE — Ce fichier date du concept "TabataFit" (abandonné).
-- Le vrai seed est dans supabase/migrations/006_seed_workout_programs.sql.
-- ============================================================
-- TabataFit Seed Data
-- ============================================================

View File

@@ -1,3 +1,5 @@
-- ⚠️ PARTIELLEMENT OBSOLÈTE — Référence TabataFit.
-- La fonction is_admin() reste valide pour admin_users.
-- ============================================================
-- Setup: Initial Admin User and Authentication
-- ============================================================

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -1,96 +0,0 @@
{
"originHash" : "42d1f35b4500c2779457daf99841f2333a14d9a2965835305d89fd95beda836e",
"pins" : [
{
"identity" : "plcrashreporter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/plcrashreporter.git",
"state" : {
"revision" : "0254f941c646b1ed17b243654723d0f071e990d0",
"version" : "1.12.2"
}
},
{
"identity" : "posthog-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/PostHog/posthog-ios",
"state" : {
"revision" : "c3efdae383a5e7a5a88c34fd774e9d7dc915b9d4",
"version" : "3.55.0"
}
},
{
"identity" : "purchases-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/RevenueCat/purchases-ios",
"state" : {
"revision" : "bd63241b2258ea519020eb32a349db44fb44b119",
"version" : "5.68.0"
}
},
{
"identity" : "supabase-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/supabase/supabase-swift",
"state" : {
"revision" : "17261e93c60aa721e3c17312bfeb2ae6de3d6f8a",
"version" : "2.43.1"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "eb50cbd14606a9161cbc5d452f18797c90ef0bab",
"version" : "1.7.0"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
"version" : "1.0.6"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
"version" : "1.3.2"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "476538ccb827f2dd18efc5de754cc87d77127a47",
"version" : "4.4.0"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types.git",
"state" : {
"revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca",
"version" : "1.5.1"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "dfd70507def84cb5fb821278448a262c6ff2bbad",
"version" : "1.9.0"
}
}
],
"version" : 3
}

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "92991789C3A5B2A5FACF07A1"
BuildableName = "TabataGo.app"
BlueprintName = "TabataGo"
ReferencedContainer = "container:TabataGo.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnableBacktraceRecording = "Yes">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "92991789C3A5B2A5FACF07A1"
BuildableName = "TabataGo.app"
BlueprintName = "TabataGo"
ReferencedContainer = "container:TabataGo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "92991789C3A5B2A5FACF07A1"
BuildableName = "TabataGo.app"
BlueprintName = "TabataGo"
ReferencedContainer = "container:TabataGo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D1E2CCCC7C9BD41029959883"
BuildableName = "TabataGoTests.xctest"
BlueprintName = "TabataGoTests"
ReferencedContainer = "container:TabataGo.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnableBacktraceRecording = "Yes">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,62 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>TabataGo.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>TabataGoTests.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>TabataGoUITests.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>TabataGoWatch.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>TabataGoWatchWidget.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>TabataGoWidget.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>3945C3998B4B66F30759718C</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>90BAF2DB5D7456CD45975E26</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>92991789C3A5B2A5FACF07A1</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>D1E2CCCC7C9BD41029959883</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

View File

@@ -1,30 +1,38 @@
import Foundation
#if canImport(PostHog)
import PostHog
#endif
/// PostHog analytics mirrors the event taxonomy from the Expo app.
final class AnalyticsService: @unchecked Sendable {
static let shared = AnalyticsService()
#if canImport(PostHog)
private let apiKey: String =
Bundle.main.infoDictionary?["POSTHOG_API_KEY"] as? String ?? ""
private let host = "https://eu.posthog.com"
#endif
private init() {}
func initialize() {
#if canImport(PostHog)
guard !apiKey.isEmpty else { return }
let config = PostHogConfig(apiKey: apiKey, host: host)
config.captureApplicationLifecycleEvents = true
config.captureScreenViews = false // manual tracking
PostHogSDK.shared.setup(config)
#endif
}
// Screens
func screen(_ name: String, properties: [String: Any] = [:]) {
#if canImport(PostHog)
PostHogSDK.shared.screen(name, properties: properties)
#endif
}
// Onboarding
@@ -100,6 +108,8 @@ final class AnalyticsService: @unchecked Sendable {
// Private
private func capture(_ event: String, properties: [String: Any] = [:]) {
#if canImport(PostHog)
PostHogSDK.shared.capture(event, properties: properties.isEmpty ? nil : properties)
#endif
}
}

View File

@@ -13,7 +13,7 @@ struct TabataEntry: TimelineEntry {
struct TabataProvider: TimelineProvider {
private let sharedDefaults = UserDefaults(suiteName: "group.com.tabatago.app")
private let sharedDefaults = UserDefaults(suiteName: "group.fr.millianlmx.tabatago")
func placeholder(in context: Context) -> TabataEntry {
TabataEntry(date: Date(), streak: 7, lastWorkoutLabel: "Today")
@@ -98,6 +98,8 @@ struct RectangularComplicationView: View {
}
/// `.accessoryCorner` tiny bolt + streak digit in the corner
#if canImport(WatchKit)
@available(watchOS 10.0, *)
struct CornerComplicationView: View {
let entry: TabataEntry
@@ -111,6 +113,7 @@ struct CornerComplicationView: View {
.widgetLabel(String(format: String(localized: LocalizedStringResource("watch.complication.dayStreakFmt")), entry.streak))
}
}
#endif
// Widget definition
@@ -124,11 +127,11 @@ struct TabataGoComplication: Widget {
}
.configurationDisplayName("TabataGo")
.description(String(localized: LocalizedStringResource("watch.complication.description")))
.supportedFamilies([
.accessoryCircular,
.accessoryRectangular,
.accessoryCorner
])
#if canImport(WatchKit)
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryCorner])
#else
.supportedFamilies([.accessoryCircular, .accessoryRectangular])
#endif
}
}
@@ -142,8 +145,10 @@ struct TabataComplicationEntryView: View {
CircularComplicationView(entry: entry)
case .accessoryRectangular:
RectangularComplicationView(entry: entry)
#if canImport(WatchKit)
case .accessoryCorner:
CornerComplicationView(entry: entry)
#endif
default:
CircularComplicationView(entry: entry)
}
@@ -163,3 +168,12 @@ struct TabataComplicationEntryView: View {
} timeline: {
TabataEntry(date: .now, streak: 7, lastWorkoutLabel: "Today")
}
#if canImport(WatchKit)
@available(watchOS 10.0, *)
#Preview("Corner", as: .accessoryCorner) {
TabataGoComplication()
} timeline: {
TabataEntry(date: .now, streak: 7, lastWorkoutLabel: "Today")
}
#endif

View File

@@ -27,6 +27,6 @@
<key>WKApplication</key>
<true/>
<key>WKCompanionAppBundleIdentifier</key>
<string>com.tabatago.app</string>
<string>fr.millianlmx.tabatago</string>
</dict>
</plist>

View File

@@ -6,7 +6,7 @@
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.tabatago.app</string>
<string>group.fr.millianlmx.tabatago</string>
</array>
</dict>
</plist>

View File

@@ -13,7 +13,7 @@ struct WatchActivityView: View {
@State private var isLoading = true
private let healthStore = HKHealthStore()
private let sharedDefaults = UserDefaults(suiteName: "group.com.tabatago.app")
private let sharedDefaults = UserDefaults(suiteName: "group.fr.millianlmx.tabatago")
var body: some View {
ScrollView {

View File

@@ -1,7 +1,7 @@
name: TabataGo
options:
bundleIdPrefix: com.tabatago
bundleIdPrefix: fr.millianlmx.tabatago
deploymentTarget:
iOS: "26.0"
watchOS: "11.0"
@@ -15,7 +15,11 @@ settings:
SWIFT_VERSION: "6.0"
IPHONEOS_DEPLOYMENT_TARGET: "26.0"
SWIFT_STRICT_CONCURRENCY: complete
SWIFT_EMIT_MODULE_FOR_EXPLICIT_BUILD: NO
GCC_TREAT_WARNINGS_AS_ERRORS: NO
SWIFT_TREAT_WARNINGS_AS_ERRORS: NO
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
DEVELOPMENT_TEAM: "2MJF39L8VY"
packages:
Supabase:
@@ -24,9 +28,6 @@ packages:
RevenueCat:
url: https://github.com/RevenueCat/purchases-ios
from: "5.0.0"
PostHog:
url: https://github.com/PostHog/posthog-ios
from: "3.0.0"
targets:
TabataGo:
@@ -66,19 +67,18 @@ targets:
com.apple.developer.healthkit.access:
- health-records
com.apple.security.application-groups:
- group.com.tabatago.app
- group.fr.millianlmx.tabatago
dependencies:
- package: Supabase
product: Supabase
- package: RevenueCat
product: RevenueCat
- package: PostHog
product: PostHog
- target: TabataGoWatch
embed: true
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago
INFOPLIST_FILE: TabataGo/Resources/Info.plist
CODE_SIGN_ENTITLEMENTS: TabataGo/Resources/TabataGo.entitlements
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
@@ -112,7 +112,7 @@ targets:
CFBundleShortVersionString: "1.0"
CFBundleVersion: "2"
WKApplication: true
WKCompanionAppBundleIdentifier: com.tabatago.app
WKCompanionAppBundleIdentifier: fr.millianlmx.tabatago
NSHealthShareUsageDescription: "TabataGo reads your heart rate and calories during workouts."
NSHealthUpdateUsageDescription: "TabataGo saves workout data to Apple Health directly from your Watch."
entitlements:
@@ -120,13 +120,13 @@ targets:
properties:
com.apple.developer.healthkit: true
com.apple.security.application-groups:
- group.com.tabatago.app
- group.fr.millianlmx.tabatago
dependencies:
- target: TabataGoWatchWidget
embed: true
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.watchkitapp
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.watchkitapp
INFOPLIST_FILE: TabataGoWatch/Resources/Info.plist
CODE_SIGN_ENTITLEMENTS: TabataGoWatch/Resources/TabataGoWatch.entitlements
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
@@ -152,7 +152,7 @@ targets:
NSExtensionPointIdentifier: com.apple.widgetkit-extension
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.watchkitapp.widget
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.watchkitapp.widget
TARGETED_DEVICE_FAMILY: "4"
WATCHOS_DEPLOYMENT_TARGET: "11.0"
@@ -166,7 +166,7 @@ targets:
- target: TabataGo
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.tests
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.tests
TabataGoUITests:
type: bundle.ui-testing
@@ -178,4 +178,14 @@ targets:
- target: TabataGo
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.uitests
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.uitests
schemes:
TabataGo:
build:
targets:
TabataGo: all
run:
config: Debug
archive:
config: Release