From 9554f9c04afab3f6a41f6eb98bbf3ef0fbdf3c85 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Thu, 23 Jul 2026 13:00:00 -0700 Subject: [PATCH] RU-T51 Checkin fixes, status fix --- .DS_Store | Bin 10244 -> 12292 bytes .../react-native-best-practices/POWER.md | 0 .../react-native-best-practices/SKILL.md | 0 .../agents/openai.yaml | 0 .../references/bundle-analyze-app.md | 0 .../references/bundle-analyze-js.md | 0 .../references/bundle-barrel-exports.md | 0 .../references/bundle-code-splitting.md | 0 .../references/bundle-hermes-mmap.md | 0 .../references/bundle-library-size.md | 0 .../references/bundle-native-assets.md | 0 .../references/bundle-r8-android.md | 0 .../references/bundle-tree-shaking.md | 0 .../bundle-treemap-source-map-explorer.png | Bin .../images/controlled-textinput-pingpong.png | Bin .../references/images/devtools-flamegraph.png | Bin .../references/images/emerge-xray-ios.png | Bin .../references/images/expo-atlas-treemap.png | Bin .../flashlight-flatlist-vs-flashlist.png | Bin .../references/images/fps-drop-graph.png | Bin .../images/memory-heap-snapshot.png | Bin .../images/tti-warm-start-diagram.png | Bin .../images/view-hierarchy-flattening.png | Bin .../images/xcode-instruments-templates.png | Bin .../references/images/xcode-thread-view.png | Bin .../references/js-animations-reanimated.md | 0 .../references/js-atomic-state.md | 0 .../references/js-bottomsheet.md | 0 .../references/js-concurrent-react.md | 0 .../references/js-lists-flatlist-flashlist.md | 0 .../references/js-measure-fps.md | 0 .../references/js-memory-leaks.md | 0 .../references/js-profile-react.md | 0 .../references/js-react-compiler.md | 0 .../references/js-uncontrolled-components.md | 0 .../native-android-16kb-alignment.md | 0 .../references/native-measure-tti.md | 0 .../references/native-memory-leaks.md | 0 .../references/native-memory-patterns.md | 0 .../references/native-platform-setup.md | 0 .../references/native-profiling.md | 0 .../references/native-sdks-over-polyfills.md | 0 .../references/native-threading-model.md | 0 .../references/native-turbo-modules.md | 0 .../references/native-view-flattening.md | 0 .../skills/upgrading-react-native/SKILL.md | 0 .../upgrading-react-native/agents/openai.yaml | 0 .../references/expo-sdk-upgrade.md | 0 .../monorepo-singlerepo-targeting.md | 0 .../references/react.md | 0 .../references/upgrade-helper-core.md | 0 .../references/upgrade-verification.md | 0 .../references/upgrading-dependencies.md | 0 .../references/upgrading-react-native.md | 0 .../react-native-best-practices/POWER.md | 161 ++++++++ .../react-native-best-practices/SKILL.md | 253 ++++++++++++ .../agents/openai.yaml | 4 + .../references/bundle-analyze-app.md | 211 ++++++++++ .../references/bundle-analyze-js.md | 262 +++++++++++++ .../references/bundle-barrel-exports.md | 248 ++++++++++++ .../references/bundle-code-splitting.md | 247 ++++++++++++ .../references/bundle-hermes-mmap.md | 167 ++++++++ .../references/bundle-library-size.md | 177 +++++++++ .../references/bundle-native-assets.md | 214 ++++++++++ .../references/bundle-r8-android.md | 225 +++++++++++ .../references/bundle-tree-shaking.md | 214 ++++++++++ .../bundle-treemap-source-map-explorer.png | Bin 0 -> 667493 bytes .../images/controlled-textinput-pingpong.png | Bin 0 -> 127223 bytes .../references/images/devtools-flamegraph.png | Bin 0 -> 520655 bytes .../references/images/emerge-xray-ios.png | Bin 0 -> 439669 bytes .../references/images/expo-atlas-treemap.png | Bin 0 -> 618904 bytes .../flashlight-flatlist-vs-flashlist.png | Bin 0 -> 313215 bytes .../references/images/fps-drop-graph.png | Bin 0 -> 152493 bytes .../images/memory-heap-snapshot.png | Bin 0 -> 308032 bytes .../images/tti-warm-start-diagram.png | Bin 0 -> 127254 bytes .../images/view-hierarchy-flattening.png | Bin 0 -> 2587021 bytes .../images/xcode-instruments-templates.png | Bin 0 -> 408360 bytes .../references/images/xcode-thread-view.png | Bin 0 -> 178440 bytes .../references/js-animations-reanimated.md | 255 ++++++++++++ .../references/js-atomic-state.md | 246 ++++++++++++ .../references/js-bottomsheet.md | 325 ++++++++++++++++ .../references/js-concurrent-react.md | 236 +++++++++++ .../references/js-lists-flatlist-flashlist.md | 244 ++++++++++++ .../references/js-measure-fps.md | 178 +++++++++ .../references/js-memory-leaks.md | 205 ++++++++++ .../references/js-profile-react.md | 174 +++++++++ .../references/js-react-compiler.md | 368 ++++++++++++++++++ .../references/js-uncontrolled-components.md | 216 ++++++++++ .../native-android-16kb-alignment.md | 113 ++++++ .../references/native-measure-tti.md | 262 +++++++++++++ .../references/native-memory-leaks.md | 240 ++++++++++++ .../references/native-memory-patterns.md | 274 +++++++++++++ .../references/native-platform-setup.md | 110 ++++++ .../references/native-profiling.md | 176 +++++++++ .../references/native-sdks-over-polyfills.md | 192 +++++++++ .../references/native-threading-model.md | 234 +++++++++++ .../references/native-turbo-modules.md | 292 ++++++++++++++ .../references/native-view-flattening.md | 201 ++++++++++ .../skills/upgrading-react-native/SKILL.md | 82 ++++ .../upgrading-react-native/agents/openai.yaml | 4 + .../references/expo-sdk-upgrade.md | 65 ++++ .../monorepo-singlerepo-targeting.md | 34 ++ .../references/react.md | 42 ++ .../references/upgrade-helper-core.md | 122 ++++++ .../references/upgrade-verification.md | 43 ++ .../references/upgrading-dependencies.md | 41 ++ .../references/upgrading-react-native.md | 52 +++ .gitignore | 1 + src/app/(app)/__tests__/routes.test.tsx | 28 ++ src/app/(app)/index.tsx | 15 +- src/app/(app)/routes.tsx | 8 +- src/app/_layout.tsx | 5 + src/app/call/[id].tsx | 15 +- src/app/routes/poi/[id].tsx | 2 - src/app/weather-alert/[id].tsx | 9 +- .../__tests__/check-in-bottom-sheet.test.tsx | 15 +- .../__tests__/check-in-tab-content.test.tsx | 109 ++++++ .../__tests__/check-in-timer-card.test.tsx | 35 +- .../check-in-timers/check-in-bottom-sheet.tsx | 21 +- .../check-in-timers/check-in-tab-content.tsx | 34 +- .../check-in-timers/check-in-timer-card.tsx | 22 +- src/components/sidebar/sidebar-content.tsx | 2 - .../__tests__/status-bottom-sheet.test.tsx | 39 ++ src/components/status/status-bottom-sheet.tsx | 21 +- .../ui/__tests__/actionsheet.test.tsx | 139 +++++++ .../ui/__tests__/shared-tabs.test.tsx | 79 ++++ src/components/ui/actionsheet/index.tsx | 29 +- src/components/ui/shared-tabs.tsx | 187 +++++++-- .../__tests__/weather-alert-card.test.tsx | 8 + .../weather-alert-detail-map.test.tsx | 124 ++++++ .../weather-alerts/weather-alert-card.tsx | 8 +- .../weather-alert-detail-map.tsx | 83 ++-- .../__tests__/use-quick-check-in.test.ts | 23 +- .../use-weather-alert-banner.test.ts | 48 +++ src/hooks/use-quick-check-in.ts | 15 +- src/hooks/use-weather-alert-banner.ts | 34 ++ .../__tests__/check-in-eligibility.test.ts | 40 ++ .../__tests__/check-in-timer-utils.test.ts | 85 ++++ src/lib/__tests__/weather-alert-utils.test.ts | 83 ++++ src/lib/check-in-eligibility.ts | 58 +++ src/lib/check-in-timer-utils.ts | 93 +++++ src/lib/weather-alert-utils.ts | 119 +++++- .../checkIn/resolvedCheckInTimerResultData.ts | 2 +- .../check-in-timers/__tests__/store.test.ts | 24 +- src/stores/check-in-timers/store.ts | 16 +- src/stores/status/__tests__/store.test.ts | 45 +++ src/stores/status/store.ts | 23 +- src/translations/ar.json | 1 + src/translations/de.json | 1 + src/translations/en.json | 1 + src/translations/es.json | 1 + src/translations/fr.json | 1 + src/translations/it.json | 1 + src/translations/pl.json | 1 + src/translations/sv.json | 1 + src/translations/uk.json | 1 + 156 files changed, 8982 insertions(+), 182 deletions(-) rename {.forge => .agent}/skills/react-native-best-practices/POWER.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/SKILL.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/agents/openai.yaml (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-analyze-app.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-analyze-js.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-barrel-exports.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-code-splitting.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-hermes-mmap.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-library-size.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-native-assets.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-r8-android.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/bundle-tree-shaking.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/bundle-treemap-source-map-explorer.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/controlled-textinput-pingpong.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/devtools-flamegraph.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/emerge-xray-ios.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/expo-atlas-treemap.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/flashlight-flatlist-vs-flashlist.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/fps-drop-graph.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/memory-heap-snapshot.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/tti-warm-start-diagram.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/view-hierarchy-flattening.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/xcode-instruments-templates.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/images/xcode-thread-view.png (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-animations-reanimated.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-atomic-state.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-bottomsheet.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-concurrent-react.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-lists-flatlist-flashlist.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-measure-fps.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-memory-leaks.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-profile-react.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-react-compiler.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/js-uncontrolled-components.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-android-16kb-alignment.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-measure-tti.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-memory-leaks.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-memory-patterns.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-platform-setup.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-profiling.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-sdks-over-polyfills.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-threading-model.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-turbo-modules.md (100%) rename {.forge => .agent}/skills/react-native-best-practices/references/native-view-flattening.md (100%) rename {.forge => .agent}/skills/upgrading-react-native/SKILL.md (100%) rename {.forge => .agent}/skills/upgrading-react-native/agents/openai.yaml (100%) rename {.forge => .agent}/skills/upgrading-react-native/references/expo-sdk-upgrade.md (100%) rename {.forge => .agent}/skills/upgrading-react-native/references/monorepo-singlerepo-targeting.md (100%) rename {.forge => .agent}/skills/upgrading-react-native/references/react.md (100%) rename {.forge => .agent}/skills/upgrading-react-native/references/upgrade-helper-core.md (100%) rename {.forge => .agent}/skills/upgrading-react-native/references/upgrade-verification.md (100%) rename {.forge => .agent}/skills/upgrading-react-native/references/upgrading-dependencies.md (100%) rename {.forge => .agent}/skills/upgrading-react-native/references/upgrading-react-native.md (100%) create mode 100644 .claude/skills/react-native-best-practices/POWER.md create mode 100644 .claude/skills/react-native-best-practices/SKILL.md create mode 100644 .claude/skills/react-native-best-practices/agents/openai.yaml create mode 100644 .claude/skills/react-native-best-practices/references/bundle-analyze-app.md create mode 100644 .claude/skills/react-native-best-practices/references/bundle-analyze-js.md create mode 100644 .claude/skills/react-native-best-practices/references/bundle-barrel-exports.md create mode 100644 .claude/skills/react-native-best-practices/references/bundle-code-splitting.md create mode 100644 .claude/skills/react-native-best-practices/references/bundle-hermes-mmap.md create mode 100644 .claude/skills/react-native-best-practices/references/bundle-library-size.md create mode 100644 .claude/skills/react-native-best-practices/references/bundle-native-assets.md create mode 100644 .claude/skills/react-native-best-practices/references/bundle-r8-android.md create mode 100644 .claude/skills/react-native-best-practices/references/bundle-tree-shaking.md create mode 100644 .claude/skills/react-native-best-practices/references/images/bundle-treemap-source-map-explorer.png create mode 100644 .claude/skills/react-native-best-practices/references/images/controlled-textinput-pingpong.png create mode 100644 .claude/skills/react-native-best-practices/references/images/devtools-flamegraph.png create mode 100644 .claude/skills/react-native-best-practices/references/images/emerge-xray-ios.png create mode 100644 .claude/skills/react-native-best-practices/references/images/expo-atlas-treemap.png create mode 100644 .claude/skills/react-native-best-practices/references/images/flashlight-flatlist-vs-flashlist.png create mode 100644 .claude/skills/react-native-best-practices/references/images/fps-drop-graph.png create mode 100644 .claude/skills/react-native-best-practices/references/images/memory-heap-snapshot.png create mode 100644 .claude/skills/react-native-best-practices/references/images/tti-warm-start-diagram.png create mode 100644 .claude/skills/react-native-best-practices/references/images/view-hierarchy-flattening.png create mode 100644 .claude/skills/react-native-best-practices/references/images/xcode-instruments-templates.png create mode 100644 .claude/skills/react-native-best-practices/references/images/xcode-thread-view.png create mode 100644 .claude/skills/react-native-best-practices/references/js-animations-reanimated.md create mode 100644 .claude/skills/react-native-best-practices/references/js-atomic-state.md create mode 100644 .claude/skills/react-native-best-practices/references/js-bottomsheet.md create mode 100644 .claude/skills/react-native-best-practices/references/js-concurrent-react.md create mode 100644 .claude/skills/react-native-best-practices/references/js-lists-flatlist-flashlist.md create mode 100644 .claude/skills/react-native-best-practices/references/js-measure-fps.md create mode 100644 .claude/skills/react-native-best-practices/references/js-memory-leaks.md create mode 100644 .claude/skills/react-native-best-practices/references/js-profile-react.md create mode 100644 .claude/skills/react-native-best-practices/references/js-react-compiler.md create mode 100644 .claude/skills/react-native-best-practices/references/js-uncontrolled-components.md create mode 100644 .claude/skills/react-native-best-practices/references/native-android-16kb-alignment.md create mode 100644 .claude/skills/react-native-best-practices/references/native-measure-tti.md create mode 100644 .claude/skills/react-native-best-practices/references/native-memory-leaks.md create mode 100644 .claude/skills/react-native-best-practices/references/native-memory-patterns.md create mode 100644 .claude/skills/react-native-best-practices/references/native-platform-setup.md create mode 100644 .claude/skills/react-native-best-practices/references/native-profiling.md create mode 100644 .claude/skills/react-native-best-practices/references/native-sdks-over-polyfills.md create mode 100644 .claude/skills/react-native-best-practices/references/native-threading-model.md create mode 100644 .claude/skills/react-native-best-practices/references/native-turbo-modules.md create mode 100644 .claude/skills/react-native-best-practices/references/native-view-flattening.md create mode 100644 .claude/skills/upgrading-react-native/SKILL.md create mode 100644 .claude/skills/upgrading-react-native/agents/openai.yaml create mode 100644 .claude/skills/upgrading-react-native/references/expo-sdk-upgrade.md create mode 100644 .claude/skills/upgrading-react-native/references/monorepo-singlerepo-targeting.md create mode 100644 .claude/skills/upgrading-react-native/references/react.md create mode 100644 .claude/skills/upgrading-react-native/references/upgrade-helper-core.md create mode 100644 .claude/skills/upgrading-react-native/references/upgrade-verification.md create mode 100644 .claude/skills/upgrading-react-native/references/upgrading-dependencies.md create mode 100644 .claude/skills/upgrading-react-native/references/upgrading-react-native.md create mode 100644 src/app/(app)/__tests__/routes.test.tsx create mode 100644 src/components/check-in-timers/__tests__/check-in-tab-content.test.tsx create mode 100644 src/components/ui/__tests__/actionsheet.test.tsx create mode 100644 src/components/ui/__tests__/shared-tabs.test.tsx create mode 100644 src/components/weather-alerts/__tests__/weather-alert-detail-map.test.tsx create mode 100644 src/hooks/__tests__/use-weather-alert-banner.test.ts create mode 100644 src/hooks/use-weather-alert-banner.ts create mode 100644 src/lib/__tests__/check-in-eligibility.test.ts create mode 100644 src/lib/__tests__/check-in-timer-utils.test.ts create mode 100644 src/lib/__tests__/weather-alert-utils.test.ts create mode 100644 src/lib/check-in-eligibility.ts create mode 100644 src/lib/check-in-timer-utils.ts diff --git a/.DS_Store b/.DS_Store index 5df6e1de06fe62ce33fe1acd891450a11f410a75..56be171ba8515ffc38c16fccfae410b6942b839d 100644 GIT binary patch delta 1506 zcmdT^OKTHR82xTCc{OR0mKdjrkC8;uLRw9y(3{ogosNzBd^QdXfB$LNPDk5=F za4GSw1pNbUG|XCDs!(v{##R4;e?ZW4r^Y7JtXv2V+?$(w&-b14o%`6nv3*Y=B6g)y z&JfLF=tUF1;Q+d4M2f$sq>eiAhoLQ_VH_q}rxF>|BAx2JW`dtiN^=4rp~bMM4FhPN ziqxS^%$70VdLc*n&P*^U^^i69)B1?`hbg7sbb3sWSMsuy`6c>}1C3Y{ue#x)U5 z50R;a(LChT={{B$sDbrOYLl_7>7}Bk7oh_xov*?0nH%!$RFobd1_;CnbO9p~+oBp~ z+E_E7vB7tzBa_l$5%yb{`B6?XpU)zwlfi?se9lrn0Dua%34Sa?i$PoCbU0S|?#%PK z37{$#b_>`N6rhGt)q)=Aax51LKx+Z0D*6ho3-YcYcdQO<1G&cn8Gvnw5=WE4KyrMw zp*57cC&Gqi=WS>=a(A$OqP&uoIkfL?LV(Rzu^r}`MA>cbkWY4D`3Rp2E02)nfHJGh zVPzBhNYv5-W5aHwRr_#^54j7#)?uawGfPpb9yGvt$Gm&wjkEvMFR>HmrEr#=^3i2j zlc(!+lhzPiqw;8+kVopkI8&ec#`wi}C!%K4%3^luNm_ZFIjgE^MO|FtpWULM{wktH+`AI-|pk}6(K&&*`OhIn5 zl7bB5<^sj*jFWdLmU0-Hn(HVSnww0%qu{WaSw)0#v!|*E **Note**: This skill involves interpreting visual size reports (Ruler, Emerge Tools X-Ray). AI agents cannot yet process screenshots autonomously. Use this as a guide while reviewing the reports manually, or await MCP-based visual feedback integration (see roadmap). + +## Key Metrics + +| Metric | Description | User Impact | +|--------|-------------|-------------| +| Download Size | Compressed, transferred over network | Download time, data usage | +| Install Size | Uncompressed, on device storage | Storage space | + +**Google finding**: Every 6 MB increase reduces installs by 1%. + +## Android: Ruler (Spotify) + +### Setup + +Add to `android/build.gradle`: + +```groovy +buildscript { + dependencies { + classpath("com.spotify.ruler:ruler-gradle-plugin:2.0.0-beta-3") + } +} +``` + +Add to `android/app/build.gradle`: + +```groovy +apply plugin: "com.spotify.ruler" + +ruler { + abi.set("arm64-v8a") // Target architecture + locale.set("en") + screenDensity.set(480) + sdkVersion.set(34) +} +``` + +### Analyze + +```bash +cd android +./gradlew analyzeReleaseBundle +``` + +Opens HTML report with: +- Download size +- Install size +- Component breakdown (biggest → smallest) + +### CI Size Validation + +```groovy +ruler { + verification { + downloadSizeThreshold = 20 * 1024 * 1024 // 20 MB + installSizeThreshold = 50 * 1024 * 1024 // 50 MB + } +} +``` + +Build fails if thresholds exceeded. + +## iOS: Xcode App Thinning + +### Via App Store Connect (Most Accurate) + +After uploading to TestFlight: +1. Open App Store Connect +2. Go to your build +3. View size table by device variant + +**Note**: TestFlight builds include debug data, App Store builds slightly larger due to DRM. + +### Via Xcode Export + +1. Archive app: **Product → Archive** +2. In Organizer, click **Distribute App** +3. Select **Custom** +4. Choose **App Thinning: All compatible device variants** + +Or in `ExportOptions.plist`: + +```xml +thinning +<thin-for-all-variants> +``` + +### Output + +Creates folder with: +- **Universal IPA**: All variants combined +- **Thinned IPAs**: One per device variant +- **App Thinning Size Report.txt**: + +``` +Variant: SampleApp-.ipa +App + On Demand Resources size: 3.5 MB compressed, 10.6 MB uncompressed +App size: 3.5 MB compressed, 10.6 MB uncompressed +``` + +- Compressed = Download size +- Uncompressed = Install size + +## Emerge Tools (Cross-Platform) + +Third-party service with visual analysis. + +### Upload + +Upload IPA, APK, or AAB through their web interface or CI integration. + +### Features + +![Emerge Tools X-Ray for iOS](images/emerge-xray-ios.png) + +- **X-Ray**: Treemap visualization (like source-map-explorer for binaries) + - Shows Frameworks (hermes.framework), Mach-O sections (TEXT, DATA), etc. + - Color-coded: Binaries, Localizations, Fonts, Asset Catalogs, Videos, CoreML Models + - Visible components: `main.jsbundle` (JS code), RCT modules, DYLD sections +- **Breakdown**: Component-by-component size +- **Insights**: Automated suggestions (use with caution) + +**Caution**: Some suggestions may not apply to React Native (e.g., "remove Hermes"). + +## Size Comparison + +| Tool | Platform | Accuracy | CI Integration | +|------|----------|----------|----------------| +| Ruler | Android | High | Yes (Gradle) | +| App Store Connect | iOS | Highest | No | +| Xcode Export | iOS | High | Yes (xcodebuild) | +| Emerge Tools | Both | High | Yes (API) | + +## Typical React Native App Sizes + +| Component | Approximate Size | +|-----------|------------------| +| Hermes engine | ~2-3 MB | +| React Native core | ~3-5 MB | +| JavaScript bundle | 1-10 MB | +| Assets (images, etc.) | Varies | + +**Baseline empty app**: ~6-10 MB download + +## Optimization Impact Example + +| Optimization | Size Reduction | +|--------------|----------------| +| Enable R8 (Android) | ~30% | +| Remove unused polyfills | 400+ KB | +| Asset catalog (iOS) | 10-50% of assets | +| Tree shaking | 10-15% | + +## Quick Commands + +```bash +# Android release bundle size +cd android && ./gradlew bundleRelease +# Check: android/app/build/outputs/bundle/release/ + +# iOS archive +cd ios && xcodebuild -workspace ios/MyApp.xcworkspace \ + -scheme MyApp \ + -configuration Release \ + -archivePath MyApp.xcarchive \ + archive + +# Export with thinning report +cd ios && xcodebuild -exportArchive \ + -archivePath MyApp.xcarchive \ + -exportPath ./export \ + -exportOptionsPlist ExportOptions.plist +``` + +## Related Skills + +- [bundle-r8-android.md](./bundle-r8-android.md) - Reduce Android size +- [bundle-native-assets.md](./bundle-native-assets.md) - Optimize asset delivery +- [bundle-analyze-js.md](./bundle-analyze-js.md) - JS bundle analysis diff --git a/.claude/skills/react-native-best-practices/references/bundle-analyze-js.md b/.claude/skills/react-native-best-practices/references/bundle-analyze-js.md new file mode 100644 index 0000000..531a4ea --- /dev/null +++ b/.claude/skills/react-native-best-practices/references/bundle-analyze-js.md @@ -0,0 +1,262 @@ +--- +title: Analyze JS Bundle Size +impact: CRITICAL +tags: bundle, analysis, source-map-explorer, expo-atlas +--- + +# Skill: Analyze JS Bundle Size + +Use source-map-explorer and Expo Atlas to visualize what's in your JavaScript bundle. + +## Quick Command + +```bash +# React Native CLI +npx react-native bundle \ + --entry-file index.js \ + --bundle-output output.js \ + --platform ios \ + --sourcemap-output output.js.map \ + --dev false --minify true && \ +npx source-map-explorer output.js --no-border-checks + +# Expo +EXPO_UNSTABLE_ATLAS=true npx expo export --platform ios && npx expo-atlas +``` + +## When to Use + +- JS bundle seems too large +- Want to identify heavy dependencies +- Investigating startup time issues +- Before/after optimization comparison + +> **Note**: This skill involves interpreting visual treemap output (source-map-explorer, Expo Atlas). AI agents cannot yet process screenshots autonomously. Use this as a guide while reviewing the visualization manually, or await MCP-based visual feedback integration (see roadmap). + +## Understanding Hermes Bytecode + +Modern React Native (0.70+) uses Hermes bytecode, not raw JavaScript: +- Skips parsing at runtime +- Still benefits from smaller bundles +- Heavy imports still execute on startup + +**Impact of bundle size:** +- Larger bytecode = longer download from store +- More imports on init path = slower TTI + +## Method 1: source-map-explorer + +### Generate Bundle with Source Map + +**React Native CLI:** + +```bash +npx react-native bundle \ + --entry-file index.js \ + --bundle-output output.js \ + --platform ios \ + --sourcemap-output output.js.map \ + --dev false \ + --minify true +``` + +**Expo (SDK 51+):** + +```bash +npx expo export --platform ios --source-maps --output-dir dist +# Bundle at: dist/ios/_expo/static/js/ios/*.js +# Source map at: dist/ios/_expo/static/js/ios/*.map +``` + +### Analyze + +```bash +npx source-map-explorer output.js --no-border-checks +``` + +**Note**: `--no-border-checks` needed due to Metro's non-standard source maps. + +Opens browser with treemap visualization: + +![Bundle Treemap from source-map-explorer](images/bundle-treemap-source-map-explorer.png) + +The treemap shows: +- **Hierarchy**: `node_modules/` → `react-native/` → `Libraries/` → individual files +- **Size**: Box area proportional to file size (KB shown in labels) +- **Major components visible**: + - `react-native` (724.18 KB, 80.5%) + - `Renderer` (208.44 KB) - ReactNativeRenderer-prod.js, ReactFabric-prod.js + - `Components` (125.29 KB) - Touchable, ScrollView, etc. + - `Animated` (79.48 KB) - Animation system + - `virtualized-lists` (57.57 KB) - FlatList internals + +Click on any section to drill down into that directory. + +**Limitation**: May lose ~30% info due to mapping issues. + +## Method 2: Expo Atlas + +More accurate for Expo projects (or with workaround for bare RN). + +### For Expo Projects + +```bash +# Start with Atlas enabled +EXPO_UNSTABLE_ATLAS=true npx expo start --no-dev + +# Or export +EXPO_UNSTABLE_ATLAS=true npx expo export +``` + +Then launch UI: + +```bash +npx expo-atlas +``` + +![Expo Atlas Treemap](images/expo-atlas-treemap.png) + +Expo Atlas provides more accurate visualization for Expo projects, with similar treemap interface showing module sizes and dependencies. + +### For Non-Expo Projects + +Use `expo-atlas-without-expo` package. + +## Method 3: Re.Pack Bundle Analysis (Webpack/Rspack) + +If using Re.Pack: + +### webpack-bundle-analyzer + +```bash +rspack build --analyze +``` + +### bundle-stats / statoscope + +```bash +# Generate stats +npx react-native bundle \ + --platform android \ + --entry-file index.js \ + --dev false \ + --minify true \ + --json stats.json + +# Analyze +npx bundle-stats --html --json stats.json +``` + +### Rsdoctor + +```javascript +// rspack.config.js +const { RsdoctorRspackPlugin } = require('@rsdoctor/rspack-plugin'); + +module.exports = { + plugins: [ + process.env.RSDOCTOR && new RsdoctorRspackPlugin(), + ].filter(Boolean), +}; +``` + +Run with: + +```bash +RSDOCTOR=true npx react-native start +``` + +## What to Look For + +### Red Flags + +| Finding | Problem | Solution | +|---------|---------|----------| +| Entire library imported | Barrel exports | Use direct imports | +| Duplicate packages | Multiple versions | Dedupe in package.json | +| Dev dependencies in bundle | Incorrect imports | Check conditional imports | +| Large polyfills | Unnecessary for Hermes | Remove (see native-sdks-over-polyfills.md) | +| Moment.js with locales | Bloated date library | Switch to date-fns or dayjs | + +### Common Offenders + +- **Lodash full import**: Use `lodash-es` or specific imports +- **Moment.js**: Replace with `date-fns` or `dayjs` +- **Intl polyfills**: Check Hermes API and method coverage before removing them +- **AWS SDK**: Import specific services only + +## Code Examples + +### Identify Barrel Import Impact + +```tsx +// BAD: Imports entire library through barrel +import { format } from 'date-fns'; + +// In bundle: All of date-fns loaded + +// GOOD: Direct import +import format from 'date-fns/format'; + +// In bundle: Only format function +``` + +## Comparing Bundles + +### source-map-explorer + +```bash +# Generate baseline +npx react-native bundle ... --bundle-output baseline.js --sourcemap-output baseline.js.map + +# Make changes, generate new bundle +npx react-native bundle ... --bundle-output current.js --sourcemap-output current.js.map + +# Compare manually in browser +``` + +### Re.Pack (automated) + +```bash +npx bundle-stats compare baseline-stats.json current-stats.json +``` + +## Quick Commands + +**React Native CLI:** + +```bash +# iOS bundle analysis +npx react-native bundle \ + --entry-file index.js \ + --bundle-output ios-bundle.js \ + --platform ios \ + --sourcemap-output ios-bundle.js.map \ + --dev false \ + --minify true && \ +npx source-map-explorer ios-bundle.js --no-border-checks + +# Android bundle analysis +npx react-native bundle \ + --entry-file index.js \ + --bundle-output android-bundle.js \ + --platform android \ + --sourcemap-output android-bundle.js.map \ + --dev false \ + --minify true && \ +npx source-map-explorer android-bundle.js --no-border-checks +``` + +**Expo:** + +```bash +# Use Expo Atlas (recommended for Expo projects) +EXPO_UNSTABLE_ATLAS=true npx expo export --platform ios +npx expo-atlas +``` + +## Related Skills + +- [bundle-barrel-exports.md](./bundle-barrel-exports.md) - Fix barrel import issues +- [bundle-tree-shaking.md](./bundle-tree-shaking.md) - Enable dead code elimination +- [bundle-library-size.md](./bundle-library-size.md) - Check library sizes before adding diff --git a/.claude/skills/react-native-best-practices/references/bundle-barrel-exports.md b/.claude/skills/react-native-best-practices/references/bundle-barrel-exports.md new file mode 100644 index 0000000..42b37f2 --- /dev/null +++ b/.claude/skills/react-native-best-practices/references/bundle-barrel-exports.md @@ -0,0 +1,248 @@ +--- +title: Avoid Barrel Exports +impact: CRITICAL +tags: bundle, imports, barrel, tree-shaking +--- + +# Skill: Avoid Barrel Exports + +Refactor barrel imports (index files) to reduce bundle size and improve startup time. + +## Quick Pattern + +**Incorrect:** + +```tsx +import { Button } from './components'; +// Loads ALL exports from components/index.ts +``` + +**Correct:** + +```tsx +import Button from './components/Button'; +// Loads only Button +``` + +## When to Use + +- Bundle contains unused code from libraries +- Circular dependency warnings in Metro +- Hot Module Replacement (HMR) breaks frequently +- TTI is slow due to module evaluation + +## What Are Barrel Exports? + +```tsx +// components/index.ts (barrel file) +export { Button } from './Button'; +export { Card } from './Card'; +export { Modal } from './Modal'; +export { Sidebar } from './Sidebar'; + +// Usage (barrel import) +import { Button } from './components'; +``` + +## Problems with Barrel Imports + +### 1. Bundle Size Overhead + +Metro includes **all exports** even if you use one: + +```tsx +// Only need Button, but entire barrel is bundled +import { Button } from './components'; +// Card, Modal, Sidebar also included! +``` + +### 2. Runtime Overhead + +All modules evaluate before returning your import: + +```tsx +import { Button } from './components'; +// JavaScript must evaluate: +// - Button.tsx +// - Card.tsx +// - Modal.tsx +// - Sidebar.tsx +// Even though you only use Button +``` + +### 3. Circular Dependencies + +Barrel files make cycles easier to create accidentally: + +``` +Warning: Require cycle: + components/index.ts -> Button.tsx -> utils/index.ts -> components/index.ts +``` + +Breaks HMR, causes unpredictable behavior. + +## Solution 1: Direct Imports + +Replace barrel imports with direct paths: + +```tsx +// BEFORE: Barrel import +import { Button, Card } from './components'; + +// AFTER: Direct imports +import Button from './components/Button'; +import Card from './components/Card'; +``` + +### Enforce with ESLint + +```bash +npm install -D eslint-plugin-no-barrel-files +``` + +```javascript +// eslint.config.js +import noBarrelFiles from 'eslint-plugin-no-barrel-files'; + +export default [ + { + plugins: { 'no-barrel-files': noBarrelFiles }, + rules: { + 'no-barrel-files/no-barrel-files': 'error', + }, + }, +]; +``` + +## Solution 2: Tree Shaking (Automatic) + +Enable tree shaking to automatically remove unused barrel exports. + +### Expo SDK 52+ + +```tsx +// metro.config.js +const { getDefaultConfig } = require('expo/metro-config'); +const config = getDefaultConfig(__dirname); + +config.transformer.getTransformOptions = async () => ({ + transform: { + experimentalImportSupport: true, + }, +}); + +module.exports = config; +``` + +```bash +# .env +EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 +EXPO_UNSTABLE_TREE_SHAKING=1 +``` + +### metro-serializer-esbuild + +```bash +npm install @rnx-kit/metro-serializer-esbuild +``` + +### Re.Pack (Webpack/Rspack) + +Tree shaking built-in. + +## Real-World Example: date-fns + +```tsx +// BAD: Imports entire library +import { format, addDays, isToday } from 'date-fns'; + +// GOOD: Direct imports +import format from 'date-fns/format'; +import addDays from 'date-fns/addDays'; +import isToday from 'date-fns/isToday'; +``` + +## Library-Specific Solutions + +Some libraries provide Babel plugins: + +### React Native Paper + +```javascript +// babel.config.js +module.exports = { + plugins: [ + 'react-native-paper/babel', // Auto-transforms imports + ], +}; +``` + +Transforms: +```tsx +import { Button } from 'react-native-paper'; +// Into: +import Button from 'react-native-paper/lib/module/components/Button'; +``` + +## Refactoring Strategy + +### Step 1: Identify Barrel Files + +Look for `index.ts` files with multiple exports: + +```bash +grep -r "export \* from" src/ +grep -r "export { .* } from" src/ +``` + +### Step 2: Update Imports + +```tsx +// Find all usages +// VS Code: Cmd+Shift+F for "from './components'" + +// Replace each with direct import +import Button from './components/Button'; +``` + +### Step 3: (Optional) Keep Barrel for External API + +If your package is consumed by others: + +```tsx +// Keep index.ts for package API +// components/index.ts +export { Button } from './Button'; + +// Internal code uses direct imports +// src/screens/Home.tsx +import Button from '../components/Button'; +``` + +## Migration Script Example + +```bash +# Use codemod or search-replace +# Find: import { (\w+) } from '\.\/components'; +# Replace: import $1 from './components/$1'; +``` + +## Verification + +After refactoring: + +1. Run bundle analysis (see [bundle-analyze-js.md](./bundle-analyze-js.md)) +2. Compare sizes before/after +3. Check for circular dependency warnings + +## Common Pitfalls + +- **Breaking external consumers**: If publishing a library, keep barrel for public API +- **IDE auto-imports**: Configure IDE to prefer direct imports +- **Inconsistent patterns**: Enforce with ESLint across team + +## Related Skills + +- [bundle-analyze-js.md](./bundle-analyze-js.md) - Verify impact +- [bundle-tree-shaking.md](./bundle-tree-shaking.md) - Automatic solution +- [bundle-library-size.md](./bundle-library-size.md) - Check library patterns diff --git a/.claude/skills/react-native-best-practices/references/bundle-code-splitting.md b/.claude/skills/react-native-best-practices/references/bundle-code-splitting.md new file mode 100644 index 0000000..9eb18f2 --- /dev/null +++ b/.claude/skills/react-native-best-practices/references/bundle-code-splitting.md @@ -0,0 +1,247 @@ +--- +title: Remote Code Loading +impact: MEDIUM +tags: code-splitting, repack, lazy-loading, chunks +--- + +# Skill: Remote Code Loading + +Set up code splitting with Re.Pack for on-demand bundle loading from trusted, first-party assets. + +## Quick Pattern + +**Before (static import):** + +```jsx +import SettingsScreen from './screens/SettingsScreen'; +``` + +**After (lazy loaded chunk):** + +```jsx +const SettingsScreen = React.lazy(() => + import(/* webpackChunkName: "settings" */ './screens/SettingsScreen') +); + +}> + + +``` + +## When to Use + +Consider code splitting when: +- **Not using Hermes** (JSC/V8 benefits more) +- App size exceeds 200 MB (Play Store limit) +- Building micro-frontend architecture +- Loading features based on user permissions +- Other optimizations exhausted + +**Note**: Hermes already uses memory mapping for efficient bundle reading. Benefits of code splitting are minimal with Hermes or even counterproductive in some cases. + +## Security Model + +Remote chunks are executable application code. Only load chunks that you build and publish yourself. + +Keep these guardrails in place: +- Serve chunks only from a first-party, HTTPS-only origin you control +- Resolve `scriptId` through a fixed allowlist or release manifest +- Fail closed if a chunk is missing or unexpected +- Do not load chunks from user-controlled input, query params, or third-party domains + +## Prerequisites + +- Re.Pack installed (replaces Metro) + +```bash +npx @callstack/repack-init +``` + +## Step-by-Step Instructions + +### 1. Initialize Re.Pack + +```bash +npx @callstack/repack-init +``` + +Follow prompts to migrate from Metro. Check [migration guide](https://re-pack.dev/docs/getting-started/quick-start). + +### 2. Create Split Point with React.lazy + +```tsx +// BEFORE: Static import +import SettingsScreen from './screens/SettingsScreen'; + +// AFTER: Dynamic import (creates split point) +const SettingsScreen = React.lazy(() => + import(/* webpackChunkName: "settings" */ './screens/SettingsScreen') +); +``` + +### 3. Wrap with Suspense + +```tsx +import React, { Suspense } from 'react'; + +const App = () => { + return ( + }> + + + ); +}; +``` + +### 4. Configure Chunk Loading + +```jsx +// index.js (before AppRegistry) +import { ScriptManager, Script } from '@callstack/repack/client'; + +const CHUNK_URLS = { + settings: 'https://assets.example.com/app/v42/settings.chunk.bundle', +}; + +ScriptManager.shared.addResolver((scriptId) => ({ + url: __DEV__ ? Script.getDevServerURL(scriptId) : getChunkUrl(scriptId), +})); + +function getChunkUrl(scriptId) { + const url = CHUNK_URLS[scriptId]; + + if (!url) { + throw new Error(`Unknown chunk: ${scriptId}`); + } + + return url; +} + +AppRegistry.registerComponent(appName, () => App); +``` + +### 5. Build and Deploy Chunks + +Build generates: +- `index.bundle` - Main bundle +- `settings.chunk.bundle` - Lazy-loaded chunk + +Deploy chunks to a first-party CDN with versioned paths, and keep the allowlist or manifest in sync with the app release. + +## Complete Example + +```tsx +// App.tsx +import React, { Suspense, useState } from 'react'; +import { Button, View, ActivityIndicator } from 'react-native'; + +// Lazy load heavy feature +const HeavyFeature = React.lazy(() => + import(/* webpackChunkName: "heavy-feature" */ './HeavyFeature') +); + +const App = () => { + const [showFeature, setShowFeature] = useState(false); + + return ( + + diff --git a/src/components/check-in-timers/check-in-tab-content.tsx b/src/components/check-in-timers/check-in-tab-content.tsx index 1565532..9768516 100644 --- a/src/components/check-in-timers/check-in-tab-content.tsx +++ b/src/components/check-in-timers/check-in-tab-content.tsx @@ -9,7 +9,10 @@ import { HStack } from '@/components/ui/hstack'; import { Text } from '@/components/ui/text'; import { VStack } from '@/components/ui/vstack'; import { useQuickCheckIn } from '@/hooks/use-quick-check-in'; +import { useAuthStore } from '@/lib/auth'; +import { CHECK_IN_TARGET_TYPE, getEligibleCheckInTypeValues, getPreferredQuickCheckInType, isCheckInTargetEligible } from '@/lib/check-in-eligibility'; import type { CheckInTimerStatusResultData } from '@/models/v4/checkIn/checkInTimerStatusResultData'; +import { useCoreStore } from '@/stores/app/core-store'; import { useCheckInTimerStore } from '@/stores/check-in-timers/store'; import { CheckInBottomSheet } from './check-in-bottom-sheet'; @@ -26,9 +29,19 @@ export const CheckInTabContent: React.FC = ({ callId }) const checkInHistory = useCheckInTimerStore((state) => state.checkInHistory); const isLoadingStatuses = useCheckInTimerStore((state) => state.isLoadingStatuses); const fetchCheckInHistory = useCheckInTimerStore((state) => state.fetchCheckInHistory); + const activeUnit = useCoreStore((state) => state.activeUnit); + const currentUserId = useAuthStore((state) => state.userId); const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false); const [showHistory, setShowHistory] = useState(false); - const { quickCheckIn, isCheckingIn } = useQuickCheckIn(callId); + const [selectedCheckInType, setSelectedCheckInType] = useState(CHECK_IN_TARGET_TYPE.PERSONNEL); + const eligibilityContext = { + currentUnitTypeId: activeUnit?.TypeId, + hasCurrentUser: Boolean(currentUserId), + }; + const eligibleTimerStatuses = timerStatuses.filter((timer) => isCheckInTargetEligible(timer, eligibilityContext)); + const availableCheckInTypes = getEligibleCheckInTypeValues(eligibleTimerStatuses, eligibilityContext); + const quickCheckInType = getPreferredQuickCheckInType(eligibleTimerStatuses, eligibilityContext, true); + const { quickCheckIn, isCheckingIn } = useQuickCheckIn(callId, quickCheckInType ?? CHECK_IN_TARGET_TYPE.PERSONNEL); useEffect(() => { if (showHistory) { @@ -36,15 +49,16 @@ export const CheckInTabContent: React.FC = ({ callId }) } }, [showHistory, callId, fetchCheckInHistory]); - const handleCardCheckIn = useCallback(() => { + const handleCardCheckIn = useCallback((checkInType: number) => { + setSelectedCheckInType(checkInType); setIsBottomSheetOpen(true); }, []); - const renderTimerCard = useCallback(({ item }: { item: CheckInTimerStatusResultData }) => , [handleCardCheckIn]); + const renderTimerCard = useCallback(({ item }: { item: CheckInTimerStatusResultData }) => handleCardCheckIn(item.TargetType)} />, [handleCardCheckIn]); const keyExtractor = useCallback((item: CheckInTimerStatusResultData) => `${item.TargetEntityId}-${item.TargetType}`, []); - if (timerStatuses.length === 0 && !isLoadingStatuses) { + if (eligibleTimerStatuses.length === 0 && !isLoadingStatuses) { return ( {t('check_in.no_timers')} @@ -55,12 +69,14 @@ export const CheckInTabContent: React.FC = ({ callId }) return ( {/* Quick Check-In button */} - + {quickCheckInType !== null ? ( + + ) : null} {/* Timer cards */} - + {/* History section */} @@ -72,7 +88,7 @@ export const CheckInTabContent: React.FC = ({ callId }) {showHistory ? : null} - setIsBottomSheetOpen(false)} callId={callId} /> + setIsBottomSheetOpen(false)} callId={callId} availableCheckInTypes={availableCheckInTypes} defaultCheckInType={selectedCheckInType} /> ); }; diff --git a/src/components/check-in-timers/check-in-timer-card.tsx b/src/components/check-in-timers/check-in-timer-card.tsx index 5dfac1d..fa778d6 100644 --- a/src/components/check-in-timers/check-in-timer-card.tsx +++ b/src/components/check-in-timers/check-in-timer-card.tsx @@ -9,14 +9,9 @@ import { Button, ButtonText } from '@/components/ui/button'; import { HStack } from '@/components/ui/hstack'; import { Text } from '@/components/ui/text'; import { VStack } from '@/components/ui/vstack'; +import { getCheckInTimerStatusColor, getCheckInTimerStatusTranslationKey, isCheckInTimerCritical } from '@/lib/check-in-timer-utils'; import type { CheckInTimerStatusResultData } from '@/models/v4/checkIn/checkInTimerStatusResultData'; -const STATUS_COLORS: Record = { - Ok: '#22C55E', - Warning: '#F59E0B', - Overdue: '#EF4444', -}; - interface CheckInTimerCardProps { timer: CheckInTimerStatusResultData; onCheckIn: () => void; @@ -40,9 +35,11 @@ export const CheckInTimerCard: React.FC = ({ timer, onChe return () => clearInterval(interval); }, []); - // Pulse animation for overdue + const isCritical = isCheckInTimerCritical(timer.Status); + + // Pulse animation for critical timers useEffect(() => { - if (timer.Status === 'Overdue') { + if (isCritical) { const animation = Animated.loop( Animated.sequence([Animated.timing(pulseAnim, { toValue: 0.5, duration: 800, useNativeDriver: true }), Animated.timing(pulseAnim, { toValue: 1, duration: 800, useNativeDriver: true })]) ); @@ -51,12 +48,13 @@ export const CheckInTimerCard: React.FC = ({ timer, onChe } else { pulseAnim.setValue(1); } - }, [timer.Status, pulseAnim]); + }, [isCritical, pulseAnim]); - const statusColor = STATUS_COLORS[timer.Status] ?? '#808080'; + const statusColor = getCheckInTimerStatusColor(timer.Status); + const statusTranslationKey = getCheckInTimerStatusTranslationKey(timer.Status); + const statusLabel = statusTranslationKey ? t(statusTranslationKey) : timer.Status || t('common.unknown'); const duration = timer.DurationMinutes ? Number(timer.DurationMinutes) : 0; const progress = duration > 0 ? Math.min(localElapsed / duration, 1) : 0; - const safeStatusLower = typeof timer.Status === 'string' ? timer.Status.toLowerCase() : ''; const minutesSinceLastCheckIn = timer.LastCheckIn ? differenceInMinutes(new Date(), timer.LastCheckIn) : 0; return ( @@ -73,7 +71,7 @@ export const CheckInTimerCard: React.FC = ({ timer, onChe - {t(`check_in.status_${safeStatusLower}`)} + {statusLabel} diff --git a/src/components/sidebar/sidebar-content.tsx b/src/components/sidebar/sidebar-content.tsx index a0464ec..d4480f1 100644 --- a/src/components/sidebar/sidebar-content.tsx +++ b/src/components/sidebar/sidebar-content.tsx @@ -12,7 +12,6 @@ import { useCoreStore } from '@/stores/app/core-store'; import { useStatusBottomSheetStore } from '@/stores/status/store'; import ZeroState from '../common/zero-state'; -import { StatusBottomSheet } from '../status/status-bottom-sheet'; import { SidebarCallCard } from './call-sidebar'; import { CheckInSidebarWidget } from './check-in-sidebar-widget'; import { SidebarRolesCard } from './roles-sidebar'; @@ -97,7 +96,6 @@ const Sidebar = ({ onClose }: SidebarProps) => { ))} )} - ); diff --git a/src/components/status/__tests__/status-bottom-sheet.test.tsx b/src/components/status/__tests__/status-bottom-sheet.test.tsx index 93c2257..0f7478d 100644 --- a/src/components/status/__tests__/status-bottom-sheet.test.tsx +++ b/src/components/status/__tests__/status-bottom-sheet.test.tsx @@ -231,6 +231,7 @@ const mockTranslation = { 'common.of': 'of', 'common.next': 'Next', 'common.previous': 'Previous', + 'common.cancel': 'Cancel', 'common.submit': 'Submit', 'common.submitting': 'Submitting', 'common.optional': 'Optional', @@ -2625,6 +2626,7 @@ describe('StatusBottomSheet', () => { ...defaultBottomSheetStore, isOpen: true, currentStep: 'add-note', + cameFromStatusSelection: true, selectedStatus, }; if (selector) { @@ -2641,6 +2643,43 @@ describe('StatusBottomSheet', () => { expect(mockSetCurrentStep).toHaveBeenCalledWith('select-status'); }); + it('should offer cancel instead of previous on the note step when the status was picked up front and has no destination', () => { + const selectedStatus = { + Id: 1, + Type: 1, + StateId: 1, + Text: 'Patient Contact', + BColor: '#28a745', + Color: '#fff', + Gps: false, + Note: 1, // Note optional + Detail: 0, // No destination step — nothing precedes the note step + }; + + mockUseStatusBottomSheetStore.mockImplementation((selector: any) => { + const store = { + ...defaultBottomSheetStore, + isOpen: true, + currentStep: 'add-note', + cameFromStatusSelection: false, + selectedStatus, + }; + if (selector) { + return selector(store); + } + return store; + }); + + render(); + + expect(screen.queryByText('Previous')).toBeNull(); + + fireEvent.press(screen.getByText('Cancel')); + + expect(mockReset).toHaveBeenCalled(); + expect(mockSetCurrentStep).not.toHaveBeenCalledWith('select-status'); + }); + it('should calculate correct step numbers with status selection', () => { // Test step 1 of 3 (status, destination, note) const selectedStatusWithAll = { diff --git a/src/components/status/status-bottom-sheet.tsx b/src/components/status/status-bottom-sheet.tsx index 643d836..3205e80 100644 --- a/src/components/status/status-bottom-sheet.tsx +++ b/src/components/status/status-bottom-sheet.tsx @@ -491,7 +491,8 @@ export const StatusBottomSheet = () => { case 'select-status': return t('status.select_status'); case 'select-destination': - return t('status.select_destination', { status: selectedStatus?.Text }); + // A status with no destination options still lands here as a confirm step. + return shouldShowDestinationStep ? t('status.select_destination', { status: selectedStatus?.Text }) : t('status.set_status'); case 'add-note': return t('status.add_note'); default: @@ -606,6 +607,10 @@ export const StatusBottomSheet = () => { return t('status.no_destination'); }; + // Opening straight into 'add-note' (status picked up front, no destination to choose) + // leaves nothing to go back to, so the note step offers Cancel instead of Previous. + const hasStepBeforeNote = cameFromStatusSelection || shouldShowDestinationStep; + const shouldShowDestinationTabs = destinationTabs.length > 1; const showCalls = destinationTabs.includes('call') && (!shouldShowDestinationTabs || selectedTab === 'call'); const showStations = destinationTabs.includes('station') && (!shouldShowDestinationTabs || selectedTab === 'station'); @@ -880,10 +885,16 @@ export const StatusBottomSheet = () => { - + {hasStepBeforeNote ? ( + + ) : ( + + )}