@rootnode-app/react-native 0.1.2 → 0.2.0

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/dist/index.d.ts CHANGED
@@ -21,8 +21,8 @@ export interface RootnodeConfig {
21
21
  onError?: (message: string) => void;
22
22
  }
23
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;
24
+ /** Test seam: vitest has no RN runtime. null restores the real requires. */
25
+ export declare function _setLoader(l: Loader | null): void;
26
26
  declare class RootnodeClient {
27
27
  private core;
28
28
  private sentry;
@@ -47,6 +47,8 @@ declare class RootnodeClient {
47
47
  private postPushToken;
48
48
  private makeStorage;
49
49
  private makeContext;
50
+ /** Auto-track push opens: emits push_opened with the campaign id. */
51
+ private watchPushOpens;
50
52
  private watchAppState;
51
53
  }
52
54
  export declare const Rootnode: RootnodeClient;
package/dist/index.js CHANGED
@@ -3,23 +3,131 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RootnodeCore = exports.Rootnode = void 0;
4
4
  exports._setLoader = _setLoader;
5
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). */
6
+ let overrideLoader = null;
7
+ /** Test seam: vitest has no RN runtime. null restores the real requires. */
11
8
  function _setLoader(l) {
12
- load = l;
9
+ overrideLoader = l;
13
10
  }
