A React Native app for Muslim prayer times in London, UK.
Web: athan.uk | iOS: ios.athan.uk | Android: athan.uk
- Six daily prayers plus five extra times (Midnight, Last Third, Suhoor, Duha, Friday Istijaba) with a live countdown to the next prayer.
- The schedule rolls over after the last prayer of the day instead of midnight. Tomorrow sits one swipe away.
- Fully offline after the first sync. The whole year is cached in MMKV, and an unreadable time shows
--:--, never a guess. - Notifications: selectable athan audio (32 public recordings, each credited in the picker), two reminder slots per prayer, a 64-request scheduling budget and deterministic IDs so alarms never double-fire.
- Home screen and Lock Screen widgets on iOS and Android: Light and Dark variants, self-refreshing, minute-ceil countdown labels. Widgets need a development or production build (iOS 16.4+), not Expo Go.
- Qibla compass on a flat world map: a north-locked dial that taps when you line up with Makkah, drawing only headings the phone has vouched for.
- Settings: large overlay font, Hijri date, Arabic prayer names, seconds, seasonal decorations, countdown bar, color picker.
- Folding iPhone (Duo) support: held tall on the open inner display the app renders top-down across both panels with no letterbox, and there is no wide mode - the wide hold keeps the system's centered portrait frame.
Prayer times come from London Prayer Times. Reminder audio is recorded in-house. The Audacity projects for every athan and reminder sound are on the releases page.
- On some Android phones (ColorOS / OxygenOS), at-time alerts can arrive about a minute late. The fix is built and proven on test devices; it ships with the next store release.
- On a folding iPhone held wide, a process launched while the device is folded keeps the system's centered portrait frame after unfolding until its next relaunch. Held tall, the same process unfolds straight onto the full tall canvas.
- Store release carrying the exact-alarm fix for ColorOS / OxygenOS phones (waits on the SDK 58 stable re-pin)
Every selectable athan sound comes from a public recording:
The 32 recordings
- Expo SDK 58 (stable), React Native 0.88, React 19
- TypeScript, strict mode
- Expo Router, Jotai, MMKV, Reanimated 4
- date-fns and date-fns-tz, with every date in the London timezone
- Biome for lint and format, Jest at 100% coverage, Pino for logging
- Native modules in
modules/: qibla heading (Google Fused Orientation on Android, Core Location on iOS), TLS 1.3 for Android 9 and below, Android widget self-refresh
app/ Expo Router entry points and navigation
components/ UI components (prayer, countdown, overlay, sheets, modals)
hooks/ useQibla, useCountdown, useSchedule
stores/ Jotai atoms (schedule, notifications, countdown, sync, widgets)
shared/ utilities: constants, prayer, time, notifications, qibla, whatsNew
widgets/ iOS widget layouts and the Android Glance layout
modules/ native Expo modules: qiblaheading, tls13, widgetrefresh
device/ platform code: notifications, listeners, updates, tasks
api/ prayer times client; mocks/ holds dev and test data
e2e/ Maestro flows, device atlas, frame audit, baseline compare
board/ the local ticket board: the stack, its data and its snapshot
patches/ holds patch-package patches. ai/ holds the agent guide. android/ and ios/ are generated by npx expo prebuild and stay git-ignored.
This repository runs its own work on a local board: a self-hosted It's a Plan instance in
board/. A ticket is an issue on that board, and its column is its state. The
board holds the plan, the steps, the relations and the history.
| Command | What it does |
|---|---|
/athan-next |
The lead reads the board, picks the runnable ticket, and routes it: plan, execute, audit, Done |
The workers are agent files in .opencode/agents/ (athan-lane, athan-executor, athan-reviewer, athan-plan-griller) running as subagents with enforced permissions: the executor cannot push, the reviewers cannot edit. Every phase writes its state to the board as it lands, so any session can be closed and resumed by running the command again in a fresh one. The workflow is self-contained: cloning this repository brings the commands, the skills, the workers and the board's compose file with it.
cd board && docker compose --env-file env up -dThe board is at http://localhost:3001 and the API at http://localhost:3000. Six containers
run, two are one-shot setup steps, and two are listed but never start because nothing here uses
webhooks, notifications or Telegram. board/README.md documents all of it.
board/data/ holds the live database and attachments and is ignored by git. board/snapshot/board.sql
is the committed copy, and it is what makes the board recoverable:
board/snapshot.sh # write snapshot/board.sql at the end of a run
board/restore.sh # rebuild the board from it, on any machineReading the board: a ticket is one unit of work, a step is a plan step. The columns are
Backlog, Deferred, Planning, Ready, In progress, Blocked, Audit, Awaiting Owner, Upstream and
Done.
The Tickets view shows the ticket number, its priority, its labels, its lane and how long it has
sat where it is. Nothing is edited through the board by hand: agents write it through MCP.
Prerequisites: Node.js 24+, Yarn, Xcode with an iOS simulator, Android Studio. The workflow commands need OpenCode or any agent that reads .opencode/ and .agents/.
| Command | Purpose |
|---|---|
yarn reset |
clean, install, set up husky, start Metro |
yarn ios / yarn android |
build and run on the simulator or emulator |
yarn validate |
tsc, Biome and the full Jest suite with coverage: the before-done gate |
yarn format |
Biome format and lint fixes |
yarn test:tz |
the Jest suite across four timezones, for anything touching dates |
yarn check:device <serial> |
audit scheduled alarms on a connected Android phone |
build-prod.zsh / build-mock.zsh |
local device builds carrying env vars and prebuild order |
maestro test e2e/flows/<flow>.yaml |
run a UI flow (needs export PATH="$HOME/.maestro/bin:$PATH") |
Add a dependency with npx expo install <package>, never with --fix. This project intentionally runs newer jest and typescript than Expo pins.
The committed config carries placeholders only: app.json names com.example.athan and a matching
widget app group, and .env.example repeats them. The published store ids are injected at build
time and never committed, because a clone has to be able to deploy this as its own app:
| Variable | What it replaces | Default |
|---|---|---|
EXPO_APP_ID |
the Android package | com.example.athan |
EXPO_BUNDLE_ID |
the iOS bundle id | com.example.athan |
EXPO_APP_GROUP |
the widget app group | group.com.example.athan |
EXPO_EAS_PROJECT_ID |
extra.eas.projectId |
none |
EXPO_PUBLIC_ANDROID_PACKAGE / EXPO_PUBLIC_IOS_BUNDLE_ID |
the ids the app reads at runtime | the same placeholders |
$HOME/athan-gitree/bin/build-prod.zsh and build-mock.zsh export the live values before
npx expo prebuild, so a device build writes the published ids into the git-ignored native trees
and badges the package it always has. A local iOS build does the same by exporting them in the
shell that runs npx expo prebuild -p ios. Maestro flows take APP_ID (-e APP_ID=<package>),
and the e2e scripts fall back to the placeholder.
scripts/privacy-manifest.json holds, base64-encoded, the terms this repository may never carry:
the owner's names, the home-path username and tool-session scratchpad paths. Three gates read it,
all deterministic: .husky/pre-commit refuses a staged line or a commit identity that carries a
term; yarn validate includes a suite test that scans every tracked file, and the pre-push hook
runs yarn validate; board/snapshot.sh refuses to write a dump that carries one. AGENTS.md
holds the placeholder vocabulary the shapes use for device and location identifiers.
- Jest runs two projects: logic suites (
.ts) and component suites (.tsx) with React Native Testing Library. - Coverage holds at 100%, enforced by global thresholds on statements, branches, functions and lines. A change covers every line it touches, red before green.
- Tests never read the real clock. Time is mocked with
jest.useFakeTimers({ now }). - Date work runs
yarn test:tzacross the timezone matrix. e2e/README.mddocuments the Maestro flows, the measurement scripts and their gotchas.
Every store release, edit WHATS_NEW in shared/whatsNew.ts:
- Stamp the items shipping in this release with the release version: new abilities, removals, behavior changes users notice. Park a draft at
version: nullwhile writing it - the contract suite stays red until it is stamped or removed, which is the ship gate. Item stamps no longer cap or gate what shows: the modal carries the newest 20 archived items (owner orders 2026-10-20 and 2026-10-21), and the archive prunes oldest-first only past 100 entries. - Bump
WHATS_NEW.versionto the store version being submitted. The modal shows nothing unless it equals the installed version. - Ship silent by leaving
WHATS_NEW.versionunmoved, or settingWHATS_NEWto null.
The debut modal (2.3.0) opens with every feature the app has ever shipped, because the modal itself is new to the store.
A contract test (shared/__tests__/whatsNew.test.ts) guards the shape. EXPO_PUBLIC_WHATS_NEW_PREVIEW=1 in .env forces the modal on cold launch in dev builds.
- AGENTS.md: the full development protocol.
- board/README.md: the board, its containers, and how to restore it.