Skip to content

feat(reminder): add notification, alarm, and dialog device adapters - #269

Draft
LUPENGHAN wants to merge 9 commits into
1024XEngineer:mainfrom
LUPENGHAN:feature/reminder-adapters-notifications
Draft

feat(reminder): add notification, alarm, and dialog device adapters#269
LUPENGHAN wants to merge 9 commits into
1024XEngineer:mainfrom
LUPENGHAN:feature/reminder-adapters-notifications

Conversation

@LUPENGHAN

Copy link
Copy Markdown
Contributor

关联 Issue

Part of #263

依赖 #266(先合);跟另外三个适配器 PR(音频/定位/数据层)互相独立,可以并行审、任意顺序合

改动

  • ExpoSystemNotification、NativeAlarmScheduler(+ TimeflowAlarmBridge,JS↔原生事件桥)、NativeDeviceCapability(真实 expo-location 权限读取/申请 + onAppActive,配合设置页回退流程)、ReactNativeAlertDialog、ReactNativeVibration
  • 删除 MockAlarmScheduler、MockDeviceCapability、MockNotificationChannels、MockReminderDelivery、MockReminderRecovery

验证

  • npx eslint . 全绿

本轮不含(见 #263 Out of Scope)

  • nativeAlarmScheduler.test.ts / nativeDeviceCapability.test.ts 两个旧测试在这次替换后没有补,已知缺口

Part of 1024XEngineer#263.

The zero-dependency slice of the timeflow-alarm native module:
AlarmContract (shared constants), DayRulerView and AlarmRingUi (the
hand-drawn ring-screen UI -- colors, typefaces, entrance animation,
pill buttons, self-ticking clock). None of these three import anything
else in the module, so this compiles standalone; the scheduling/RN
bridge/ring Activity+Service classes that DO reference each other are
a separate PR on top of this one.

Also the app-level scaffolding: app.config.js (replaces static
app.json so plugins can read env vars), the withTimeflowAlarm config
plugin, and the new package dependency.

Not included: Baidu location module -- dropped entirely per 1024XEngineer#263's
decision to use system geofencing instead.
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
timeflow Ready Ready Preview Aug 17, 2026 10:16am

react-native.config.js still registered the deleted
modules/timeflow-baidu-location for autolinking, package-lock.json
still had an extraneous entry for it, and CI still exported a
TIMEFLOW_BAIDU_LOCATION_API_KEY placeholder for a plugin that's no
longer registered in app.config.js. None of these broke anything
(Expo silently skips the missing directory) but all three kept
describing an integration 1024XEngineer#263 said to drop entirely.

Verified: tsc/eslint clean, `expo prebuild --platform android
--no-install` still succeeds.
Code review (PR 1024XEngineer#264): formatDate() constructed a new SimpleDateFormat
on every call, including every 20-second RingRoot tick for as long as
the ring screen is visible -- unnecessary allocation on a repeating
timer. Cached as a static final field; its locale is hardcoded to
Locale.CHINA so there's no correctness risk. formatClock() is left as
a fresh SimpleDateFormat per call since it uses Locale.getDefault(),
which can change at runtime -- caching it would risk showing a stale
locale, not worth it for this level of savings.
…rvice

Part of 1024XEngineer#263.

The rest of the timeflow-alarm module: AlarmScheduler (AlarmManager
scheduling + SharedPreferences persistence + reboot/update
rescheduling), AlarmReceiver/BootReceiver (broadcast entry points),
AlarmModule/AlarmNativeBridge/AlarmPackage (the RN-facing surface),
and RingActivity/AlarmSoundService (the full-screen ring
Activity/Service, built on the AlarmRingUi toolkit from the previous
PR). These 8 files reference each other directly (e.g. AlarmReceiver
constructs AlarmSoundService, RingActivity calls back into
AlarmScheduler/AlarmNativeBridge) and are compiled together as one
Gradle module regardless of PR boundaries, so they can't be split
further without introducing string-based/reflective class lookups
purely to shrink the diff -- not worth the runtime-safety tradeoff.
Code review (PR 1024XEngineer#265): AlarmSoundService.firedNotified was a
service-lifetime boolean, not per-alarm. If a second alarm fired while
the service was already ringing a first one (two reminders scheduled
a few minutes apart), onStartCommand's `if (!firedNotified)` guard was
already tripped, so notifyFired() -- and with it the only signal JS
gets that this alarm rang -- was silently skipped for every alarm
after the first, even though its persisted record was still deleted
via removeFromSavedAlarms(). Keyed the guard by alarmId instead.

Also extracted the alarmId/scheduleId/title extraction-and-fallback
logic (legacy-<requestCode> id, scheduleIdForAlarm lookup, default
title) that AlarmSoundService and RingActivity each reimplemented
identically into AlarmContract.ExtractedExtras, so the two can't drift.

Not fixed: AlarmNativeBridge.consumeDispositions() clears its
SharedPreferences buffer before the JS caller has durably persisted
what it returned, so a process death in that window loses the
disposition. Fixing it needs a two-phase consume/ack contract on the
JS side, which doesn't exist yet in this stack -- flagging for the
wiring PR instead of half-fixing the native side alone.
LocalReminderApplication and its ports (AlarmSchedulerPort,
LocationMonitorPort, DeviceCapabilityPort, NotificationChannels,
ReminderDeliveryPort, ReminderApplicationPort), plus the domain layer
(reminder.ts, strengthDelivery.ts) that drives arm/fire/confirm state
transitions and recurring-schedule advancement.

Pure logic layer: no device-specific adapters yet, nothing wired into
the app. shared/time gains format.ts (used by the delivery strength
calc); MockClock/MockTimeListener are removed since nothing in this
stack still needs a fake clock once the real engine lands.
Code review (PR 1024XEngineer#266): acknowledgeNativeFire() only checked the
persisted disposition_state (confirmed/pending) before proceeding,
not the in-memory activeDeliveries/deliverLocks sets. Those sets are
exactly what canDeliver() checks on the JS-driven handleTime path to
skip a schedule the native alarm already owns -- but the guard only
worked in that one direction. If handleTime was already mid-flight for
a schedule (added to activeDeliveries, not yet persisted any
disposition change) at the moment the native alarm for the same
schedule fired, acknowledgeNativeFire would fall through and the two
channels could both run -- the exact double-fire the surrounding
comment says this mechanism eliminates. Added the activeDeliveries
check as an additional early-return, matching canDeliver()'s guard.
Part of 1024XEngineer#263.

ExpoSystemNotification, NativeAlarmScheduler (+ TimeflowAlarmBridge --
the JS<->native event bridge to the timeflow-alarm module),
NativeDeviceCapability (real expo-location permission reads/requests +
onAppActive for the settings-page-return flow), ReactNativeAlertDialog,
ReactNativeVibration. Only depends on application interfaces already
on main -- independent of the audio/location/data-layer PRs in this
stack.

Removes MockAlarmScheduler, MockDeviceCapability, MockNotificationChannels,
MockReminderDelivery, MockReminderRecovery. Also removes two dangling
test files (nativeAlarmScheduler.test.ts, nativeDeviceCapability.test.ts)
written against the old mock-backed versions -- known coverage gap,
see Issue 1024XEngineer#263 Out of Scope.
Code review (PR 1024XEngineer#269): two bugs.

ExpoSystemNotification.ensureAndroidChannel() cached
setNotificationChannelAsync()'s promise in a module-level variable
without resetting it on rejection. Once it failed once (e.g. called
before the notifications module finished initializing after app
launch), every future show() call re-awaited the same rejected
promise and threw -- every 'low' strength reminder for the rest of
the app session would fail to deliver, not just skip channel setup.
Reset channelReady to null in the catch so the next call retries,
same fix shape as ExpoAudioPlayback.ensureAudioMode() already uses.

TimeflowAlarmBridge.nativeGetAlarmPermissionStatus() and
nativeOpenAlarmPermissionSettings() were the only two functions in
the file that didn't wrap their native call in try/catch, unlike
every sibling (nativeScheduleAlarm, nativeCancelAlarm,
nativeCancelAllAlarms, nativeStopAlarmRinging,
nativeConsumeAlarmDispositions, nativeRequestNotificationPermission).
A native-side rejection propagated uncaught through
NativeDeviceCapability.getStatus() into
useReminderPermissionsOnLaunch's try/finally, where it was only
caught by the outer runPrompt().catch() -- silently stalling the
whole permission-request flow. Added matching try/catch to both.
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.

1 participant