@klyftig/missions
v0.3.1
Published
Klyftig alarm dismissal missions for Expo / React Native: registry, MissionRunner (chains), type picker + config editor, and the Math / Shake / Memory / Barcode / Typing / Squat / Steps / Photo / Maze screens. Opt-in entry @klyftig/missions/pushup: camera
Downloads
910
Maintainers
Readme
@klyftig/missions
The dismissal missions for Klyftig's alarm apps: the obstacle a user must clear
before an alarm from @klyftig/alarm-core stops. Extracted from Sleepwise's
src/missions/ (2026-09-15).
Nine built-in missions plus one opt-in (Push-ups, below), all self-contained
screens with props { config, onComplete, onStuck? }:
Math, Shake, Memory, Barcode (Sleepwise's free tier) · Typing, Squat, Steps,
Photo, Maze. nfc exists in the data model but is not registered (needs
react-native-nfc-manager + device validation). Pure game logic
(mathProblems, shakeDetection, memorySequence, squatDetection, maze)
is unit-tested; sensor/camera screens are not.
import { MissionRunner, MissionTypePicker, MissionConfigEditor, MissionGatingProvider } from '@klyftig/missions';
// Ringing screen: walk the alarm's ordered missions (Multiple Missions).
<MissionRunner missions={alarm.missions} onComplete={dismiss} />
// Edit screen: pick a type / edit its config.
<MissionGatingProvider isPremium={isPremium} requirePremium={(proceed) => requirePremium('premiumMission', proceed)}>
<MissionConfigEditor config={m.config} onChange={...} />
</MissionGatingProvider>Gating is per app. The package never imports an entitlement store:
MissionGatingProvider supplies isPremium, requirePremium(proceed) and
optionally isPremiumMission(type) (defaults to @klyftig/alarm-core's
PREMIUM_MISSIONS). Without a provider every mission is selectable — the
right default for tests, dev harnesses and apps that don't gate missions.
Adding a mission: write the screen, register it in registry.tsx, add its
MissionType + MissionConfig + default in @klyftig/alarm-core
(domain/mission.ts), give it a labels entry and its copy in strings.tsx,
and give each app a config editor branch.
Copy is per app too. Every visible and accessibility string lives in
strings.tsx (DEFAULT_MISSION_STRINGS, English). Apps pass overrides from
their own i18n through MissionStringsProvider; sections merge one level deep,
so a partial section keeps English for the keys it leaves out, and counters are
functions of the count so the app's plural rules apply:
<MissionStringsProvider strings={{
labels: { math: t('missions:labels.math') },
shake: { toGo: (n) => t('missions:shake.toGo', { count: n }) },
}}>Mission labels live only there (MissionDefinition has no label since
0.2.0). Without a provider every screen renders English.
PhotoCapture (shared by the Photo mission and reference registration) has a
torch toggle left of the shutter; pass suggestTorch to highlight it (e.g.
the app judged the last capture too dark). Strings: camera.torchOnA11y /
camera.torchOffA11y.
Push-ups (opt-in, 0.3.0)
@klyftig/missions/pushup counts full push-ups with the front camera and an
on-device pose model (MoveNet Lightning int8, 2.9 MB, shipped in assets/).
The phone stands upright on the floor beside the user; a rep = shoulders sink
to ≤ 55 % of their height above the wrists and come back, in a straight-legged
plank, in 0.5–8 s (knee push-ups don't count). It is a separate entry so apps
that don't offer it never bundle the camera / ML native modules. To offer it:
npm i react-native-vision-camera react-native-vision-camera-worklets react-native-worklets \
react-native-fast-tflite react-native-nitro-modules react-native-nitro-image// metro.config.js — the model is a bundled asset
config.resolver.assetExts.push('tflite');
// app.config.js — iOS: VisionCameraWorklets needs React-Core's header path
plugins: ['@klyftig/missions', ...]import { registerMission } from '@klyftig/missions';
import { pushupMission } from '@klyftig/missions/pushup';
registerMission(pushupMission); // once, at app start — the pickers and the runner then offer itWays out, so nobody is trapped: if the phone can't run it (no front camera,
the model won't load, frames unreadable) the mission fails open
(onComplete); if the user is out of view for 90 s it calls the host's
onStuck (MissionRunner passes it through) — the host decides what that
costs; without one it keeps waiting. Nothing is recorded. The pure parts
(stepCounter, readPose, crop maths) are exported from the same entry; the
counter's tuning and evaluation live in ~/Code/pushup-spike.
Gotchas: the Android emulator can't read camera frames (Nitro HardwareBuffer
lock fails) and the iOS simulator has no camera — test on a phone. The camera
uses orientationSource="interface" (a missing accelerometer makes 'device'
throw).
Peer deps
@klyftig/alarm-core, @klyftig/mobile-ui (design tokens),
expo-camera (Barcode/Photo), expo-sensors (Shake/Squat/Steps),
expo-haptics, @expo/vector-icons. Optional (push-ups only): the six
packages above + @expo/config-plugins (for app.plugin.js).
npm test # 108 tests
npm run typecheck
npm run pack-local