@tiledev/sdk-tile-notification
v0.1.0
Published
OneSignal push notifications for TilePacket apps — real on iOS/Android, no-op on web. Powers audience targeting for the Tile notification center.
Readme
@tiledev/sdk-tile-notification
OneSignal push notifications for TilePacket apps — real on iOS/Android, a no-op on web.
This package is the device half of Tile's notification center. The dashboard stores a per-app OneSignal App ID + REST API Key and sends through OneSignal's REST API; this package is what makes the app a subscriber of that OneSignal app, and what sets the user tags the dashboard's automations select on.
npm install @tiledev/sdk-tile-notificationreact-native-onesignal is an optional peer — install it (and run a prebuild so the
native module links) only for the platforms that need real push:
npm install react-native-onesignal@^5
npx expo install onesignal-expo-pluginWithout it the package still installs and imports cleanly; every call is simply a no-op.
Usage
import AsyncStorage from '@react-native-async-storage/async-storage';
import { onesignal } from '@tiledev/sdk-tile-notification';
// The App ID is the published app's OWN OneSignal app — not the tile platform's.
// Fetch it at boot: GET /api/runtime/onesignal-app-id?appId=<tileAppId>
await onesignal.init({
appId,
storage: AsyncStorage, // persists the re-prompt throttle
reOptInPeriodDays: 7, // don't re-ask a user who declined, for 7 days
onDeeplink: (url) => navigate(url),
logger: console,
logLevel: 'none',
});Then feed it commerce events — this is what makes audience targeting work at all, since OneSignal segments are built only from tags and outcomes:
onesignal.trackEvent('updateCartQuantity', { totalQuantity: 2 });
onesignal.trackEvent('purchase', { totalValue: 49.99 });
onesignal.trackEvent('pageView', { pageId: 'Home', storeName: 'acme' });
await onesignal.identify({ customerId, email, phone, firstName, lastName });
await onesignal.clearIdentity(); // on logoutWhat each event does
| Event | Effect | Dashboard automation it enables |
| --- | --- | --- |
| updateCartQuantity | sets cart_update to a unix timestamp, or removes it when the cart empties | Abandoned Cart |
| purchase | Purchase outcome with revenue, and clears cart_update | Order Success |
| login / signup | external_id + email/SMS subscriptions + first_name/last_name tags | New User Welcome |
| logout | detaches the device from that customer | — |
| pageView (Home only) | sets the store tag | audience segmentation |
purchase clearing cart_update is deliberate — without it a customer who converted would
stay in the abandoned-cart segment.
API
| Method | Notes |
| --- | --- |
| init(config) | Idempotent; safe on every boot. Initializes the SDK, requests permission (subject to reOptInPeriodDays) and wires the tap handler. |
| trackEvent(name, data?) | Applies the tag/outcome map above. |
| identify(identity) | customerId becomes OneSignal's external_id — what per-user sends target. |
| clearIdentity() | OneSignal.logout(). |
| getPermissionStatus() | 'granted' \| 'denied' \| 'undetermined'. Does not prompt. |
| getSubscriptionId() | This device's OneSignal id, or null. |
| isReady() | True once init() completed against a real SDK. Always false on web. |
| isNoop | True in the web build. |
./eventMap is also exported on its own — it's pure (event → tag/outcome mutations, no SDK
calls), so the mapping can be tested without a device.
Design notes
Platform split, not a mock build. provider.native.ts holds the real implementation and
provider.ts a no-op; Metro resolves ./provider per platform. react-native-onesignal
has no web build, so it must never enter a web bundle — the split is on a file, which is
unambiguous in Metro, rather than a directory index.
One build, all real. There is no mock/stub variant. A OneSignal App ID is public by design (it ships inside every client binary) and the REST API Key stays server-side, so there is nothing to withhold from a published artifact.
Zero dependencies. The SDK is required lazily inside init() and typed against a
narrow local interface, so tsc passes with nothing installed and importing the package
where the SDK is absent cannot throw. Only init() can fail, and it reports why.
No import-time side effects. sideEffects: false is honest — initialize, log level and
the tap listener all happen inside init().
Development
npm run build # clean + tsc (fails loudly; noEmitOnError)
npm test # publish gate — see below
npm run lint # tsc --noEmitnpm test enforces two invariants that a reviewer can't eyeball, and runs again from
prepublishOnly:
- The web closure never imports
react-native-onesignal. Checked at the level ofrequire/importspecifiers, not raw text — the doc comments legitimately name the package, so a plain grep gives false positives. - The tag map still produces what the dashboard selects on. A silent change here crashes nothing; it just stops campaigns matching anyone, which stays invisible until a send reaches zero devices.
See docs/ARCHITECTURE.md for how this fits the notification center end to end.
