Skip to content

feat: Offline Map Downloads by Region#1903

Draft
Copilot wants to merge 4 commits intomainfrom
copilot/feature-offline-map-downloads
Draft

feat: Offline Map Downloads by Region#1903
Copilot wants to merge 4 commits intomainfrom
copilot/feature-offline-map-downloads

Conversation

Copy link
Contributor

Copilot AI commented Mar 9, 2026

  • Explore repository structure and understand patterns
  • Add enableOfflineMaps feature flag to apps/expo/config.ts
  • Create apps/expo/features/offline-maps/types.ts
  • Create apps/expo/features/offline-maps/store/offlineMaps.ts (Legend State + SQLite)
  • Create apps/expo/features/offline-maps/constants.ts (shared OFFLINE_MAPS_DIR, REGION_TEAL_COLOR, REGION_FILL_COLOR, ERR_DUPLICATE_DOWNLOAD, ERR_INSUFFICIENT_STORAGE)
  • Create apps/expo/features/offline-maps/utils/regions.ts (predefined regions, size estimation)
  • Create hooks: useOfflineMapRegions, useDownloadMapRegion, useDeleteMapRegion, useOfflineMapsStorageInfo
  • Create OfflineMapsTile.tsx dashboard tile component
  • Create apps/expo/features/offline-maps/screens/OfflineMapsScreen.tsx
  • Create apps/expo/app/(app)/offline-maps.tsx route
  • Register OfflineMapsTile in home dashboard (index.tsx)
  • Add offlineMaps translations to en.json
  • Address CodeRabbit review feedback (all 15 items)
  • Fix TypeScript CI failures:
    • Replace invalid "download" icon with "inbox-arrow-down" in OfflineMapsTile
    • Add @ts-ignore (Legend-State Proxy pattern) on dynamic store property access
    • Wrap positioned <Icon> in <View> (style prop not available on Icon)
  • Address Copilot PR reviewer feedback:
    • Fix StorageBanner double-count — use i18n plural keys (completedRegions_one/other) instead of manual ${count} + t(key)
    • downloadRegion throws ERR_DUPLICATE_DOWNLOAD instead of silently returning; modal shows inline error
    • runDownloadInBackground — second signal.cancelled check after setTimeout sleep prevents post-cancel store writes
    • useOfflineMapsStorageInfototalSize now sums only completed regions
    • Remove dead duplicateDownloadTitle/insufficientStorageTitle i18n strings; duplicateDownloadMessage wired into error handler
    • Extract error code constants to constants.ts (no magic string matching)
  • Run format/lint — clean
  • Run type check — passes
  • Run CodeQL — no alerts
Original prompt

This section details on the original issue you should resolve

<issue_title>Feature: Offline Map Downloads by Region</issue_title>
<issue_description>## Summary

Allow hikers to download map tiles for specific regions for offline use.

Why

  • Many trails have no cellular connectivity
  • Offline maps are essential for safety and navigation
  • Users can plan trips and access maps without signal
  • Competitive advantage over apps that require connectivity

Technical Approach

MBTiles Format

  • SQLite-based tile container (compact, efficient)
  • Vector tiles preferred (smaller, fully stylable)
  • Pre-generated tiles for popular hiking regions

Progressive Download UI

  • State park: 10-50 MB
  • County: 50-200 MB
  • State: 200-500 MB
  • User selects region and zoom levels

Storage Management

  • Configurable storage limits
  • Auto-cleanup of unused tiles
  • Size estimates before download

Deliverable

MVP that demonstrates:

  • Select region for download
  • Progress indicator
  • Offline map viewing
  • Storage management</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

- Add enableOfflineMaps feature flag to config.ts
- Create features/offline-maps module with types, store, hooks, components, screens
- Add OfflineMapsTile to home dashboard
- Add offline-maps route
- Add offlineMaps translations to en.json

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copilot AI changed the title [WIP] Add offline map downloads by region feature feat: Offline Map Downloads by Region Mar 9, 2026
@andrew-bierman
Copy link
Collaborator

@coderabbitai Please review this PR for code quality, best practices, and potential issues.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List all files in the offline-maps feature module
find . -path "*/offline-maps*" -type f | sort

