Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7a22198
feat: add alarm-scheduler native module skeleton and prebuild config
HoweiAY Aug 3, 2026
c41e1d2
feat: add scheduled alarms registry (schema + Zustand store)
HoweiAY Aug 3, 2026
aca99ff
feat: add scheduling orchestration, AlarmDisplay, and deep-link routing
HoweiAY Aug 3, 2026
39d5778
feat: implement Android AlarmScheduler, sound service, and boot re-ar…
HoweiAY Aug 3, 2026
9540678
feat: implement iOS AlarmScheduler (degraded) and finalize reconcilia…
HoweiAY Aug 3, 2026
dc373e1
feat: block duplicate-time alarms with a user message
HoweiAY Aug 3, 2026
273bc29
chore: include Gradle build artifacts in native/.gitignore
HoweiAY Aug 3, 2026
0f49b1b
style: show 'off' button only when the alarm has no tasks
HoweiAY Aug 3, 2026
b3b9823
build: add DB migration squashing script
HoweiAY Aug 4, 2026
d819fa5
docs: update AGENTS.md
HoweiAY Aug 4, 2026
0ae980b
fix: fix alarm task difficulty and round count mismatch
HoweiAY Aug 4, 2026
173eeba
fix: duplicate home screen on navigation stack after alarm dismissal
HoweiAY Aug 4, 2026
78e9f58
feat: shorten alarm countdown interval to 1 second
HoweiAY Aug 4, 2026
68d1503
feat: add push notification for alarms
HoweiAY Aug 4, 2026
5c339f1
feat: track active alarms and block back navigation on alarm screens
HoweiAY Aug 5, 2026
563f789
build: add script for building native module
HoweiAY Aug 5, 2026
40826d0
docs: update AGENTS.md
HoweiAY Aug 5, 2026
19d7838
refactor: simplify data persistence layer
HoweiAY Aug 5, 2026
68d7336
chore: remove squash migration script
HoweiAY Aug 5, 2026
10f5daa
docs: update AGENTS.md
HoweiAY Aug 5, 2026
1930a59
chore: move test files to isolated directory
HoweiAY Aug 5, 2026
891cc86
fix: set loaded=true on alarmRegistrationsStore init failure, log ini…
HoweiAY Aug 6, 2026
4089f29
fix: update registry records inline in upsert/remove/removeForAlarm
HoweiAY Aug 6, 2026
485286a
fix: cancel snooze identifiers for disabled alarms in setAlarm and re…
HoweiAY Aug 6, 2026
4ff8fb5
fix: guard forceDismissFiring by active alarm ID in setAlarm
HoweiAY Aug 6, 2026
f7b181a
fix: validate deep link path and alarm existence in handleAlarmUrl
HoweiAY Aug 6, 2026
3441456
fix: deduplicate notification alarm activation and pass snapshot to f…
HoweiAY Aug 6, 2026
90ab616
fix: use Promise.allSettled so dismissal phases are independent
HoweiAY Aug 6, 2026
83ba351
feat: extract parseTaskParams helper for validated task parameter par…
HoweiAY Aug 6, 2026
2205a5b
fix: compare unrounded now in weekly trigger to avoid missing same-mi…
HoweiAY Aug 6, 2026
9e7035c
fix: remove getAlarmScheduler from type-only re-export block
HoweiAY Aug 6, 2026
20acb22
style: auto-fix prettier formatting in _layout.tsx
HoweiAY Aug 6, 2026
4a5f764
fix: use effectiveSnapshot instead of snapshot! in alarm screen handlers
HoweiAY Aug 6, 2026
72d392b
fix: missing alarm sound and notification after handling replacement …
HoweiAY Aug 7, 2026
7e92baf
refactor: reject non-finite task round counts
HoweiAY Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Always read the relevant `docs/` section before implementing a subsystem; the do
- **Navigation:** `expo-router` (file-based). Route groups `app/(main)/` and `app/(alarm)/`, each with `_layout.tsx`.
- **State:** Zustand (+ immer). One shared `alarmStore`, plus per-screen `homeStore` / `createAlarmStore` mirroring the Kotlin `ViewModel`/`StateFlow` contracts.
- **Persistence:** `expo-sqlite` via Drizzle ORM (`drizzle-orm/expo-sqlite`); drizzle-kit migrations in `drizzle/` — after editing `src/data/schema.ts` run `npm run db:generate` and include the generated artifacts when committing the schema change is requested. Store `days` as **JSON** (Drizzle `json` column mode), never CSV. `babel.config.js` exists only for `babel-plugin-inline-import` (`.sql` inlining for Drizzle migrations) and `metro.config.js` adds `sql` to `sourceExts`; do not re-add decorator plugins.
- **Native module build:** Run `npm run build:native` (`scripts/build-native-module.js`) to compile the custom `alarm-scheduler` Expo module at `native/`. Accepts an optional platform argument (`android`, `ios`, or `all` — default `all`). The script first runs `expo prebuild --platform <p> --no-install` if the native project directories (`android/`, `ios/`) don't exist. For **Android**, it invokes `./gradlew :expo.modules.alarmscheduler:assembleRelease` in the generated `android/` directory, compiling only the alarm-scheduler library (not the full app). For **iOS**, it runs `pod install` in the generated `ios/` directory to build the CocoaPods-based module. Run this after making changes to any file under `native/android/` (Kotlin sources, `build.gradle`, `AndroidManifest.xml`) or `native/ios/` (Swift sources, `AlarmScheduler.podspec`) to verify the module compiles before a full `expo run:android`/`expo run:ios`. The `native/` module is auto-linked via `expo.autolinking.nativeModulesDir: "./native"` in `package.json` and its config plugin (`native/app.plugin.js`) is registered in `app.json`.
- **Alarm scheduling:** custom Expo native module for exact, wake-up, recurring alarms (iOS has no exact alarms — degraded UX is expected and must be documented). Complement with `expo-notifications` + `expo-task-manager`.
- **Sound:** `expo-audio` (or `expo-av`) looping on the alarm audio category; override silent/DND where the platform allows. Keep playback alive in the background while ringing.
- **Audio picking:** `expo-document-picker` + `expo-file-system` copy to sandbox (fixes the persisted-URI bug from doc 06 §2.3).
Expand Down
4 changes: 3 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
]
}
],
"expo-sqlite"
"expo-sqlite",
"expo-notifications",
"./native/app.plugin.js"
],
"experiments": {
"typedRoutes": true,
Expand Down
10 changes: 10 additions & 0 deletions drizzle/0001_equal_jetstream.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE `scheduled_alarms` (
`id` text PRIMARY KEY NOT NULL,
`alarm_id` text NOT NULL,
`weekday` integer,
`type` text NOT NULL,
`trigger_at` integer NOT NULL,
`payload` text NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
6 changes: 6 additions & 0 deletions drizzle/0002_bent_zemo.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE `active_alarm` (
`id` text PRIMARY KEY NOT NULL,
`payload` text NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
15 changes: 15 additions & 0 deletions drizzle/0003_misty_moon.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE `alarm_registrations` (
`alarm_id` text NOT NULL,
`type` text NOT NULL,
PRIMARY KEY (`alarm_id`, `type`)
);

INSERT INTO `alarm_registrations` (`alarm_id`, `type`)
SELECT DISTINCT `alarm_id`, 'weekly' FROM `scheduled_alarms`
WHERE `type` = 'weekly';

INSERT INTO `alarm_registrations` (`alarm_id`, `type`)
SELECT DISTINCT `alarm_id`, 'snooze' FROM `scheduled_alarms`
WHERE `type` = 'snooze';

DROP TABLE `scheduled_alarms`;
178 changes: 178 additions & 0 deletions drizzle/meta/0001_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
{
"version": "6",
"dialect": "sqlite",
"id": "a6f8aeec-c7aa-4dd2-be62-bda2fce7860d",
"prevId": "4b78312f-618e-4fa6-82ef-683351061594",
"tables": {
"alarms": {
"name": "alarms",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"days": {
"name": "days",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hour": {
"name": "hour",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"minute": {
"name": "minute",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"task": {
"name": "task",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"rounds": {
"name": "rounds",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"difficulty": {
"name": "difficulty",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sound": {
"name": "sound",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_snooze": {
"name": "is_snooze",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_enabled": {
"name": "is_enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"scheduled_alarms": {
"name": "scheduled_alarms",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"alarm_id": {
"name": "alarm_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"weekday": {
"name": "weekday",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"trigger_at": {
"name": "trigger_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"payload": {
"name": "payload",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
Loading