{"_id":"@amritk/lynx-notifications","_rev":"4-86d93aadd67a02f4ac48327bb949f091","name":"@amritk/lynx-notifications","dist-tags":{"latest":"0.2.2"},"versions":{"0.1.0":{"name":"@amritk/lynx-notifications","version":"0.1.0","keywords":["lynx","notifications","push-notifications","apns","fcm","native-module","typescript","mjst"],"author":{"name":"amritk"},"license":"MIT","_id":"@amritk/lynx-notifications@0.1.0","maintainers":[{"name":"amritk","email":"amrit+spam@hockey-community.com"}],"homepage":"https://github.com/amritk/mini/tree/main/packages/lynx-notifications#readme","bugs":{"url":"https://github.com/amritk/mini/issues"},"dist":{"shasum":"db6c46e5c7e6d72974ce820c6532f74a4f718311","tarball":"https://registry.npmjs.org/@amritk/lynx-notifications/-/lynx-notifications-0.1.0.tgz","fileCount":43,"integrity":"sha512-8WHo6sQ+jgflmwpDtrOBmPU6lHR4S1ZCGR6S0scEscEBP4OhSRRqZly7Gtd/cR2G4lSWSMbOzsAo0B2WdOujPg==","signatures":[{"sig":"MEUCIQCt8YQBIO7mcZRk1uUwlCaVl3a/6smKgmYre8iiSRKHfwIgcGFdPWP7jI/xmguVdMdIItJByzo7KmhF2wlp1mRh06I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":117951},"type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js","development":"./src/index.ts"},"./testing":{"types":"./dist/testing/index.d.ts","import":"./dist/testing/index.js","default":"./dist/testing/index.js","development":"./src/testing/index.ts"},"./package.json":"./package.json","./lynx.lib.json":"./lynx.lib.json"},"gitHead":"aea9072d4a679858c41a7d4eaf1f90f0d153798f","scripts":{"test":"NODE_ENV=production vitest run --root ../.. packages/lynx-notifications/","build":"tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs","types:check":"tsgo -p . --noEmit"},"_npmUser":{"name":"amritk","email":"amrit+spam@hockey-community.com"},"repository":{"url":"git+https://github.com/amritk/mini.git","type":"git","directory":"packages/lynx-notifications"},"_npmVersion":"11.16.0","description":"Local and remote push notifications for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.","directories":{},"sideEffects":false,"_nodeVersion":"26.3.0","dependencies":{"@amritk/mini-lynx-native":"workspace:*"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5"},"peerDependencies":{"typescript":"^5"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lynx-notifications_0.1.0_1785895810624_0.26394272294251353","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@amritk/lynx-notifications","version":"0.2.0","keywords":["lynx","notifications","push-notifications","apns","fcm","native-module","typescript","mjst"],"author":{"name":"amritk"},"license":"MIT","_id":"@amritk/lynx-notifications@0.2.0","maintainers":[{"name":"amritk","email":"amrit+spam@hockey-community.com"}],"homepage":"https://github.com/amritk/mini/tree/main/packages/lynx-notifications#readme","bugs":{"url":"https://github.com/amritk/mini/issues"},"dist":{"shasum":"987af541a935edb78bb80096bb14c27ce5f4de3d","tarball":"https://registry.npmjs.org/@amritk/lynx-notifications/-/lynx-notifications-0.2.0.tgz","fileCount":82,"integrity":"sha512-WT+BKVubCme8DW4tMK+1CshhUiAHXBBkHoYgETaCzohfKc+QrK5zl5zVNskJvDNw0c3J3yAAqE9TfE/TakbMig==","signatures":[{"sig":"MEQCIGkq/iKX0Kb5GkeGwb9ud5DJDVZmftcUeaZLq+X5IRLfAiAlQ6QDiKJgM5oUq128IPgkXAvUuO/ZU14nP3VgG4Q4sg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@amritk%2flynx-notifications@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":156265},"type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./testing":{"types":"./dist/testing/index.d.ts","import":"./dist/testing/index.js","default":"./dist/testing/index.js"},"./package.json":"./package.json","./lynx.lib.json":"./lynx.lib.json"},"gitHead":"9c6b6bc96beba6a5f106f5e1bf5d7d0ab3b762c1","scripts":{"test":"NODE_ENV=production vitest run --root ../.. packages/lynx-notifications/","build":"tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs","types:check":"tsgo -p . --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b724caf1-b048-4a93-bc02-8531f1467da2"}},"repository":{"url":"git+https://github.com/amritk/mini.git","type":"git","directory":"packages/lynx-notifications"},"_npmVersion":"12.0.2","description":"Local and remote push notifications for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"@amritk/mini-lynx-native":"0.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5"},"peerDependencies":{"typescript":"^5"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lynx-notifications_0.2.0_1785905545858_0.19545873150226312","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@amritk/lynx-notifications","version":"0.2.1","keywords":["lynx","notifications","push-notifications","apns","fcm","native-module","typescript","mjst"],"author":{"name":"amritk"},"license":"MIT","_id":"@amritk/lynx-notifications@0.2.1","maintainers":[{"name":"amritk","email":"amrit+spam@hockey-community.com"}],"homepage":"https://github.com/amritk/mini/tree/main/packages/lynx-notifications#readme","bugs":{"url":"https://github.com/amritk/mini/issues"},"dist":{"shasum":"03acd26c8fea35533d7a9a834243161ce3a38fe7","tarball":"https://registry.npmjs.org/@amritk/lynx-notifications/-/lynx-notifications-0.2.1.tgz","fileCount":82,"integrity":"sha512-Ak+8h+r0A8u2UjhHnXJYSwGu2ViBrUUBGj1Wb6y4kaqUUMUAwQHDMiwiWStrpDVHi7GOuqqJOEo27iwQ6qvDtw==","signatures":[{"sig":"MEUCIQDDCH8+CEGx1/FVykhd6TN4JyVh3TUGci+m+C+eh8pzlwIgWSAHke1esl9K8fRisO/idw1lD9atkSwChNvXYm9pcds=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@amritk%2flynx-notifications@0.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":156331},"type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./testing":{"types":"./dist/testing/index.d.ts","import":"./dist/testing/index.js","default":"./dist/testing/index.js"},"./package.json":"./package.json","./lynx.lib.json":"./lynx.lib.json"},"gitHead":"9c78706160a9e76fc5a6e31ef33a1b7c5fdfeb03","scripts":{"test":"NODE_ENV=production vitest run --root ../.. packages/lynx-notifications/","build":"tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs","types:check":"tsgo -p . --noEmit","prepublishOnly":"node ../../scripts/check-publishable.mjs"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b724caf1-b048-4a93-bc02-8531f1467da2"}},"repository":{"url":"git+https://github.com/amritk/mini.git","type":"git","directory":"packages/lynx-notifications"},"_npmVersion":"12.0.2","description":"Local and remote push notifications for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"@amritk/mini-lynx-native":"0.2.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5"},"peerDependencies":{"typescript":"^5"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lynx-notifications_0.2.1_1785949011444_0.6767882331108033","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@amritk/lynx-notifications","version":"0.2.2","description":"Local and remote push notifications for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.","type":"module","license":"MIT","author":{"name":"amritk"},"sideEffects":false,"keywords":["lynx","notifications","push-notifications","apns","fcm","native-module","typescript","mjst"],"repository":{"type":"git","url":"git+https://github.com/amritk/mini.git","directory":"packages/lynx-notifications"},"homepage":"https://github.com/amritk/mini/tree/main/packages/lynx-notifications#readme","bugs":{"url":"https://github.com/amritk/mini/issues"},"publishConfig":{"access":"public"},"scripts":{"build":"tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs","prepublishOnly":"node ../../scripts/check-publishable.mjs","types:check":"tsgo -p . --noEmit","test":"NODE_ENV=production vitest run --root ../.. packages/lynx-notifications/"},"exports":{"./package.json":"./package.json","./lynx.lib.json":"./lynx.lib.json",".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./testing":{"types":"./dist/testing/index.d.ts","import":"./dist/testing/index.js","default":"./dist/testing/index.js"}},"dependencies":{"@amritk/mini-lynx-native":"0.2.2"},"peerDependencies":{"typescript":"^5"},"peerDependenciesMeta":{"typescript":{"optional":true}},"devDependencies":{"typescript":"^5"},"gitHead":"c2516a9f0a935e9e8e1eda316d651ac48e0ccc71","_id":"@amritk/lynx-notifications@0.2.2","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-yCrqjHNtdap4L09PII4kFaLo50KhW+RXKJSccM6K53YaYyckFsn5ViYmzKL1nrdjjRA5yBxM2oU9I9oe4Gmfvg==","shasum":"016aecc46c174b7efd1c58a42ee914a5dc380a89","tarball":"https://registry.npmjs.org/@amritk/lynx-notifications/-/lynx-notifications-0.2.2.tgz","fileCount":82,"unpackedSize":156331,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@amritk%2flynx-notifications@0.2.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFZCdicXuT0tAeUkqBoYSPZMqSVmHE1yQ3Osl+/5FnjHAiAwJEHbKSTBK4VfMeT4PyKB3I7hiKucU5h/8noitzKuEQ=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b724caf1-b048-4a93-bc02-8531f1467da2"}},"directories":{},"maintainers":[{"name":"amritk","email":"amrit+spam@hockey-community.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/lynx-notifications_0.2.2_1787351375456_0.42201941304232493"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-05T02:10:10.361Z","modified":"2026-08-21T22:29:35.911Z","0.1.0":"2026-08-05T02:10:10.789Z","0.2.0":"2026-08-05T04:52:26.017Z","0.2.1":"2026-08-05T16:56:51.784Z","0.2.2":"2026-08-21T22:29:35.622Z"},"bugs":{"url":"https://github.com/amritk/mini/issues"},"author":{"name":"amritk"},"license":"MIT","homepage":"https://github.com/amritk/mini/tree/main/packages/lynx-notifications#readme","keywords":["lynx","notifications","push-notifications","apns","fcm","native-module","typescript","mjst"],"repository":{"type":"git","url":"git+https://github.com/amritk/mini.git","directory":"packages/lynx-notifications"},"description":"Local and remote push notifications for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.","maintainers":[{"name":"amritk","email":"amrit+spam@hockey-community.com"}],"readme":"# @amritk/lynx-notifications\n\nLocal and remote push notifications for Lynx — an Android module, an iOS module,\nand a promise-shaped facade that reaches them from the main thread.\n\n- **iOS** — `UNUserNotificationCenter` for scheduling and delivery, APNs for\n  remote push.\n- **Android** — `NotificationManager` + `AlarmManager` for scheduling, Firebase\n  Cloud Messaging for remote push.\n\nTransport-agnostic on the server side: forward the device token to your backend\nand send through APNs and FCM directly, or through anything that fronts them.\n\n## Why this exists\n\nLynx ships no notifications module. Sparkling's `sparkling-notifications` is a\nreserved npm name with no implementation behind it (`0.0.1`, *\"implementation\nfollows\"*). The official answer in\n[lynx-family/lynx#67](https://github.com/lynx-family/lynx/discussions/67) is\nstill \"write native code and send it into your Lynx code\".\n\n## Install\n\n```sh\nbun add @amritk/lynx-notifications\n```\n\n`@amritk/mini-lynx-native` comes with it — it is the thread hop this package's\ncalls travel over, because `NativeModules` is background-thread-only and a\n`@amritk/mini-lynx` tree renders on the main thread.\n\n## JavaScript setup\n\nOne line in the background chunk, then use it from anywhere:\n\n```ts\n// background chunk\nimport { installNativeBridge } from '@amritk/mini-lynx-native/background'\n\ninstallNativeBridge()\n```\n\n```tsx\n// main-thread chunk\nimport {\n  createNotificationChannel,\n  getPermissionStatus,\n  onNotificationResponse,\n  requestPermission,\n  scheduleNotification,\n} from '@amritk/lynx-notifications'\n\n// At the app root, during the first render — a cold-start tap is replayed to\n// whoever subscribes first, and only once.\nonCleanup(\n  onNotificationResponse(({ notification }) => {\n    const screen = notification.data['screen']\n    if (typeof screen === 'string') router.navigate(screen)\n  }),\n)\n\n// Android needs a channel or notifications are dropped in silence. No-op on iOS.\nawait createNotificationChannel({ id: 'reminders', name: 'Reminders', importance: 'high' })\n\nif ((await getPermissionStatus()) === 'undetermined') await requestPermission()\n\nawait scheduleNotification({\n  title: 'Standup',\n  body: 'In five minutes',\n  channelId: 'reminders',\n  data: { screen: 'calendar' },\n  trigger: { type: 'timeInterval', seconds: 300 },\n})\n```\n\n## Host-app setup\n\nThe native sources are declared in `lynx.lib.json`, so Lynx's autolinking picks\nthem up. What autolinking cannot do for you is credentials and entitlements.\n\n### Android\n\nLocal notifications work with no further setup. The library's manifest\ncontributes `POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, the alarm and tap\nreceivers, and the permission-prompt activity through manifest merging.\n\nThe small icon is your app's launcher icon. If you want a dedicated one, add a\n`drawable` named for it and override in your own manifest.\n\nFor **remote push**, add Firebase to the host app — the library depends on\n`firebase-messaging` as `compileOnly`, so an app that only schedules local\nnotifications does not inherit it:\n\n```kotlin\n// app/build.gradle.kts\nplugins { id(\"com.google.gms.google-services\") }\ndependencies { implementation(\"com.google.firebase:firebase-messaging:24.0.0\") }\n```\n\nand drop your `google-services.json` into `app/`.\n\nIf your build does not run Lynx's annotation processor, register the module by\nhand:\n\n```kotlin\nLynxEnv.inst().registerModule(\n  \"MiniLynxNotificationsModule\",\n  MiniLynxNotificationsModule::class.java,\n)\n```\n\n### iOS\n\nRegister the module where you configure Lynx:\n\n```objc\n[config registerModule:MiniLynxNotificationsModule.class];\n```\n\nFor **remote push**, add the Push Notifications capability and the\n`aps-environment` entitlement, then forward APNs' two app-delegate callbacks —\niOS delivers these to the app delegate and there is no supported way for a\nlibrary to intercept them without swizzling:\n\n```objc\n#import <MiniLynxNotifications/MiniLynxNotificationsCenter.h>\n\n- (void)application:(UIApplication *)application\n    didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {\n  [MiniLynxNotificationsCenter.shared didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];\n}\n\n- (void)application:(UIApplication *)application\n    didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {\n  [MiniLynxNotificationsCenter.shared didFailToRegisterForRemoteNotificationsWithError:error];\n}\n```\n\nWithout this, local notifications work and `getDeviceToken` always resolves\n`null`.\n\n## API\n\n| Function | Returns |\n| --- | --- |\n| `getPermissionStatus()` | `'undetermined' \\| 'denied' \\| 'granted' \\| 'provisional'` |\n| `requestPermission(request?)` | the status the user chose |\n| `getDeviceToken()` | `DeviceToken \\| null` |\n| `onDeviceToken(listener)` | unsubscribe |\n| `scheduleNotification(request)` | the id it was filed under |\n| `cancelNotification(id)` / `cancelAllNotifications()` | — |\n| `getScheduledNotifications()` | what has not fired yet |\n| `onNotificationReceived(listener)` | unsubscribe (foreground arrivals) |\n| `onNotificationResponse(listener)` | unsubscribe (taps, including cold start) |\n| `getBadgeCount()` / `setBadgeCount(n)` | — |\n| `createNotificationChannel(channel)` | — (Android; no-op on iOS) |\n| `isNotificationsAvailable()` | whether the host app linked the module |\n\n## The things that actually bite\n\n- **You get one permission prompt, ever.** iOS shows it once per install;\n  Android 13+ stops after two dismissals. A request made after a refusal shows\n  nothing and reports the refusal. Check `getPermissionStatus()` first, and spend\n  the prompt at a moment the user already understands.\n- **Android drops a notification with no valid channel, silently.** Call\n  `createNotificationChannel` at startup. The module creates a default channel on\n  demand so an app that forgets still delivers, but every notification lands in\n  one bucket the user can only turn off wholesale.\n- **A channel is immutable after creation.** Android ignores every field but the\n  name and description on a channel that already exists. Get the importance right\n  first time, or use a new id.\n- **A foreground notification shows no banner, by design.** It arrives at\n  `onNotificationReceived` and the in-app treatment is yours. A system banner\n  over the screen the user is looking at is worse.\n- **The device token is not an identifier.** iOS reissues it on reinstall and\n  restore; FCM rotates it on its own schedule. Send every arrival to your\n  backend — storing the first and assuming it holds is why \"push stopped working\n  for some users\" takes weeks to find.\n- **Scheduled notifications are not exact on Android.** They use inexact alarms\n  deliberately: exact alarms need `SCHEDULE_EXACT_ALARM`, which Google Play\n  restricts to alarm clocks and calendars. Doze can delay one. Fine for a\n  reminder, wrong for a countdown.\n- **Subscribe to taps at the app root, during the first render.** A cold-start\n  tap is held by the native side and replayed once, to whoever subscribes first.\n\n## Testing\n\n`@amritk/lynx-notifications/testing` ships the native module in memory, so\nyour own screens can be tested with no device:\n\n```ts\nimport { installNativeBridge } from '@amritk/mini-lynx-native/background'\nimport { createFakeContexts, createFakeEmitter } from '@amritk/mini-lynx-native/testing'\nimport { MODULE } from '@amritk/lynx-notifications'\nimport { createFakeNotifications } from '@amritk/lynx-notifications/testing'\n\nconst contexts = createFakeContexts()\nconst emitter = createFakeEmitter()\nconst notifications = createFakeNotifications(emitter)\nsetPeerContext(contexts.mainThread)\ninstallNativeBridge({ peer: contexts.background, emitter, modules: { [MODULE]: notifications.module } })\n\nnotifications.deliver({ title: 'Order shipped' })\n```\n\n## Status, and what is actually verified\n\n| | Checked by | Runs where |\n| --- | --- | --- |\n| TypeScript facade | `bun run test` | everywhere |\n| JS ⇄ native contract (names, arities, event names) | `src/native-contract.test.ts` | everywhere |\n| Kotlin compiles and packages to an AAR | `bun run check:android` | needs an Android SDK; CI |\n| Objective-C compiles against the iOS SDK | `pod lib lint` | macOS only; not in CI, run by hand |\n| Any of it working on a device | — | **nothing** |\n\nThe last row is the one to keep in mind. Both native halves compile against the\nreal Lynx SDK, and the contract between the three implementations is pinned by a\ntest that fails when any of them drifts. None of that is the same as a\nnotification appearing on a phone: permission flows, `AlarmManager` behaviour\nunder Doze, APNs registration and FCM delivery are all things only a device can\nanswer.\n\n`bun run check:android` skips with an explanation when there is no SDK, so a\ncontributor without one is told what is being skipped rather than handed a\nfailure they cannot act on. CI passes `--require-sdk` and fails instead.\n\n## Licence\n\nMIT\n","readmeFilename":"README.md"}