echo "--- config.ts ---"
fd "config.ts" --max-depth 3 | head -5

echo "--- en.json ---"
fd "en.json" --max-depth 5 | head -5

echo "--- offline-maps route ---"
fd "offline-maps.tsx" | head -5

Length of output: 921


🏁 Script executed:

#!/bin/bash
echo "=== types.ts ==="
cat apps/expo/features/offline-maps/types.ts

echo "=== store/offlineMaps.ts ==="
cat apps/expo/features/offline-maps/store/offlineMaps.ts

echo "=== hooks/useDownloadMapRegion.ts ==="
cat apps/expo/features/offline-maps/hooks/useDownloadMapRegion.ts

echo "=== hooks/useDeleteMapRegion.ts ==="
cat apps/expo/features/offline-maps/hooks/useDeleteMapRegion.ts

echo "=== hooks/useOfflineMapRegions.ts ==="
cat apps/expo/features/offline-maps/hooks/useOfflineMapRegions.ts

echo "=== hooks/useOfflineMapsStorageInfo.ts ==="
cat apps/expo/features/offline-maps/hooks/useOfflineMapsStorageInfo.ts

echo "=== hooks/index.ts ==="
cat apps/expo/features/offline-maps/hooks/index.ts

echo "=== utils/regions.ts ==="
cat apps/expo/features/offline-maps/utils/regions.ts

echo "=== store/offlineMaps.ts ==="
cat apps/expo/features/offline-maps/store/offlineMaps.ts

echo "=== screens/OfflineMapsScreen.tsx ==="
cat apps/expo/features/offline-maps/screens/OfflineMapsScreen.tsx

echo "=== components/OfflineMapsTile.tsx ==="
cat apps/expo/features/offline-maps/components/OfflineMapsTile.tsx

echo "=== index.ts ==="
cat apps/expo/features/offline-maps/index.ts

echo "=== app route ==="
cat apps/expo/app/\(app\)/offline-maps.tsx

echo "=== config.ts ==="
cat apps/expo/config.ts

Length of output: 36524


@andrew-bierman, here is a detailed code quality review of PR #1903:


Review: feat/offline-maps

Overall the module is well-structured with clean types, a solid Legend State + SQLite store, and good i18n integration. The below issues range from a hook lifecycle bug to performance and UX gaps.


🔴 Critical / Bugs

1. useDownloadMapRegion instantiated inside AddRegionModalcancelRefs is lost on close

useDownloadMapRegion() is called inside AddRegionModal, which unmounts when onClose() is called. The download coroutine keeps running (because it captures local variables), but the cancelRefs from the hook are destroyed with the component. Any future attempt to call cancelDownload would operate on a new, empty cancelRefs, silently failing.

// OfflineMapsScreen.tsx — AddRegionModal
const { downloadRegion } = useDownloadMapRegion(); // ❌ hook dies when modal closes

const handleDownload = async () => {
  if (!selected) return;
  onClose();                                        // modal unmounts → hook's cancelRefs gone
  await downloadRegion(selected, minZoom, maxZoom); // download still runs
};

Fix: Move useDownloadMapRegion up to OfflineMapsScreen and pass downloadRegion as a prop to AddRegionModal, or call downloadRegion before onClose().


2. cancelDownload is exported but never wired to any UI

The hook exposes cancelDownload and the DownloadStatus type includes 'paused', but there is no cancel button in RegionCard for in-progress downloads. Users have no way to cancel a running download.

// RegionCard: only shows delete, not cancel for 'downloading' status
{region.status === 'completed' && (
  <Button onPress={onViewMap}>...</Button>       // ✅ view
)}
<Button onPress={onDelete}>...</Button>          // ✅ delete
// ❌ no cancel button when status === 'downloading'

3. Cancelled downloads leave orphaned 'idle' entries in the store

In cancelDownload, the status is set to 'idle' but the store entry is never removed. Over time these accumulate in the list.