14
- function tryLoad(name, onError) {
11
+ /**
12
+ * Every native module gets its own LITERAL require inside its own try/catch —
13
+ * Metro only resolves static names, and only treats a require as optional
14
+ * when the call sits in a try block. A require call with a variable name ships
15
+ * a bundle where every native feature silently dies (Foneria, 0.1.2).
16
+ */
17
+ function viaOverride(name) {
18
+ if (!overrideLoader)
19
+ return { hit: false, mod: null };
15
20
  try {
16
- return load(name);
21
+ return { hit: true, mod: overrideLoader(name) };
22
+ }
23
+ catch {
24
+ return { hit: true, mod: null };
25
+ }
26
+ }
27
+ function loadSentry() {
28
+ const o = viaOverride('@sentry/react-native');
29
+ if (o.hit)
30
+ return o.mod;
31
+ try {
32
+ return require('@sentry/react-native');
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function loadNotifications() {
39
+ const o = viaOverride('expo-notifications');
40
+ if (o.hit)
41
+ return o.mod;
42
+ try {
43
+ return require('expo-notifications');
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function loadDevice() {
50
+ const o = viaOverride('expo-device');
51
+ if (o.hit)
52
+ return o.mod;
53
+ try {
54
+ return require('expo-device');
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ function loadReactNative() {
61
+ const o = viaOverride('react-native');
62
+ if (o.hit)
63
+ return o.mod;
64
+ try {
65
+ return require('react-native');
17
66
  }
18
67
  catch {
19
- onError?.(`[rootnode] optional module ${name} unavailable — feature disabled`);
20
68
  return null;
21
69
  }
22
70
  }
71
+ function loadAsyncStorage() {
72
+ const o = viaOverride('@react-native-async-storage/async-storage');
73
+ if (o.hit)
74
+ return o.mod;
75
+ try {
76
+ return require('@react-native-async-storage/async-storage');
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function loadFileSystemLegacy() {
83
+ const o = viaOverride('expo-file-system/legacy');
84
+ if (o.hit)
85
+ return o.mod;
86
+ try {
87
+ return require('expo-file-system/legacy');
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ function loadFileSystem() {
94
+ const o = viaOverride('expo-file-system');
95
+ if (o.hit)
96
+ return o.mod;
97
+ try {
98
+ return require('expo-file-system');
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ function loadApplication() {
105
+ const o = viaOverride('expo-application');
106
+ if (o.hit)
107
+ return o.mod;
108
+ try {
109
+ return require('expo-application');
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ function loadUpdates() {
116
+ const o = viaOverride('expo-updates');
117
+ if (o.hit)
118
+ return o.mod;
119
+ try {
120
+ return require('expo-updates');
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ function warned(mod, name, onError) {
127
+ if (mod == null)
128
+ onError?.(`[rootnode] optional module ${name} unavailable — feature disabled`);
129
+ return mod;
130
+ }
23
131
  class RootnodeClient {
24
132
  constructor() {
25
133
  this.core = null;
@@ -37,7 +145,7 @@ class RootnodeClient {
37
145
  ? `https://${cfg.dsn.match(/^https?:\/\/[^@]+@([^/]+)\//)[1]}`
38
146
  : 'https://rootnode.app');
39
147
  if (cfg.dsn) {
40
- this.sentry = tryLoad('@sentry/react-native', cfg.onError);
148
+ this.sentry = warned(loadSentry(), '@sentry/react-native', cfg.onError);
41
149
  this.sentry?.init?.({
42
150
  dsn: cfg.dsn,
43
151
  environment: cfg.environment,
@@ -46,6 +154,7 @@ class RootnodeClient {
46
154
  });
47
155
  }
48
156
  if (cfg.apiKey) {
157
+ this.watchPushOpens();
49
158
  this.core = new core_1.RootnodeCore({
50
159
  apiKey: cfg.apiKey,
51
160
  host,
@@ -118,10 +227,10 @@ class RootnodeClient {
118
227
  try {
119
228
  if (!this.cfg.apiKey)
120
229
  return false;
121
- const Notifications = tryLoad('expo-notifications', this.cfg.onError);
230
+ const Notifications = warned(loadNotifications(), 'expo-notifications', this.cfg.onError);
122
231
  if (!Notifications)
123
232
  return false;
124
- const Device = tryLoad('expo-device', this.cfg.onError);
233
+ const Device = loadDevice();
125
234
  if (Device && Device.isDevice === false)
126
235
  return false; // simulators get nothing
127
236
  const perm = await Notifications.getPermissionsAsync();
@@ -176,10 +285,10 @@ class RootnodeClient {
176
285
  }
177
286
  }
178
287
  makeStorage() {
179
- const AsyncStorage = tryLoad('@react-native-async-storage/async-storage', undefined)?.default;
288
+ const AsyncStorage = loadAsyncStorage()?.default;
180
289
  if (AsyncStorage?.getItem)
181
290
  return AsyncStorage;
182
- const FS = tryLoad('expo-file-system/legacy', undefined) ?? tryLoad('expo-file-system', undefined);
291
+ const FS = loadFileSystemLegacy() ?? loadFileSystem();
183
292
  if (FS?.documentDirectory && FS.readAsStringAsync) {
184
293
  const path = (k) => `${FS.documentDirectory}${k.replace(/[^\w.-]/g, '_')}.json`;
185
294
  return {
@@ -199,10 +308,10 @@ class RootnodeClient {
199
308
  return null; // memory-only queue — still works, just not restart-proof
200
309
  }
201
310
  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);
311
+ const RN = loadReactNative();
312
+ const Device = loadDevice();
313
+ const App = loadApplication();
314
+ const Updates = loadUpdates();
206
315
  return () => {
207
316
  const ctx = {};
208
317
  try {
@@ -224,9 +333,33 @@ class RootnodeClient {
224
333
  return ctx;
225
334
  };
226
335
  }
336
+ /** Auto-track push opens: emits push_opened with the campaign id. */
337
+ watchPushOpens() {
338
+ try {
339
+ const Notifications = loadNotifications();
340
+ if (!Notifications?.addNotificationResponseReceivedListener)
341
+ return;
342
+ Notifications.addNotificationResponseReceivedListener((res) => {
343
+ try {
344
+ const data = res?.notification?.request?.content?.data ?? {};
345
+ this.core?.capture('push_opened', {
346
+ ...(data._rn_campaign ? { _rn_campaign: String(data._rn_campaign) } : {}),
347
+ ...(typeof data.screen === 'string' ? { screen: data.screen } : {}),
348
+ });
349
+ void this.core?.flush();
350
+ }
351
+ catch {
352
+ /* tracking must never break the tap */
353
+ }
354
+ });
355
+ }
356
+ catch {
357
+ /* no notifications module = no open tracking */
358
+ }
359
+ }
227
360
  watchAppState() {
228
361
  try {
229
- const RN = tryLoad('react-native', this.cfg.onError);
362
+ const RN = warned(loadReactNative(), 'react-native', this.cfg.onError);
230
363
  if (!RN?.AppState?.addEventListener)
231
364
  return;
232
365
  this.appStateSub = RN.AppState.addEventListener('change', (state) => {
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@rootnode-app/react-native",
3
- "version": "0.1.2",
4
- "description": "Rootnode for React Native / Expo apps: crash reporting, analytics, sessions and push registration — one install, one init.",
3
+ "version": "0.2.0",
4
+ "description": "Rootnode for React Native / Expo apps: crash reporting, analytics, sessions and push registration \u2014 one install, one init.",
5
5
  "license": "MIT",
6
- "author": "Doğan Aktı",
6
+ "author": "Do\u011fan Akt\u0131",
7
7
  "homepage": "https://rootnode.app/docs/integration.md",
8
8
  "repository": {
9
9
  "type": "git",
@@ -34,4 +34,4 @@
34
34
  "vitest": "^2.1.9",
35
35
  "esbuild": "^0.24.0"
36
36
  }
37
- }
37
+ }