countries.yaml. Adding a new country requires no app update.app/_layout.tsx mounts, wraps app in zoneStore.Provider and waktuSolatStore.ProviderrequestAllPermissions() runs once — requests foreground/background location, notifications, exact alarms, battery optimizationapp/index.tsx) renders and triggers hooksGPS → Country Detection (bundled ADM0) → Zone Resolution (fetched geojson) → Prayer Times (CDN, HH:MM) → Timezone Conversion → Display / Notifications / Widgets
useLocation() fetches GPS on app resume and date changelookupZoneByGps(lat, lng) in lib/service/zone.ts:
countries.yaml from CDN (cached 1 month) → check if country is officially supportedPolygonLookup.search() → shapeName → mapping → zone codezones/{CC}.yaml from CDN (cached 1 month) → IANA timezone for the zoneOfficialZone { zone, country, state, district, timezone, source } or CalculatedZone for unsupported countrieszoneStorefetchAndMergePrayerTimes() in lib/service/waktuSolat.ts:
waktuSolatStore[year::month::date::zone]prayer-times/{CC}/{zone}/{year}-{month}.json), convert HH:MM → epoch using zone’s IANA timezone via date-fns-tzMain screen uses:
useWaktuSolatCurrent() — today’s prayer timesuseWaktuSolatTomorrow() — tomorrow’s (swipe to view)useUpdatedZone() — current zone from GPSRegistered in lib/tasks/backgroundTasks.ts. OS triggers every ~15 minutes.
Each run:
lib/service/notifee.ts:
waktu_solat::prayer::year::month::date::zone — same ID replaces, preventing duplicatesSET_ALARM_CLOCK with allowWhileIdle: true (survives Doze mode)waktu_solat channel with AndroidImportance.HIGHlib/service/notifeeBackground.ts)lib/service/waktuSolatWidget.ts:
Promise.all| Data | Storage | TTL | Abstraction |
|---|---|---|---|
| countries.yaml | AsyncStorage | 1 month | asyncCacheStore.getCachedOrFetch |
| zones/{CC}.yaml | AsyncStorage | 1 month | asyncCacheStore.getCachedOrFetch |
| GeoJSON | documentDirectory | Indefinite (by URL) | fileCacheStore.getCachedFileOrFetch |
| Mappings | documentDirectory | Indefinite (by URL) | fileCacheStore.getCachedFileOrFetch |
| Prayer times | AsyncStorage | Trimmed daily (>24h old) | waktuSolatStore (React context) |
| Zone | AsyncStorage | Until GPS detects new zone | zoneStore (React context) |
GeoJSON/mapping invalidation: URLs in countries.yaml are datestamped. New date = new URL = cache miss → fetch. clearStaleFiles() removes old files.
Module-level Map<string, Promise<void>> in lib/hooks/waktuSolat.tsx.
Prevents race conditions when multiple hooks (today + tomorrow) request the same zone simultaneously. The second caller waits for the first fetch to complete, then reads from cache.
For zones outside officially supported countries, lib/domain/adhanCalculator.ts uses adhan-js:
lib/
├── domain/ # Pure types + business logic (no I/O)
│ ├── zone.ts # Zone types, display helpers
│ ├── prayerTime.ts # PrayerTime/WaktuSolat types, getTimeText
│ ├── adhanCalculator.ts # Country→method mapping + adhan calculation
│ └── datetime.ts # HH:MM + timezone → epoch conversion
├── data/ # Persistence (AsyncStorage, filesystem)
│ ├── dataStore.tsx # Generic React context + AsyncStorage factory
│ ├── zoneStore.ts
│ ├── waktuSolatStore.ts
│ ├── asyncCacheStore.ts # TTL-based AsyncStorage cache
│ └── fileCacheStore.ts # URL-keyed filesystem cache
├── service/ # Side effects + orchestration
│ ├── zone.ts # GPS → zone resolution (fetches geojson on demand)
│ ├── waktuSolat.ts # Prayer time fetch + merge + cache
│ ├── waktuSolatWidget.ts # Widget update orchestration
│ ├── prayerData.ts # Zone + prayer time aggregation
│ ├── notifee.ts # Notification scheduling
│ ├── notifeeBackground.ts # Notification delivery handler
│ ├── location.ts # GPS access
│ └── permissions.ts # Permission requests
├── remote/ # Network fetch
│ └── simplesolat.ts # CDN fetch (countries, zones, geojson, prayer times)
├── hooks/ # React hooks
├── widgets/ # Android home screen widget components
└── tasks/ # Background task registration