// useDownloadMapRegion.ts
const cancelDownload = useCallback((id: string) => {
  if (cancelRefs.current[id]) {
    cancelRefs.current[id].cancelled = true;
  }
  offlineMapsStore[id].set((prev) => ({
    ...prev,
    status: 'idle',        // entry stays in store forever
    updatedAt: new Date().toISOString(),
  }));
}, []);

Consider calling offlineMapsStore[id].delete() instead.


4. No duplicate download guard

Each call to downloadRegion generates a new entry with ${region.id}-${Date.now()}. The same region can be downloaded multiple times concurrently with no warning.

const id = `${region.id}-${Date.now()}`;  // always unique → duplicates silently allowed

Consider checking if a region with the same region.id prefix already exists before starting a new download.


🟠 Performance

5. Multiple MapView instances inside a ScrollView

RegionCard renders a native MapView for each downloaded region. Mounting multiple heavy native map views in a single scroll list will cause significant memory pressure and UI jank on low-end devices.

// RegionCard — rendered for every region in the list
<View className="h-28 w-full">
  <MapView provider={PROVIDER_GOOGLE} style={{ flex: 1 }} ...>  // ❌ one per card
    <Polygon ... />
  </MapView>
</View>

Consider replacing these with a lightweight static image snapshot (MapView.takeSnapshot) generated once after download, or using a custom SVG/Canvas polygon preview instead.


🟡 Code Quality

6. OFFLINE_MAPS_DIR duplicated across two hook files

// useDownloadMapRegion.ts
const OFFLINE_MAPS_DIR = `${FileSystem.documentDirectory}offline-maps/`;

// useDeleteMapRegion.ts
const OFFLINE_MAPS_DIR = `${FileSystem.documentDirectory}offline-maps/`;  // ❌ exact duplicate

Extract to a shared constants.ts or store/offlineMaps.ts.


7. Unused first parameter in simulateDownload

async function simulateDownload(
  _regionId: string,   // ❌ unused — can be removed
  estimatedSize: number,
  ...

8. Redundant double-null guard in ViewRegionModal

<ViewRegionModal region={viewRegion} onClose={...} />
// region prop is typed OfflineMapRegion | null

// Inside ViewRegionModal:
if (!region) return null;           // guard `#1`
// ...
<Modal visible={!!region} ...>      // guard `#2` — redundant

Prefer a single pattern: either conditional rendering at the call site (viewRegion && <ViewRegionModal .../>) or the internal null guard — not both.


9. Hardcoded hex color "#14b8a6" in multiple locations

// OfflineMapsScreen.tsx (multiple occurrences)
<Icon name="plus" size={24} color="#14b8a6" />
strokeColor="#14b8a6"
fillColor="rgba(20,184,166,0.15)"

// OfflineMapsTile.tsx
<View className="... bg-teal-500">

The Tailwind class bg-teal-500 and the raw hex are used interchangeably. Define a single design token or constant.


10. Size category labels not i18n-ized

// AddRegionModal — hardcoded English strings + non-localized number formatting
region.sizeCategory === 'state-park' ? '10–50 MB' :
region.sizeCategory === 'county'     ? '50–200 MB' : '200–500 MB'

These should be added to the offlineMaps i18n namespace.


11. Silent error swallowing in download catch block

} catch {
  offlineMapsStore[id].set((prev) => ({ ...prev, status: 'failed', ... }));
  // ❌ error is never logged — impossible to debug failures
}

Add at minimum console.error or integrate with the app's error reporting.


12. handleDownload errors silently discarded

const handleDownload = async () => {
  if (!selected) return;
  onClose();
  await downloadRegion(selected, minZoom, maxZoom);  // ❌ unhandled rejection if this throws
};

Wrap in try/catch and surface an error to the user.


13. No storage quota check before download

Before calling simulateDownload, the app does not check FileSystem.getFreeDiskStorageAsync() against estimatedSize. A user with insufficient storage will get a silent failure or OS-level error.


14. ViewRegionModal uses a live online map, not offline tiles

The "View Map" modal for a completed download renders <MapView provider={PROVIDER_GOOGLE}>, which requires network access — defeating the purpose of offline maps. The PR description acknowledges the simulation, but the UI label "View Map" implies offline functionality. Consider adding a note in the UI or disabling the button when offline.


