@rootnode-app/react-native 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # @rootnode-app/react-native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [Rootnode](https://rootnode.app) for React Native / Expo apps — **one install,
4
+ one init**: crash reporting (native engine included), analytics with offline
5
+ queueing and sessions, person properties, push registration.
6
+
7
+ ```bash
8
+ npx expo install @rootnode-app/react-native
9
+ ```
10
+
11
+ > Includes a native crash engine — ship it with a new build (EAS), not an OTA
12
+ > update.
13
+
14
+ ```tsx
15
+ import { Rootnode } from '@rootnode-app/react-native';
16
+
17
+ Rootnode.init({
18
+ dsn: process.env.EXPO_PUBLIC_ROOTNODE_DSN, // crash reporting
19
+ apiKey: process.env.EXPO_PUBLIC_ROOTNODE_KEY, // analytics (rn_…)
20
+ });
21
+
22
+ export default Rootnode.wrap(App);
23
+
24
+ // analytics — queued on disk, batched, survives offline & restarts
25
+ Rootnode.screen('FundDetail'); // screen_viewed + sticky screen
26
+ Rootnode.capture('fund_viewed', { code: 'TLY' });
27
+
28
+ // identity: after sign-in (also moves the push token to this user)
29
+ await Rootnode.identify(userId, { plan: 'pro' });
30
+ await Rootnode.reset(); // on sign-out
31
+
32
+ // push — permission + Expo token + registration in one call
33
+ await Rootnode.registerForPush(userId);
34
+
35
+ // crashes are automatic; manual reports:
36
+ Rootnode.captureException(err, { route: '/funds/TLY' });
37
+ ```
38
+
39
+ ### What you get automatically
40
+
41
+ - **Sessions**: `session_id` rotates after 30 min in background;
42
+ `app_opened` / `app_backgrounded` emitted; flush on background.
43
+ - **Context** on every event: platform, OS/app version, device timezone,
44
+ `update_id` when expo-updates is present.
45
+ - **Offline-safe queue**: persisted via AsyncStorage or expo-file-system when
46
+ available (memory otherwise), failed sends retry, capped at 1000.
47
+ - **Crash-proof contract**: no SDK error ever reaches your app; missing
48
+ native modules degrade features to no-ops.
49
+
50
+ Delivery credentials for push stay in **your** Expo project (expo.dev —
51
+ APNs automatic, Android needs a one-time FCM service-account upload). Full
52
+ guides: [integration](https://rootnode.app/docs/integration.md) ·
53
+ [push](https://rootnode.app/docs/push.md).
54
+
55
+ MIT © Doğan Aktı
package/dist/core.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Platform-free core of the React Native SDK: queue + batching + retry,
3
+ * persisted across restarts, session rotation, anonymous→user identity.
4
+ * Everything platform-ish (AppState, expo modules, Sentry) is injected by
5
+ * the binding in index.ts — so this logic runs (and is tested) in plain node.
6
+ *
7
+ * Contract: nothing here ever throws into the host app.
8
+ */
9
+ export interface CoreStorage {
10
+ getItem(key: string): Promise<string | null>;
11
+ setItem(key: string, value: string): Promise<void>;
12
+ }
13
+ export interface CoreDeps {
14
+ fetch: typeof fetch;
15
+ now: () => number;
16
+ /** Persisted queue + identity; null = memory only. */
17
+ storage?: CoreStorage | null;
18
+ /** Static context merged into every event's properties. */
19
+ context: () => Record<string, unknown>;
20
+ onError?: (message: string) => void;
21
+ }
22
+ export interface CoreConfig {
23
+ apiKey: string;
24
+ host: string;
25
+ flushIntervalMs?: number;
26
+ sessionTimeoutMs?: number;
27
+ }
28
+ export declare class RootnodeCore {
29
+ private readonly cfg;
30
+ private readonly deps;
31
+ private pending;
32
+ private id;
33
+ private session;
34
+ private backgroundedAt;
35
+ private stickyScreen;
36
+ private flushing;
37
+ private timer;
38
+ constructor(cfg: CoreConfig, deps: CoreDeps);
39
+ get distinctId(): string;
40
+ get sessionId(): string;
41
+ /** Restore identity + queue; start the flush timer. Never throws. */
42
+ start(): Promise<void>;
43
+ capture(event: string, properties?: Record<string, unknown>): void;
44
+ /** Track a screen view and make it the sticky context for later events. */
45
+ screen(name: string): void;
46
+ identify(userId: string, set?: Record<string, unknown>): Promise<void>;
47
+ /** Sign-out: fresh anonymous identity and a fresh session. */
48
+ reset(): Promise<void>;
49
+ appBackgrounded(): void;
50
+ appForegrounded(): void;
51
+ flush(): Promise<void>;
52
+ close(): Promise<void>;
53
+ private doFlush;
54
+ private persistQueue;
55
+ private persistId;
56
+ private report;
57
+ }
package/dist/core.js ADDED
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ /**
3
+ * Platform-free core of the React Native SDK: queue + batching + retry,
4
+ * persisted across restarts, session rotation, anonymous→user identity.
5
+ * Everything platform-ish (AppState, expo modules, Sentry) is injected by
6
+ * the binding in index.ts — so this logic runs (and is tested) in plain node.
7
+ *
8
+ * Contract: nothing here ever throws into the host app.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.RootnodeCore = void 0;
12
+ const QUEUE_KEY = 'rootnode.queue';
13
+ const ID_KEY = 'rootnode.distinct_id';
14
+ const QUEUE_CAP = 1000;
15
+ const AUTO_FLUSH_AT = 20;
16
+ // No Node builtins here — this file must bundle under Metro (React Native).
17
+ // Prefer the Web Crypto UUID when the runtime has one; fall back to a plain
18
+ // Math.random v4 (these are analytics ids, not secrets).
19
+ const uuid = () => {
20
+ const c = globalThis.crypto;
21
+ if (typeof c?.randomUUID === 'function')
22
+ return c.randomUUID();
23
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {
24
+ const r = (Math.random() * 16) | 0;
25
+ return (ch === 'x' ? r : (r & 0x3) | 0x8).toString(16);
26
+ });
27
+ };
28
+ class RootnodeCore {
29
+ constructor(cfg, deps) {
30
+ this.cfg = cfg;
31
+ this.deps = deps;
32
+ this.pending = [];
33
+ this.id = `anon-${uuid()}`;
34
+ this.session = uuid();
35
+ this.backgroundedAt = null;
36
+ this.flushing = null;
37
+ this.timer = null;
38
+ }
39
+ get distinctId() {
40
+ return this.id;
41
+ }
42
+ get sessionId() {
43
+ return this.session;
44
+ }
45
+ /** Restore identity + queue; start the flush timer. Never throws. */
46
+ async start() {
47
+ try {
48
+ const saved = await this.deps.storage?.getItem(ID_KEY);
49
+ if (saved)
50
+ this.id = saved;
51
+ else
52
+ await this.persistId();
53
+ const q = await this.deps.storage?.getItem(QUEUE_KEY);
54
+ if (q) {
55
+ const parsed = JSON.parse(q);
56
+ if (Array.isArray(parsed))
57
+ this.pending.push(...parsed.slice(-QUEUE_CAP));
58
+ }
59
+ }
60
+ catch (e) {
61
+ this.report(`restore failed: ${String(e)}`);
62
+ }
63
+ const interval = this.cfg.flushIntervalMs ?? 10000;
64
+ if (interval > 0) {
65
+ this.timer = setInterval(() => void this.flush(), interval);
66
+ this.timer?.unref?.();
67
+ }
68
+ }
69
+ capture(event, properties = {}) {
70
+ try {
71
+ this.pending.push({
72
+ event,
73
+ distinct_id: this.id,
74
+ timestamp: new Date(this.deps.now()).toISOString(),
75
+ properties: {
76
+ ...this.deps.context(),
77
+ ...(this.stickyScreen ? { screen: this.stickyScreen } : {}),
78
+ ...properties,
79
+ session_id: this.session,
80
+ },
81
+ });
82
+ if (this.pending.length > QUEUE_CAP)
83
+ this.pending.splice(0, this.pending.length - QUEUE_CAP);
84
+ void this.persistQueue();
85
+ if (this.pending.length >= AUTO_FLUSH_AT)
86
+ void this.flush();
87
+ }
88
+ catch (e) {
89
+ this.report(`capture failed: ${String(e)}`);
90
+ }
91
+ }
92
+ /** Track a screen view and make it the sticky context for later events. */
93
+ screen(name) {
94
+ this.stickyScreen = name;
95
+ this.capture('screen_viewed', { screen: name });
96
+ }
97
+ async identify(userId, set) {
98
+ try {
99
+ this.id = userId;
100
+ await this.persistId();
101
+ this.capture('$identify', set ? { $set: set } : {});
102
+ await this.flush();
103
+ }
104
+ catch (e) {
105
+ this.report(`identify failed: ${String(e)}`);
106
+ }
107
+ }
108
+ /** Sign-out: fresh anonymous identity and a fresh session. */
109
+ async reset() {
110
+ this.id = `anon-${uuid()}`;
111
+ this.session = uuid();
112
+ await this.persistId();
113
+ }
114
+ appBackgrounded() {
115
+ this.backgroundedAt = this.deps.now();
116
+ this.capture('app_backgrounded', {});
117
+ void this.flush();
118
+ }
119
+ appForegrounded() {
120
+ const timeout = this.cfg.sessionTimeoutMs ?? 30 * 60 * 1000;
121
+ const away = this.backgroundedAt != null ? this.deps.now() - this.backgroundedAt : 0;
122
+ this.backgroundedAt = null;
123
+ if (away >= timeout) {
124
+ this.session = uuid();
125
+ this.capture('app_opened', {});
126
+ }
127
+ }
128
+ async flush() {
129
+ // Queue behind an in-flight flush (events captured meanwhile must go out
130
+ // too), then take the slot. The while re-check keeps concurrent callers
131
+ // from double-sending the same batch.
132
+ while (this.flushing)
133
+ await this.flushing;
134
+ const run = this.doFlush().finally(() => {
135
+ if (this.flushing === run)
136
+ this.flushing = null;
137
+ });
138
+ this.flushing = run;
139
+ return run;
140
+ }
141
+ async close() {
142
+ if (this.timer)
143
+ clearInterval(this.timer);
144
+ this.timer = null;
145
+ await this.flush();
146
+ }
147
+ async doFlush() {
148
+ try {
149
+ if (!this.cfg.apiKey || this.pending.length === 0)
150
+ return;
151
+ const batch = this.pending.slice(0, QUEUE_CAP);
152
+ const res = await this.deps.fetch(`${this.cfg.host}/v1/capture`, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ body: JSON.stringify({ api_key: this.cfg.apiKey, batch }),
156
+ });
157
+ if (res.status < 400) {
158
+ this.pending.splice(0, batch.length);
159
+ await this.persistQueue();
160
+ }
161
+ else
162
+ this.report(`capture refused: http ${res.status}`);
163
+ }
164
+ catch (e) {
165
+ this.report(`flush failed: ${String(e)}`);
166
+ }
167
+ }
168
+ async persistQueue() {
169
+ try {
170
+ await this.deps.storage?.setItem(QUEUE_KEY, JSON.stringify(this.pending));
171
+ }
172
+ catch {
173
+ /* storage is a convenience, never a requirement */
174
+ }
175
+ }
176
+ async persistId() {
177
+ try {
178
+ await this.deps.storage?.setItem(ID_KEY, this.id);
179
+ }
180
+ catch {
181
+ /* same */
182
+ }
183
+ }
184
+ report(msg) {
185
+ try {
186
+ this.deps.onError?.(`[rootnode] ${msg}`);
187
+ }
188
+ catch {
189
+ /* the error hook must not throw either */
190
+ }
191
+ }
192
+ }
193
+ exports.RootnodeCore = RootnodeCore;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Rootnode for React Native / Expo: one install, one init.
3
+ * Rootnode.init({ dsn, apiKey }) → crash (Sentry engine inside) + analytics + sessions
4
+ * Rootnode.registerForPush(userId) → push token registry
5
+ * Every platform module is loaded defensively: a missing/incompatible native
6
+ * module degrades that feature to a no-op instead of crashing the app.
7
+ */
8
+ export interface RootnodeConfig {
9
+ /** Crash DSN of this app's target. Omit to disable crash reporting. */
10
+ dsn?: string;
11
+ /** Analytics API key (rn_…). Omit to disable analytics. */
12
+ apiKey?: string;
13
+ /** Defaults to https://rootnode.app (or the DSN's host). */
14
+ host?: string;
15
+ environment?: string;
16
+ /** Session gap that starts a new session (default 30 min). */
17
+ sessionTimeoutMs?: number;
18
+ flushIntervalMs?: number;
19
+ /** Extra options passed straight to the crash engine's init. */
20
+ crashOptions?: Record<string, unknown>;
21
+ onError?: (message: string) => void;
22
+ }
23
+ type Loader = (name: string) => any;
24
+ /** Test seam: swap module loading (vitest has no RN runtime). */
25
+ export declare function _setLoader(l: Loader): void;
26
+ declare class RootnodeClient {
27
+ private core;
28
+ private sentry;
29
+ private cfg;
30
+ private lastPushToken;
31
+ private appStateSub;
32
+ /** Safe to call once at startup; everything after degrades gracefully. */
33
+ init(cfg: RootnodeConfig): void;
34
+ captureException(err: unknown, context?: Record<string, unknown>): void;
35
+ /** Wrap your root component (touch/startup instrumentation). Identity if crash is off. */
36
+ wrap<T>(component: T): T;
37
+ capture(event: string, properties?: Record<string, unknown>): void;
38
+ screen(name: string): void;
39
+ identify(userId: string, set?: Record<string, unknown>): Promise<void>;
40
+ reset(): Promise<void>;
41
+ flush(): Promise<void>;
42
+ close(): Promise<void>;
43
+ /** Ask permission (if needed), fetch the Expo push token, register it. */
44
+ registerForPush(distinctId?: string): Promise<boolean>;
45
+ unregisterPush(): Promise<void>;
46
+ private base;
47
+ private postPushToken;
48
+ private makeStorage;
49
+ private makeContext;
50
+ private watchAppState;
51
+ }
52
+ export declare const Rootnode: RootnodeClient;
53
+ export { RootnodeCore } from './core';
package/dist/index.js ADDED
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RootnodeCore = exports.Rootnode = void 0;
4
+ exports._setLoader = _setLoader;
5
+ const core_1 = require("./core");
6
+ let load = (name) => {
7
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
8
+ return require(name);
9
+ };
10
+ /** Test seam: swap module loading (vitest has no RN runtime). */
11
+ function _setLoader(l) {
12
+ load = l;
13
+ }
14
+ function tryLoad(name, onError) {
15
+ try {
16
+ return load(name);
17
+ }
18
+ catch {
19
+ onError?.(`[rootnode] optional module ${name} unavailable — feature disabled`);
20
+ return null;
21
+ }
22
+ }
23
+ class RootnodeClient {
24
+ constructor() {
25
+ this.core = null;
26
+ this.sentry = null;
27
+ this.cfg = {};
28
+ this.lastPushToken = null;
29
+ this.appStateSub = null;
30
+ }
31
+ /** Safe to call once at startup; everything after degrades gracefully. */
32
+ init(cfg) {
33
+ try {
34
+ this.cfg = cfg;
35
+ const host = cfg.host?.replace(/\/$/, '') ??
36
+ (cfg.dsn?.match(/^https?:\/\/[^@]+@([^/]+)\//)?.[1]
37
+ ? `https://${cfg.dsn.match(/^https?:\/\/[^@]+@([^/]+)\//)[1]}`
38
+ : 'https://rootnode.app');
39
+ if (cfg.dsn) {
40
+ this.sentry = tryLoad('@sentry/react-native', cfg.onError);
41
+ this.sentry?.init?.({
42
+ dsn: cfg.dsn,
43
+ environment: cfg.environment,
44
+ tracesSampleRate: 0,
45
+ ...cfg.crashOptions,
46
+ });
47
+ }
48
+ if (cfg.apiKey) {
49
+ this.core = new core_1.RootnodeCore({
50
+ apiKey: cfg.apiKey,
51
+ host,
52
+ flushIntervalMs: cfg.flushIntervalMs,
53
+ sessionTimeoutMs: cfg.sessionTimeoutMs,
54
+ }, {
55
+ fetch: (...a) => fetch(...a),
56
+ now: () => Date.now(),
57
+ storage: this.makeStorage(),
58
+ context: this.makeContext(),
59
+ onError: cfg.onError,
60
+ });
61
+ void this.core.start().then(() => this.core?.capture('app_opened', {}));
62
+ this.watchAppState();
63
+ }
64
+ }
65
+ catch (e) {
66
+ cfg.onError?.(`[rootnode] init failed: ${String(e)}`);
67
+ }
68
+ }
69
+ // ------------------------------------------------------------- crash
70
+ captureException(err, context) {
71
+ try {
72
+ this.sentry?.captureException?.(err, context ? { extra: context } : undefined);
73
+ }
74
+ catch {
75
+ /* never into the app */
76
+ }
77
+ }
78
+ /** Wrap your root component (touch/startup instrumentation). Identity if crash is off. */
79
+ wrap(component) {
80
+ try {
81
+ return this.sentry?.wrap ? this.sentry.wrap(component) : component;
82
+ }
83
+ catch {
84
+ return component;
85
+ }
86
+ }
87
+ // --------------------------------------------------------- analytics
88
+ capture(event, properties = {}) {
89
+ this.core?.capture(event, properties);
90
+ }
91
+ screen(name) {
92
+ this.core?.screen(name);
93
+ }
94
+ async identify(userId, set) {
95
+ await this.core?.identify(userId, set);
96
+ // The device now belongs to this user — move its push address too.
97
+ if (this.lastPushToken)
98
+ await this.postPushToken(this.lastPushToken, userId);
99
+ }
100
+ async reset() {
101
+ await this.core?.reset();
102
+ }
103
+ async flush() {
104
+ await this.core?.flush();
105
+ }
106
+ async close() {
107
+ try {
108
+ this.appStateSub?.remove?.();
109
+ }
110
+ catch {
111
+ /* already gone */
112
+ }
113
+ await this.core?.close();
114
+ }
115
+ // -------------------------------------------------------------- push
116
+ /** Ask permission (if needed), fetch the Expo push token, register it. */
117
+ async registerForPush(distinctId) {
118
+ try {
119
+ if (!this.cfg.apiKey)
120
+ return false;
121
+ const Notifications = tryLoad('expo-notifications', this.cfg.onError);
122
+ if (!Notifications)
123
+ return false;
124
+ const Device = tryLoad('expo-device', this.cfg.onError);
125
+ if (Device && Device.isDevice === false)
126
+ return false; // simulators get nothing
127
+ const perm = await Notifications.getPermissionsAsync();
128
+ const granted = perm?.granted ? true : (await Notifications.requestPermissionsAsync())?.granted;
129
+ if (!granted)
130
+ return false;
131
+ const token = (await Notifications.getExpoPushTokenAsync())?.data;
132
+ if (!token)
133
+ return false;
134
+ this.lastPushToken = token;
135
+ return this.postPushToken(token, distinctId ?? this.core?.distinctId ?? 'anonymous');
136
+ }
137
+ catch (e) {
138
+ this.cfg.onError?.(`[rootnode] push register failed: ${String(e)}`);
139
+ return false;
140
+ }
141
+ }
142
+ async unregisterPush() {
143
+ try {
144
+ if (!this.lastPushToken || !this.cfg.apiKey)
145
+ return;
146
+ await fetch(`${this.base()}/v1/push/unregister`, {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify({ api_key: this.cfg.apiKey, token: this.lastPushToken }),
150
+ });
151
+ this.lastPushToken = null;
152
+ }
153
+ catch {
154
+ /* goal state is "not registered"; a lost unregister self-heals via pruning */
155
+ }
156
+ }
157
+ // ---------------------------------------------------------- plumbing
158
+ base() {
159
+ return (this.cfg.host?.replace(/\/$/, '') ??
160
+ (this.cfg.dsn?.match(/^https?:\/\/[^@]+@([^/]+)\//)?.[1]
161
+ ? `https://${this.cfg.dsn.match(/^https?:\/\/[^@]+@([^/]+)\//)[1]}`
162
+ : 'https://rootnode.app'));
163
+ }
164
+ async postPushToken(token, distinctId) {
165
+ try {
166
+ const res = await fetch(`${this.base()}/v1/push/register`, {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({ api_key: this.cfg.apiKey, distinct_id: distinctId, token }),
170
+ });
171
+ return res.status < 400;
172
+ }
173
+ catch (e) {
174
+ this.cfg.onError?.(`[rootnode] push register failed: ${String(e)}`);
175
+ return false;
176
+ }
177
+ }
178
+ makeStorage() {
179
+ const AsyncStorage = tryLoad('@react-native-async-storage/async-storage', undefined)?.default;
180
+ if (AsyncStorage?.getItem)
181
+ return AsyncStorage;
182
+ const FS = tryLoad('expo-file-system/legacy', undefined) ?? tryLoad('expo-file-system', undefined);
183
+ if (FS?.documentDirectory && FS.readAsStringAsync) {
184
+ const path = (k) => `${FS.documentDirectory}${k.replace(/[^\w.-]/g, '_')}.json`;
185
+ return {
186
+ getItem: async (k) => {
187
+ try {
188
+ return await FS.readAsStringAsync(path(k));
189
+ }
190
+ catch {
191
+ return null;
192
+ }
193
+ },
194
+ setItem: async (k, v) => {
195
+ await FS.writeAsStringAsync(path(k), v);
196
+ },
197
+ };
198
+ }
199
+ return null; // memory-only queue — still works, just not restart-proof
200
+ }
201
+ makeContext() {
202
+ const RN = tryLoad('react-native', undefined);
203
+ const Device = tryLoad('expo-device', undefined);
204
+ const App = tryLoad('expo-application', undefined);
205
+ const Updates = tryLoad('expo-updates', undefined);
206
+ return () => {
207
+ const ctx = {};
208
+ try {
209
+ if (RN?.Platform?.OS)
210
+ ctx.platform = RN.Platform.OS;
211
+ if (Device?.osVersion)
212
+ ctx.os_version = String(Device.osVersion);
213
+ if (App?.nativeApplicationVersion)
214
+ ctx.app_version = App.nativeApplicationVersion;
215
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
216
+ if (tz)
217
+ ctx.device_tz = tz;
218
+ if (Updates?.updateId)
219
+ ctx.update_id = Updates.updateId;
220
+ }
221
+ catch {
222
+ /* context is best-effort */
223
+ }
224
+ return ctx;
225
+ };
226
+ }
227
+ watchAppState() {
228
+ try {
229
+ const RN = tryLoad('react-native', this.cfg.onError);
230
+ if (!RN?.AppState?.addEventListener)
231
+ return;
232
+ this.appStateSub = RN.AppState.addEventListener('change', (state) => {
233
+ if (state === 'background' || state === 'inactive')
234
+ this.core?.appBackgrounded();
235
+ else if (state === 'active')
236
+ this.core?.appForegrounded();
237
+ });
238
+ }
239
+ catch {
240
+ /* no AppState = no session rotation, nothing else breaks */
241
+ }
242
+ }
243
+ }
244
+ exports.Rootnode = new RootnodeClient();
245
+ var core_2 = require("./core");
246
+ Object.defineProperty(exports, "RootnodeCore", { enumerable: true, get: function () { return core_2.RootnodeCore; } });
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@rootnode-app/react-native",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Rootnode for React Native / Expo apps: crash reporting, analytics, sessions and push registration — one install, one init.",
5
+ "license": "MIT",
6
+ "author": "Doğan Aktı",
7
+ "homepage": "https://rootnode.app/docs/integration.md",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/doganakti/rootnode.git",
11
+ "directory": "sdk/react-native"
12
+ },
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "test": "vitest run",
22
+ "prepublishOnly": "npm run test && npm run build"
23
+ },
24
+ "dependencies": {
25
+ "@sentry/react-native": ">=5.0.0",
26
+ "expo-device": "*",
27
+ "expo-notifications": "*"
28
+ },
29
+ "peerDependencies": {
30
+ "react-native": "*"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.20.5",
34
+ "typescript": "^5.9.3",
35
+ "vitest": "^2.1.9"
36
+ }
37
+ }