15. Missing accessibility labels on icon-only Pressable buttons

<Pressable onPress={onClose} hitSlop={8}>
  <Icon name="close" ... />   // ❌ no accessibilityLabel
</Pressable>

Add accessibilityLabel and accessibilityRole="button" to all icon-only interactive elements.


Summary Table

# Severity File Issue
1 🔴 Bug OfflineMapsScreen.tsx Hook lifecycle — cancelRefs lost on modal unmount
2 🔴 Bug OfflineMapsScreen.tsx No cancel UI despite hook exporting cancelDownload
3 🔴 Bug useDownloadMapRegion.ts Cancelled downloads leave orphaned 'idle' entries
4 🔴 Bug useDownloadMapRegion.ts No duplicate download guard
5 🟠 Perf OfflineMapsScreen.tsx Multiple MapView in ScrollView — memory/jank risk
6 🟡 Quality useDownloadMapRegion.ts / useDeleteMapRegion.ts OFFLINE_MAPS_DIR duplicated
7 🟡 Quality useDownloadMapRegion.ts Unused _regionId param
8 🟡 Quality OfflineMapsScreen.tsx Redundant double-null guard in ViewRegionModal
9 🟡 Quality OfflineMapsScreen.tsx Hardcoded "#14b8a6" hex color
10 🟡 Quality OfflineMapsScreen.tsx Size category labels not i18n-ized
11 🟡 Quality useDownloadMapRegion.ts Silent error swallowing in catch
12 🟡 Quality OfflineMapsScreen.tsx handleDownload errors unhandled
13 🟡 Quality useDownloadMapRegion.ts No storage quota check
14 🟡 UX OfflineMapsScreen.tsx "View Map" is live/online, not offline tiles
15 🟡 A11y OfflineMapsScreen.tsx Icon-only buttons missing accessibilityLabel

@andrew-bierman
Copy link
Collaborator

@copilot CodeRabbit has provided review feedback on this PR. Please address the comments and suggestions. Once you've made the necessary changes, I'll ask CodeRabbit to re-review.

- Fix hook lifecycle bug: move useDownloadMapRegion to screen level
- Fix duplicate check delimiter to use region.id + '-' prefix
- Add cancel button wired to cancelDownload in RegionCard
- cancelDownload now deletes store entry (no orphaned idle entries)
- downloadRegion now fire-and-forget; throws on validation failures
- AddRegionModal shows inline error and closes only on success
- No failed store entry for insufficient storage (throw instead)
- Replace MapView in ScrollView cards with lightweight RegionBoundsPreview
- Fix double-null guard in ViewRegionModal (single guard)
- Extract OFFLINE_MAPS_DIR, REGION_TEAL_COLOR, REGION_FILL_COLOR to constants.ts
- Add i18n keys for size categories, errors, accessibility labels
- Update viewMapOnlineNote to clarify preview vs offline map
- Add accessibilityLabel and accessibilityRole to all icon-only Pressables
- Add storage quota check with error thrown on insufficient space
- Log errors in catch blocks

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot CodeRabbit has provided review feedback on this PR. Please address the comments and suggestions. Once you've made the necessary changes, I'll ask CodeRabbit to re-review.

All 15 issues from CodeRabbit's review have been addressed in commit 81a1b79:

🔴 Critical bugs fixed:

🟠 Performance fixed:

  • Andrew firebase to andrew OSM route #5 MapView in RegionCard (inside ScrollView) replaced with a lightweight RegionBoundsPreview using pure RN Views — eliminates memory/jank from multiple native map instances

🟡 Code quality fixed:

@andrew-bierman
Copy link
Collaborator

@copilot CodeRabbit has reviewed this PR and provided feedback. Please address the review comments and push updates. Once you've made the changes, the monitor will ask CodeRabbit to re-review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an Offline Maps feature to the Expo app that lets users download predefined map regions for offline use, track download progress, and manage stored regions behind a feature flag.

Changes:

  • Introduces an offline-maps feature module (types, constants, SQLite-backed Legend State store, region presets + size estimation, and download/delete hooks).
  • Adds Offline Maps UI: home dashboard tile, Offline Maps screen with region download modal, storage banner, and region management actions.
  • Adds route + feature flag + English translations for Offline Maps.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/expo/lib/i18n/locales/en.json Adds Offline Maps translation strings.
apps/expo/features/offline-maps/utils/regions.ts Defines predefined regions and size/tile-count estimation helpers.
apps/expo/features/offline-maps/types.ts Adds types for regions and download status.
apps/expo/features/offline-maps/store/offlineMaps.ts Adds Legend State + SQLite persisted store for offline map regions.
apps/expo/features/offline-maps/screens/index.ts Re-exports the Offline Maps screen.
apps/expo/features/offline-maps/screens/OfflineMapsScreen.tsx Implements the Offline Maps screen UI (list, progress, modals, MapView preview).
apps/expo/features/offline-maps/index.ts Exports feature hooks/types.
apps/expo/features/offline-maps/hooks/useOfflineMapsStorageInfo.ts Computes aggregate storage + counts for the feature.
apps/expo/features/offline-maps/hooks/useOfflineMapRegions.ts Reads and sorts regions from the store.
apps/expo/features/offline-maps/hooks/useDownloadMapRegion.ts Starts/cancels background “download” simulation with storage checks + duplicate guard.
apps/expo/features/offline-maps/hooks/useDeleteMapRegion.ts Deletes a region from filesystem + store.
apps/expo/features/offline-maps/hooks/index.ts Barrel export for Offline Maps hooks.
apps/expo/features/offline-maps/constants.ts Centralizes offline maps directory + shared colors.
apps/expo/features/offline-maps/components/OfflineMapsTile.tsx Adds dashboard tile entrypoint for Offline Maps.
apps/expo/config.ts Enables enableOfflineMaps feature flag.
apps/expo/app/(app)/offline-maps.tsx Adds the Offline Maps route.
apps/expo/app/(app)/(tabs)/(home)/index.tsx Registers the Offline Maps tile in dashboard layout and tile metadata.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +33 to +37
<Text variant="footnote" className="text-muted-foreground mt-1">
{completedCount} {t('offlineMaps.completedRegions')}
{downloadingCount > 0
? ` · ${downloadingCount} ${t('offlineMaps.downloading', { count: downloadingCount })}`
: ''}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StorageBanner is double-counting the downloading regions: the string prepends downloadingCount and also calls t('offlineMaps.downloading', { count }) where the translation already includes {{count}}. This will render like "· 2 2 downloading". Also, completedCount can render as "1 downloaded regions" (plural).

Suggestion: either remove the leading ${downloadingCount} and let i18n handle the count (and/or add proper pluralized keys for "downloaded region(s)").

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +79
// Guard: prevent duplicate downloads for the same region
const existingEntries = Object.values(offlineMapsStore.get());
const alreadyActive = existingEntries.some(
(entry) => entry.id.startsWith(`${region.id}-`) && entry.status === 'downloading',
);
if (alreadyActive) return;

Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate-download guard silently returns when a matching region is already downloading. In AddRegionModal, handleDownload treats a resolved onStartDownload as success and closes the modal, so the user gets no feedback and no download starts.

Suggestion: make downloadRegion signal this case (e.g., throw a specific error or return a boolean) so the modal can keep open and display a message (you already added offlineMaps.duplicateDownloadTitle/message strings that currently aren’t used).

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +31
for (let tick = 1; tick <= TOTAL_TICKS; tick++) {
if (signal.cancelled) return;
await new Promise<void>((resolve) => setTimeout(resolve, TICK_MS));
const progress = Math.round((tick / TOTAL_TICKS) * 100);
const downloadedSize = Math.round((tick / TOTAL_TICKS) * estimatedSize);
offlineMapsStore[id].set((prev) => ({
...prev,
progress,
downloadedSize,
status: 'downloading',
updatedAt: new Date().toISOString(),
}));
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runDownloadInBackground only checks signal.cancelled before the sleep. If cancelDownload is called after that check but before the store update, the next tick will still write progress and can re-create the store entry after it was deleted, leaving a cancelled region visible again (and potentially persisted).

Suggestion: add a second if (signal.cancelled) return; immediately after the await setTimeout(...) (or before writing to offlineMapsStore[id]) to prevent any post-cancel writes.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +11
/** Returns total storage used by all completed offline map regions */
export function useOfflineMapsStorageInfo() {
return use$(() => {
const regions = Object.values(offlineMapsStore.get());
const totalSize = regions.reduce((sum, r) => sum + r.downloadedSize, 0);
const completedCount = regions.filter((r) => r.status === 'completed').length;
const downloadingCount = regions.filter((r) => r.status === 'downloading').length;
return { totalSize, completedCount, downloadingCount, totalCount: regions.length };
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says this hook returns storage used by completed regions, but totalSize is computed by summing downloadedSize for all regions (including downloading/failed). Either filter to status === 'completed' when computing totalSize, or update the comment (and any UI labels) to reflect that partial downloads are included.

Copilot uses AI. Check for mistakes.
Comment on lines +937 to +940
"duplicateDownloadTitle": "Already Downloading",
"duplicateDownloadMessage": "This region is already being downloaded.",
"insufficientStorageTitle": "Not Enough Storage",
"insufficientStorageMessage": "There is not enough free space on your device to download this region.",
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

offlineMaps.duplicateDownloadTitle, offlineMaps.duplicateDownloadMessage, and offlineMaps.insufficientStorageTitle are added but not referenced anywhere in the code (search in apps/expo shows no usages). Either wire these into the UI (e.g., Alert/toast) or remove them to avoid dead translation strings.

Copilot uses AI. Check for mistakes.
@andrew-bierman
Copy link
Collaborator

@copilot The check-types CI check is failing on this PR. Please fix the TypeScript type errors.

@andrew-bierman
Copy link
Collaborator

@copilot In addition to fixing the check-types CI failure, please also address the 5 CodeRabbit inline review comments.

- Fix TS2322: replace invalid 'download' icon with 'inbox-arrow-down' in OfflineMapsTile
- Fix TS2532: add @ts-ignore for Legend-State Proxy pattern in useDeleteMapRegion and useDownloadMapRegion
- Fix TS2322: wrap Icon with View for absolute positioning (style prop not on Icon)
- Fix StorageBanner double-count: use t('completedRegions', {count}) with _one/_other plural keys
- Fix duplicate download: throw ERR_DUPLICATE_DOWNLOAD instead of silently returning
- Fix runDownloadInBackground: add second signal.cancelled check after setTimeout sleep
- Fix useOfflineMapsStorageInfo: totalSize now sums only completed regions
- Extract ERR_DUPLICATE_DOWNLOAD and ERR_INSUFFICIENT_STORAGE to constants.ts
- Wire duplicateDownloadMessage into AddRegionModal error handler
- Remove unused duplicateDownloadTitle and insufficientStorageTitle i18n keys

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot The check-types CI check is failing on this PR. Please fix the TypeScript type errors.

Fixed in commit fd45f03. The three TypeScript errors from CI were:

  1. OfflineMapsTile.tsx"download" isn't in the @roninoss/icons type union; replaced with "inbox-arrow-down" (already used elsewhere in the codebase)
  2. useDeleteMapRegion.ts / useDownloadMapRegion.tsofflineMapsStore[id] is typed as ... | undefined because the store is observable<Record<string, OfflineMapRegion>>. Added // @ts-ignore: Safe because Legend-State uses Proxy following the existing pattern in useUpdatePackTemplateItem.ts
  3. OfflineMapsScreen.tsx<Icon> doesn't accept a style prop; wrapped the absolutely-positioned check icon in <View style={{ position: 'absolute', ... }}> instead

Also addressed the Copilot PR reviewer comments in the same commit: fixed StorageBanner double-count (i18n plural keys), downloadRegion now throws ERR_DUPLICATE_DOWNLOAD instead of silently returning, added a second cancellation check in runDownloadInBackground to prevent post-cancel store writes, totalSize now only sums completed regions, and wired the duplicate download error message into the modal.

@copilot CodeRabbit has reviewed this PR and provided feedback. Please address the review comments and push updates. Once you've made the changes, the monitor will ask CodeRabbit to re-review.

All Copilot PR reviewer feedback addressed in commit fd45f03:

  • StorageBanner double-count — removed the bare ${downloadingCount} prefix; now uses t('offlineMaps.downloading', { count }) alone. Completed region count uses new completedRegions_one/completedRegions_other plural keys ("1 downloaded region" / "N downloaded regions") instead of manual interpolation
  • downloadRegion silent duplicate guard — now throws ERR_DUPLICATE_DOWNLOAD; AddRegionModal catch block keeps the modal open and shows t('offlineMaps.duplicateDownloadMessage')
  • Post-cancel store write — added a second if (signal.cancelled) return immediately after the setTimeout sleep in runDownloadInBackground
  • useOfflineMapsStorageInfo totalSize — now sums downloadedSize only for completed regions
  • Dead i18n stringsduplicateDownloadTitle and insufficientStorageTitle removed; duplicateDownloadMessage is now actively used in the error handler
  • Error type safety — extracted ERR_DUPLICATE_DOWNLOAD and ERR_INSUFFICIENT_STORAGE constants to constants.ts; error detection uses these constants instead of raw magic strings

Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot In addition to fixing the check-types CI failure, please also address the 5 CodeRabbit inline review comments.

Both the check-types CI failure and all 5 CodeRabbit inline comments were addressed together in commit fd45f03:

TypeScript fixes:

  • "download" icon → "inbox-arrow-down" (valid in the icon type union)
  • @ts-ignore added for Legend-State Proxy dynamic property access (same pattern as useUpdatePackTemplateItem.ts)
  • <Icon style=...> → wrapped in <View style=...> (Icon doesn't accept style)

5 CodeRabbit comments:

  1. StorageBanner double-count — removed bare ${downloadingCount} prefix; uses t('offlineMaps.downloading', { count }) alone; completed count uses new completedRegions_one/completedRegions_other plural i18n keys
  2. Silent duplicate guarddownloadRegion now throws ERR_DUPLICATE_DOWNLOAD; modal stays open and shows t('offlineMaps.duplicateDownloadMessage')
  3. Post-cancel store write — second if (signal.cancelled) return added after the setTimeout sleep in runDownloadInBackground
  4. totalSize over-countinguseOfflineMapsStorageInfo now sums only completed regions' downloadedSize
  5. Dead i18n stringsduplicateDownloadTitle/insufficientStorageTitle removed; duplicateDownloadMessage and insufficientStorageMessage wired into the error handler; ERR_DUPLICATE_DOWNLOAD/ERR_INSUFFICIENT_STORAGE constants extracted to constants.ts

@andrew-bierman
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Warning

Rate limit exceeded

@andrew-bierman has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 169a0200-5836-4ebd-960a-a1f22686acbd

📥 Commits

Reviewing files that changed from the base of the PR and between 3f94763 and fd45f03.

📒 Files selected for processing (17)
  • apps/expo/app/(app)/(tabs)/(home)/index.tsx
  • apps/expo/app/(app)/offline-maps.tsx
  • apps/expo/config.ts
  • apps/expo/features/offline-maps/components/OfflineMapsTile.tsx
  • apps/expo/features/offline-maps/constants.ts
  • apps/expo/features/offline-maps/hooks/index.ts
  • apps/expo/features/offline-maps/hooks/useDeleteMapRegion.ts
  • apps/expo/features/offline-maps/hooks/useDownloadMapRegion.ts
  • apps/expo/features/offline-maps/hooks/useOfflineMapRegions.ts
  • apps/expo/features/offline-maps/hooks/useOfflineMapsStorageInfo.ts
  • apps/expo/features/offline-maps/index.ts
  • apps/expo/features/offline-maps/screens/OfflineMapsScreen.tsx
  • apps/expo/features/offline-maps/screens/index.ts
  • apps/expo/features/offline-maps/store/offlineMaps.ts
  • apps/expo/features/offline-maps/types.ts
  • apps/expo/features/offline-maps/utils/regions.ts
  • apps/expo/lib/i18n/locales/en.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/feature-offline-map-downloads

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Offline Map Downloads by Region

3 participants