react-native-test-adx 0.1.1 → 0.1.3

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
@@ -36,12 +36,6 @@ import { AdaxonProvider, useAdaxon } from 'react-native-test-adx';
36
36
 
37
37
  Both packages expose the same API. Only the npm name differs.
38
38
 
39
- Recommended storage helper:
40
-
41
- ```sh
42
- npx expo install expo-secure-store
43
- ```
44
-
45
39
  ## Quick start (React Native)
46
40
 
47
41
  Wrap the app in `AdaxonProvider`. Screens use `useAdaxon()`.
@@ -56,12 +50,12 @@ The SDK owns the tracker host and API paths (`/event/tracking`, `/api/mts/recomm
56
50
  |---|---|---|
57
51
  | `appId` | **Yes** | None. MTS app id (`adx_…`). |
58
52
  | `sdkKey` | **Yes** | None. Sent as the `x-sdk-key` header. |
59
- | `storage` | No | In-memory. Queue and anonymous id are lost on app restart. |
53
+ | `storage` | No | In-memory event queue and anonymous id. Offline events and the generated id are lost on app restart. |
60
54
  | `timeoutMs` | No | `10000` |
61
55
  | `testMode` | No | `false` (`sdk_version` gets a `-test` suffix when true) |
62
56
  | `externalUserId` | No | Unset |
63
57
 
64
- Pass `storage` in production so the anonymous id and queued events survive restart. The SDK detects the device platform and collects device/app fields automatically. Do **not** pass a tracker URL — requests go to the SDK `BASE_URL`.
58
+ The SDK detects the device platform and collects device/app fields automatically. Do **not** pass a tracker URL — requests go to the SDK `BASE_URL`.
65
59
 
66
60
  ```tsx
67
61
  import {
@@ -70,7 +64,6 @@ import {
70
64
  useAdaxon,
71
65
  type RecommendationOffer,
72
66
  } from 'react-native-adaxon';
73
- import * as SecureStore from 'expo-secure-store';
74
67
  import { Button, Text, View } from 'react-native';
75
68
 
76
69
  function App() {
@@ -80,11 +73,6 @@ function App() {
80
73
  appId: 'adx_your_app_id',
81
74
  sdkKey: 'your_sdk_key',
82
75
  testMode: __DEV__,
83
- storage: {
84
- getItem: (key) => SecureStore.getItemAsync(key),
85
- setItem: (key, value) => SecureStore.setItemAsync(key, value),
86
- removeItem: (key) => SecureStore.deleteItemAsync(key),
87
- },
88
76
  }}
89
77
  >
90
78
  <HomeScreen />
@@ -156,6 +144,21 @@ function HomeScreen() {
156
144
 
157
145
  Do **not** pass device or app fields. The SDK attaches `device` and `app` automatically on every event.
158
146
 
147
+ ### Anonymous id
148
+
149
+ `user.anonymous_id` is generated by the SDK. It reads a unique id from React Native `NativeModules` when the host exposes one, otherwise it generates a UUID. The value is cached for the process and written to the configured `storage` adapter so the same id is reused across app launches. No third-party device-info package is required.
150
+
151
+ ### Storage
152
+
153
+ `storage` is optional. It persists the **event queue** (offline / pending events) and the **anonymous id**. Only `appId` and `sdkKey` are required.
154
+
155
+ | What you pass | What happens |
156
+ |---|---|
157
+ | No `storage` | The queue and anonymous id stay in memory. Tracking still works. Both are lost if the app closes. |
158
+ | A storage adapter | Queued events and the anonymous id survive restart. |
159
+
160
+ The adapter is only `getItem` / `setItem` / `removeItem`. You do not implement the queue.
161
+
159
162
  ### Non-component boot
160
163
 
161
164
  `Adaxon.initialize()` is still available if you are not inside a React tree. Prefer the provider in React Native apps.
@@ -218,16 +221,27 @@ POST /event/tracking
218
221
 
219
222
  Rules:
220
223
 
221
- - Pending events are stored through the `storage` adapter, so they survive app restart.
224
+ - Pending events are stored through the `storage` adapter when you pass one, so they survive app restart. If you omit `storage`, they stay in memory only.
222
225
  - Events are processed in creation order (FIFO).
223
226
  - Each request contains at most **100** events. A queue of 250 is sent as 100 + 100 + 50.
224
227
  - Events are removed only after the API confirms success (`success: true`). On network, HTTP, or unknown errors they stay in the queue.
225
228
  - Only one processor runs at a time for the same app. A second `processPendingEvents()` call returns `already_processing`.
226
229
  - Large queues are read 100 events at a time. The SDK does not load the whole queue into memory.
227
- - On `Adaxon.initialize()` / `AdaxonProvider` mount, pending events start flushing in the background without blocking startup.
230
+ - On `Adaxon.initialize()` / `AdaxonProvider` mount, pending events start flushing in the background without blocking startup when the network is available.
228
231
  - Pass `removeItem` on the storage adapter so successful events can be deleted. If it is omitted, the SDK writes an empty value instead.
229
232
 
230
- Expo SecureStore has a small per-key size limit. For a large event queue, use a storage backend that can hold JSON payloads (for example AsyncStorage) and keep SecureStore for the anonymous id if you want.
233
+ ## Network loss
234
+
235
+ The SDK watches connectivity through the host app's NetInfo / Expo Network listener. It does **not** poll.
236
+
237
+ - Online: `track` / `trackBatch` send immediately, same as before.
238
+ - Offline: events are stored in the event queue and no API request is made.
239
+ - Offline → online: the batch processor is notified once and pending events are flushed. Duplicate `ONLINE` events do not start extra processors.
240
+ - `isNetworkAvailable()` reports the current state. `destroy()` (and Provider unmount) removes the network listener.
241
+
242
+ A missing or failing network module does not crash the app. Tracking continues as online until the device is known to be offline.
243
+
244
+ Offline events use the same `storage` adapter as above. Without it, they are held in memory and are lost if the app is closed before the network returns.
231
245
 
232
246
  ## Automatic device information
233
247
 
@@ -253,7 +267,7 @@ Values come from React Native `Platform` (`src/platform/reactNative.ts`). They a
253
267
  "app": {
254
268
  "package_name": "com.example.app",
255
269
  "app_version": "2.4.1",
256
- "sdk_version": "0.1.1"
270
+ "sdk_version": "0.1.3"
257
271
  }
258
272
  }
259
273
  ```
@@ -289,7 +303,7 @@ MTS tracker
289
303
  | `src/platform/` | Android/iOS fields. **Only `react-native` import** |
290
304
  | `src/config/` | Tracker host, API paths, timeouts, storage key rules |
291
305
  | `src/validation/` | Event name, property, and batch-size rules |
292
- | `src/storage/` | Anonymous user id and persistent event queue |
306
+ | `src/storage/` | Persistent event queue |
293
307
  | `example/src/` | Demo app: `App.tsx`, `screens/`, `components/` |
294
308
 
295
309
  Screens belong in the **host app**, not in the SDK.
@@ -3,7 +3,7 @@
3
3
  import { AdaxonClient } from "./core/AdaxonClient.js";
4
4
  import { MEDIATION_PROVIDER, OFFER_OPENED_EVENT } from "./config/ApiConstants.js";
5
5
  import { AdaxonSdkError } from "./errors/AdaxonSdkError.js";
6
- import { detectReactNativePlatform, reactNativeAppInfoReader, reactNativeDeviceReader, reactNativeNetworkInfoReader, reactNativeUrlOpener } from "./platform/reactNative.js";
6
+ import { detectReactNativePlatform, reactNativeAppInfoReader, reactNativeDeviceReader, reactNativeNetworkInfoReader, reactNativeNetworkStatusReader, reactNativeUrlOpener } from "./platform/reactNative.js";
7
7
  import { buildAddToCartProperties, buildPurchaseProperties, normalizeEventValues } from "./services/event/normalizeProperties.js";
8
8
  import { AdxEvent } from "./services/event/predefined.js";
9
9
  export class AdaxonSdk {
@@ -18,18 +18,25 @@ export class AdaxonSdk {
18
18
  }
19
19
  async initialize(configuration, listener) {
20
20
  try {
21
- this.client = new AdaxonClient({
21
+ const previous = this.client;
22
+ const client = new AdaxonClient({
22
23
  ...configuration,
23
24
  platform: detectReactNativePlatform(),
24
25
  deviceInfoReader: reactNativeDeviceReader,
25
26
  appInfoReader: reactNativeAppInfoReader,
26
- networkInfoReader: reactNativeNetworkInfoReader
27
+ networkInfoReader: reactNativeNetworkInfoReader,
28
+ networkStatusReader: reactNativeNetworkStatusReader
27
29
  });
30
+ await client.start();
31
+ previous?.destroy();
32
+ this.client = client;
28
33
  this.testMode = Boolean(configuration.testMode);
29
34
  listener?.onInitializationComplete();
30
- this.client.processPendingEvents().catch(() => undefined);
35
+ if (client.isNetworkAvailable()) {
36
+ client.processPendingEvents().catch(() => undefined);
37
+ }
31
38
  } catch (error) {
32
- this.reset();
39
+ this.destroy();
33
40
  const message = AdaxonSdkError.messageOf(error, 'Initialization failed.');
34
41
  const code = error instanceof AdaxonSdkError && error.code === 'INVALID_CONFIG' ? 'INVALID_CONFIG' : 'UNKNOWN';
35
42
  listener?.onInitializationFailed(code, message);
@@ -107,14 +114,18 @@ export class AdaxonSdk {
107
114
  setExternalUserId(externalUserId) {
108
115
  this.requireClient().setExternalUserId(externalUserId);
109
116
  }
117
+ isNetworkAvailable() {
118
+ return this.client?.isNetworkAvailable() ?? false;
119
+ }
120
+ destroy() {
121
+ this.client?.destroy();
122
+ this.client = null;
123
+ this.testMode = false;
124
+ }
110
125
  requireClient() {
111
126
  if (!this.client) {
112
127
  throw new AdaxonSdkError('INVALID_CONFIG', 'Adaxon SDK is not initialized. Call Adaxon.initialize(...) first.');
113
128
  }
114
129
  return this.client;
115
130
  }
116
- reset() {
117
- this.client = null;
118
- this.testMode = false;
119
- }
120
131
  }
@@ -20,10 +20,12 @@ export function AdaxonProvider({
20
20
  useEffect(() => {
21
21
  let cancelled = false;
22
22
  sdk.initialize(configurationRef.current, listenerRef.current).then(() => {
23
- if (!cancelled) {
24
- setIsInitialized(true);
25
- setInitializationError(null);
23
+ if (cancelled) {
24
+ sdk.destroy();
25
+ return;
26
26
  }
27
+ setIsInitialized(true);
28
+ setInitializationError(null);
27
29
  }).catch(error => {
28
30
  if (!cancelled) {
29
31
  setIsInitialized(false);
@@ -32,6 +34,7 @@ export function AdaxonProvider({
32
34
  });
33
35
  return () => {
34
36
  cancelled = true;
37
+ sdk.destroy();
35
38
  };
36
39
  }, [sdk]);
37
40
  const value = useMemo(() => ({
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- export const SDK_VERSION = '0.1.1';
3
+ export const SDK_VERSION = '0.1.3';
4
4
  export const MEDIATION_PROVIDER = 'mts';
5
5
  export const BASE_URL = 'http://localhost:3002';
6
6
  export const EVENT_TRACKING_PATH = '/event/tracking';
@@ -4,7 +4,7 @@ export const ANONYMOUS_ID_KEY_PREFIX = 'adaxon_sdk.anonymous_id.';
4
4
  export const EVENT_QUEUE_META_KEY_PREFIX = 'adaxon_sdk.event_queue.meta.';
5
5
  export const EVENT_QUEUE_ITEM_KEY_PREFIX = 'adaxon_sdk.event_queue.item.';
6
6
 
7
- // Expo SecureStore and Keychain-backed adapters only accept alphanumerics
7
+ // Queue adapters (and Keychain-backed stores) only accept alphanumerics
8
8
  // plus `.`, `-` and `_`, so app ids are sanitized before use as a key.
9
9
  const UNSAFE_STORAGE_KEY_CHARS = /[^A-Za-z0-9._-]/g;
10
10
  export function sanitizeStorageId(appId) {
@@ -10,10 +10,11 @@ import { EventQueueService } from "../services/event/EventQueueService.js";
10
10
  import { EventService } from "../services/event/EventService.js";
11
11
  import { ApiClient } from "../services/network/ApiClient.js";
12
12
  import { NetworkInfoService } from "../services/network/NetworkInfoService.js";
13
- import { AnonymousIdService } from "../storage/AnonymousIdService.js";
13
+ import { NetworkService } from "../services/network/NetworkService.js";
14
14
  import { EventQueueStorage } from "../storage/EventQueueStorage.js";
15
15
  import { EventValidator } from "../validation/EventValidator.js";
16
16
  export class AdaxonClient {
17
+ started = false;
17
18
  constructor(config) {
18
19
  const auth = new AuthenticationService(config);
19
20
  this.validator = new EventValidator();
@@ -25,10 +26,9 @@ export class AdaxonClient {
25
26
  const networkInfo = new NetworkInfoService(config.networkInfoReader);
26
27
  this.builder = new EventBuilder({
27
28
  appId: auth.appId,
28
- deviceInfo: new DeviceInfoService(config.platform ?? 'web', config.deviceInfoReader),
29
+ deviceInfo: new DeviceInfoService(config.platform ?? 'web', config.deviceInfoReader, config.storage, auth.appId),
29
30
  appInfo: new AppInfoService(Boolean(config.testMode), config.appInfoReader),
30
31
  networkInfo,
31
- anonymousId: new AnonymousIdService(auth.appId, config.storage),
32
32
  validator: this.validator,
33
33
  externalUserId: config.externalUserId
34
34
  });
@@ -42,12 +42,48 @@ export class AdaxonClient {
42
42
  queue: this.eventQueue,
43
43
  sendBatch: payloads => this.events.sendPayloads(payloads)
44
44
  });
45
+ this.network = new NetworkService(config.networkStatusReader);
46
+ }
47
+ async start() {
48
+ if (this.started) {
49
+ return;
50
+ }
51
+ this.started = true;
52
+ await this.network.initialize();
53
+ this.network.subscribe(() => {
54
+ this.batchProcessor.process().catch(() => undefined);
55
+ });
56
+ }
57
+ destroy() {
58
+ this.network.destroy();
59
+ this.started = false;
60
+ }
61
+ isNetworkAvailable() {
62
+ return this.network.isAvailable();
45
63
  }
46
64
  track(eventName, properties = {}, options = {}) {
47
- return this.events.track(eventName, properties, options);
65
+ return this.trackBatch([{
66
+ eventName,
67
+ properties,
68
+ ...options
69
+ }]);
48
70
  }
49
- trackBatch(events) {
50
- return this.events.trackBatch(events);
71
+ async trackBatch(events) {
72
+ if (this.network.isAvailable()) {
73
+ return this.events.trackBatch(events);
74
+ }
75
+ this.validator.validateBatch(events);
76
+ const payloads = await Promise.all(events.map(event => this.builder.build(event)));
77
+ for (const payload of payloads) {
78
+ await this.eventQueue.add(payload);
79
+ }
80
+ return {
81
+ success: true,
82
+ message: 'queued',
83
+ accepted: payloads.length,
84
+ received: payloads.length,
85
+ rejected: 0
86
+ };
51
87
  }
52
88
  async enqueueEvent(eventName, properties = {}, options = {}) {
53
89
  const payload = await this.builder.build({
@@ -13,6 +13,26 @@ function publicIpAddress(value) {
13
13
  const ip = usableIpAddress(value);
14
14
  return ip && !isPrivateIpAddress(ip) ? ip : undefined;
15
15
  }
16
+ export function isOnlineFromNativeState(state) {
17
+ const value = asNativeNetworkState(state);
18
+ if (!value) {
19
+ return null;
20
+ }
21
+ if (value.isConnected === false || value.isInternetReachable === false) {
22
+ return false;
23
+ }
24
+ const type = nonEmptyString(value.type)?.toLowerCase();
25
+ if (type === 'none') {
26
+ return false;
27
+ }
28
+ if (value.isConnected === true) {
29
+ return true;
30
+ }
31
+ if (type && type !== 'unknown') {
32
+ return true;
33
+ }
34
+ return null;
35
+ }
16
36
  export function connectionTypeFromNativeState(state) {
17
37
  const type = nonEmptyString(asNativeNetworkState(state)?.type)?.toLowerCase();
18
38
  return type && CONNECTION_TYPES.has(type) ? type : undefined;
@@ -1,12 +1,34 @@
1
1
  "use strict";
2
2
 
3
- import { Linking, NativeModules, Platform } from 'react-native';
3
+ import { Linking, NativeEventEmitter, NativeModules, Platform } from 'react-native';
4
4
  import { nonEmptyString, setIfPresent } from "../utils/strings.js";
5
5
  import { readLocaleFields } from "../utils/locale.js";
6
6
  import { appFieldsFromNative } from "./app.js";
7
7
  import { deviceFieldsFromAndroid } from "./android.js";
8
8
  import { deviceFieldsFromIos } from "./ios.js";
9
- import { connectionTypeFromNativeState, ipAddressFromNativeState, usableIpAddress } from "./network.js";
9
+ import { connectionTypeFromNativeState, ipAddressFromNativeState, isOnlineFromNativeState, usableIpAddress } from "./network.js";
10
+ function nativeModule(name) {
11
+ return (NativeModules ?? {})[name];
12
+ }
13
+ async function callNative(method, arg) {
14
+ if (typeof method !== 'function') {
15
+ return undefined;
16
+ }
17
+ try {
18
+ return await method(arg);
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function safeUnsubscribe(remove) {
24
+ return () => {
25
+ try {
26
+ remove?.();
27
+ } catch {
28
+ // Native unsubscribe can throw after the module is torn down.
29
+ }
30
+ };
31
+ }
10
32
  export function detectReactNativePlatform() {
11
33
  return Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'web';
12
34
  }
@@ -27,52 +49,85 @@ function collectFromReactNative(platform) {
27
49
  Object.assign(device, readLocaleFields());
28
50
  return device;
29
51
  }
30
- export const reactNativeDeviceReader = {
31
- readDeviceInfo: collectFromReactNative
32
- };
33
- export const reactNativeAppInfoReader = {
34
- readAppInfo: () => appFieldsFromNative(readNativeAppConstants())
35
- };
36
- export const reactNativeNetworkInfoReader = {
37
- readNetworkInfo: collectFromNativeNetwork
38
- };
39
- export const reactNativeUrlOpener = Linking;
52
+ async function readUniqueIdFromNative() {
53
+ const deviceInfo = nativeModule('RNDeviceInfo');
54
+ return nonEmptyString(await callNative(deviceInfo?.getUniqueId)) ?? '';
55
+ }
40
56
  function readNativeAppConstants() {
41
- const modules = NativeModules ?? {};
42
- const expoApp = modules.ExpoApplication;
43
- const expoConstants = modules.ExponentConstants;
44
- const deviceInfo = modules.RNDeviceInfo;
45
57
  return {
46
- ...deviceInfo,
47
- ...expoConstants,
48
- ...expoApp
58
+ ...nativeModule('RNDeviceInfo'),
59
+ ...nativeModule('ExponentConstants'),
60
+ ...nativeModule('ExpoApplication')
49
61
  };
50
62
  }
51
- async function readOptionalNative(read) {
63
+ function nativeNetworkModules() {
64
+ return {
65
+ netInfo: nativeModule('RNCNetInfo'),
66
+ expoNetwork: nativeModule('ExpoNetwork')
67
+ };
68
+ }
69
+ async function readIsOnlineFromNative() {
70
+ const {
71
+ netInfo,
72
+ expoNetwork
73
+ } = nativeNetworkModules();
74
+ const fromNetInfo = isOnlineFromNativeState(await callNative(netInfo?.getCurrentState, null));
75
+ if (fromNetInfo !== null) {
76
+ return fromNetInfo;
77
+ }
78
+ return isOnlineFromNativeState(await callNative(expoNetwork?.getNetworkStateAsync));
79
+ }
80
+ function subscribeNativeNetworkStatus(listener) {
81
+ const {
82
+ netInfo,
83
+ expoNetwork
84
+ } = nativeNetworkModules();
85
+ const onState = state => {
86
+ listener(isOnlineFromNativeState(state));
87
+ };
52
88
  try {
53
- return await read();
54
- } catch {
55
- return undefined;
89
+ if (netInfo) {
90
+ const subscription = new NativeEventEmitter(NativeModules.RNCNetInfo).addListener('netInfo.networkStatusDidChange', onState);
91
+ return safeUnsubscribe(() => subscription.remove());
92
+ }
93
+ if (expoNetwork && typeof expoNetwork.addListener === 'function') {
94
+ const subscription = expoNetwork.addListener('onNetworkStateChanged', onState);
95
+ return safeUnsubscribe(() => subscription?.remove?.());
96
+ }
97
+ } catch (error) {
98
+ console.error('[Adaxon] Failed to register the network listener', error);
56
99
  }
100
+ return () => undefined;
57
101
  }
58
102
  async function collectFromNativeNetwork() {
59
103
  const network = {};
60
- const modules = NativeModules ?? {};
61
- const netInfo = modules.RNCNetInfo;
62
- const expoNetwork = modules.ExpoNetwork;
63
- const getCurrentState = netInfo?.getCurrentState;
64
- if (typeof getCurrentState === 'function') {
65
- const state = await readOptionalNative(() => getCurrentState(null));
66
- setIfPresent(network, 'connection_type', connectionTypeFromNativeState(state));
67
- setIfPresent(network, 'ip_address', ipAddressFromNativeState(state));
104
+ const {
105
+ netInfo,
106
+ expoNetwork
107
+ } = nativeNetworkModules();
108
+ const state = await callNative(netInfo?.getCurrentState, null);
109
+ setIfPresent(network, 'connection_type', connectionTypeFromNativeState(state));
110
+ setIfPresent(network, 'ip_address', ipAddressFromNativeState(state));
111
+ if (!network.connection_type) {
112
+ setIfPresent(network, 'connection_type', connectionTypeFromNativeState(await callNative(expoNetwork?.getNetworkStateAsync)));
68
113
  }
69
- const getNetworkState = expoNetwork?.getNetworkStateAsync;
70
- if (!network.connection_type && typeof getNetworkState === 'function') {
71
- setIfPresent(network, 'connection_type', connectionTypeFromNativeState(await readOptionalNative(getNetworkState)));
72
- }
73
- const getIpAddress = expoNetwork?.getIpAddressAsync;
74
- if (!network.ip_address && typeof getIpAddress === 'function') {
75
- setIfPresent(network, 'ip_address', usableIpAddress(nonEmptyString(await readOptionalNative(getIpAddress))));
114
+ if (!network.ip_address) {
115
+ setIfPresent(network, 'ip_address', usableIpAddress(nonEmptyString(await callNative(expoNetwork?.getIpAddressAsync))));
76
116
  }
77
117
  return network;
78
- }
118
+ }
119
+ export const reactNativeDeviceReader = {
120
+ readDeviceInfo: collectFromReactNative,
121
+ getUniqueId: readUniqueIdFromNative
122
+ };
123
+ export const reactNativeAppInfoReader = {
124
+ readAppInfo: () => appFieldsFromNative(readNativeAppConstants())
125
+ };
126
+ export const reactNativeNetworkInfoReader = {
127
+ readNetworkInfo: collectFromNativeNetwork
128
+ };
129
+ export const reactNativeNetworkStatusReader = {
130
+ getIsOnline: readIsOnlineFromNative,
131
+ subscribe: subscribeNativeNetworkStatus
132
+ };
133
+ export const reactNativeUrlOpener = Linking;
@@ -1,22 +1,71 @@
1
1
  "use strict";
2
2
 
3
+ import { ANONYMOUS_ID_KEY_PREFIX, sanitizeStorageId } from "../../config/StorageKeys.js";
4
+ import { nonEmptyString } from "../../utils/strings.js";
5
+ import { createUuid } from "../../utils/uuid.js";
3
6
  export class DeviceInfoService {
4
- constructor(platform, reader) {
7
+ constructor(platform, reader, storage, appId) {
5
8
  this.platform = platform;
6
9
  this.reader = reader;
10
+ this.storage = storage;
11
+ this.anonymousIdKey = storage && appId ? `${ANONYMOUS_ID_KEY_PREFIX}${sanitizeStorageId(appId)}` : undefined;
7
12
  }
8
13
  getDeviceInfo() {
9
14
  if (!this.cached) {
10
- try {
11
- this.cached = this.reader?.readDeviceInfo(this.platform) ?? {
12
- platform: this.platform
13
- };
14
- } catch {
15
- this.cached = {
16
- platform: this.platform
17
- };
18
- }
15
+ this.cached = this.readDeviceInfo();
19
16
  }
20
17
  return this.cached;
21
18
  }
19
+ getUniqueId() {
20
+ if (!this.uniqueIdPromise) {
21
+ this.uniqueIdPromise = this.resolveUniqueId();
22
+ }
23
+ return this.uniqueIdPromise;
24
+ }
25
+ readDeviceInfo() {
26
+ const fallback = {
27
+ platform: this.platform
28
+ };
29
+ try {
30
+ return this.reader?.readDeviceInfo(this.platform) ?? fallback;
31
+ } catch {
32
+ return fallback;
33
+ }
34
+ }
35
+ async resolveUniqueId() {
36
+ const stored = await this.readStoredId();
37
+ if (stored) {
38
+ return stored;
39
+ }
40
+ const uniqueId = (await this.readNativeId()) ?? createUuid();
41
+ await this.writeStoredId(uniqueId);
42
+ return uniqueId;
43
+ }
44
+ async readNativeId() {
45
+ try {
46
+ return nonEmptyString(await this.reader?.getUniqueId?.());
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ async readStoredId() {
52
+ if (!this.storage || !this.anonymousIdKey) {
53
+ return undefined;
54
+ }
55
+ try {
56
+ return nonEmptyString(await this.storage.getItem(this.anonymousIdKey));
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ async writeStoredId(uniqueId) {
62
+ if (!this.storage || !this.anonymousIdKey) {
63
+ return;
64
+ }
65
+ try {
66
+ await this.storage.setItem(this.anonymousIdKey, uniqueId);
67
+ } catch {
68
+ // Persistence must not block tracking.
69
+ }
70
+ }
22
71
  }
@@ -15,14 +15,13 @@ export class EventBuilder {
15
15
  const {
16
16
  validator,
17
17
  appId,
18
- anonymousId,
19
18
  deviceInfo,
20
19
  appInfo,
21
20
  networkInfo
22
21
  } = this.config;
23
22
  validator.validateTrackInput(input);
24
23
  const user = {
25
- anonymous_id: await anonymousId.getAnonymousId()
24
+ anonymous_id: await deviceInfo.getUniqueId()
26
25
  };
27
26
  setIfPresent(user, 'external_user_id', nonEmptyString(this.externalUserId));
28
27
  return {
@@ -8,13 +8,6 @@ export class EventService {
8
8
  this.builder = config.builder;
9
9
  this.validator = config.validator;
10
10
  }
11
- track(eventName, properties = {}, options = {}) {
12
- return this.trackBatch([{
13
- eventName,
14
- properties,
15
- ...options
16
- }]);
17
- }
18
11
  async trackBatch(events) {
19
12
  this.validator.validateBatch(events);
20
13
  const payloads = await Promise.all(events.map(event => this.builder.build(event)));
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ function toState(isOnline) {
4
+ if (isOnline === true) {
5
+ return 'ONLINE';
6
+ }
7
+ if (isOnline === false) {
8
+ return 'OFFLINE';
9
+ }
10
+ return 'UNKNOWN';
11
+ }
12
+ export class NetworkService {
13
+ state = 'UNKNOWN';
14
+ started = false;
15
+ listeners = new Set();
16
+ constructor(reader) {
17
+ this.reader = reader;
18
+ }
19
+ getState() {
20
+ return this.state;
21
+ }
22
+ isAvailable() {
23
+ return this.state !== 'OFFLINE';
24
+ }
25
+ async initialize() {
26
+ if (this.started) {
27
+ return;
28
+ }
29
+ this.started = true;
30
+ if (!this.reader) {
31
+ this.applyState('ONLINE');
32
+ return;
33
+ }
34
+ try {
35
+ this.applyState(toState(await this.reader.getIsOnline()));
36
+ } catch (error) {
37
+ console.error('[Adaxon] Network monitoring failed', error);
38
+ this.applyState('UNKNOWN');
39
+ }
40
+ try {
41
+ this.unsubscribeReader = this.reader.subscribe(isOnline => {
42
+ this.applyState(toState(isOnline));
43
+ });
44
+ } catch (error) {
45
+ console.error('[Adaxon] Network monitoring failed', error);
46
+ this.applyState('UNKNOWN');
47
+ }
48
+ }
49
+ subscribe(listener) {
50
+ this.listeners.add(listener);
51
+ return () => {
52
+ this.listeners.delete(listener);
53
+ };
54
+ }
55
+ destroy() {
56
+ try {
57
+ this.unsubscribeReader?.();
58
+ } catch (error) {
59
+ console.error('[Adaxon] Failed to remove the network listener', error);
60
+ }
61
+ this.unsubscribeReader = undefined;
62
+ this.started = false;
63
+ this.listeners.clear();
64
+ }
65
+ applyState(next) {
66
+ const previous = this.state;
67
+ if (previous === next) {
68
+ return;
69
+ }
70
+ this.state = next;
71
+ if (previous !== 'OFFLINE' || next !== 'ONLINE') {
72
+ return;
73
+ }
74
+ for (const listener of [...this.listeners]) {
75
+ try {
76
+ listener();
77
+ } catch (error) {
78
+ console.error('[Adaxon] Network listener failed', error);
79
+ }
80
+ }
81
+ }
82
+ }
@@ -22,7 +22,8 @@ export declare class AdaxonSdk {
22
22
  loadRecommendations(eventName: string, properties?: EventProperties, listener?: AdaxonRecommendationLoadListener): Promise<RecommendationOffer[]>;
23
23
  showOffer(offer: RecommendationOffer, listener?: AdaxonOfferShowListener): Promise<void>;
24
24
  setExternalUserId(externalUserId?: string): void;
25
+ isNetworkAvailable(): boolean;
26
+ destroy(): void;
25
27
  private requireClient;
26
- private reset;
27
28
  }
28
29
  //# sourceMappingURL=AdaxonSdk.d.ts.map
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "0.1.1";
1
+ export declare const SDK_VERSION = "0.1.3";
2
2
  export declare const MEDIATION_PROVIDER = "mts";
3
3
  export declare const BASE_URL = "http://localhost:3002";
4
4
  export declare const EVENT_TRACKING_PATH = "/event/tracking";
@@ -1,4 +1,4 @@
1
- import type { AppInfoReader, DeviceInfoReader, NetworkInfoReader } from '../platform/types.js';
1
+ import type { AppInfoReader, DeviceInfoReader, NetworkInfoReader, NetworkStatusReader } from '../platform/types.js';
2
2
  import type { AdaxonSdkConfig } from '../types/AdaxonConfig.js';
3
3
  import type { EventIngestionResponse, RecommendationResponse } from '../types/ApiTypes.js';
4
4
  import type { AdaxonPlatform, ClickAttribution, EventProperties, TrackEventInput, TrackEventOptions } from '../types/EventTypes.js';
@@ -8,6 +8,7 @@ interface AdaxonClientConfig extends AdaxonSdkConfig {
8
8
  deviceInfoReader?: DeviceInfoReader;
9
9
  appInfoReader?: AppInfoReader;
10
10
  networkInfoReader?: NetworkInfoReader;
11
+ networkStatusReader?: NetworkStatusReader;
11
12
  }
12
13
  export declare class AdaxonClient {
13
14
  private readonly events;
@@ -15,7 +16,12 @@ export declare class AdaxonClient {
15
16
  private readonly validator;
16
17
  private readonly eventQueue;
17
18
  private readonly batchProcessor;
19
+ private readonly network;
20
+ private started;
18
21
  constructor(config: AdaxonClientConfig);
22
+ start(): Promise<void>;
23
+ destroy(): void;
24
+ isNetworkAvailable(): boolean;
19
25
  track(eventName: string, properties?: EventProperties, options?: TrackEventOptions): Promise<EventIngestionResponse>;
20
26
  trackBatch(events: readonly TrackEventInput[]): Promise<EventIngestionResponse>;
21
27
  enqueueEvent(eventName: string, properties?: EventProperties, options?: TrackEventOptions): Promise<void>;
@@ -1,3 +1,4 @@
1
+ export declare function isOnlineFromNativeState(state: unknown): boolean | null;
1
2
  export declare function connectionTypeFromNativeState(state: unknown): string | undefined;
2
3
  export declare function ipAddressFromNativeState(state: unknown): string | undefined;
3
4
  export declare function usableIpAddress(value: string | undefined): string | undefined;
@@ -1,8 +1,9 @@
1
1
  import type { AdaxonPlatform } from '../types/EventTypes.js';
2
- import type { AppInfoReader, DeviceInfoReader, NetworkInfoReader, UrlOpener } from './types.js';
2
+ import type { AppInfoReader, DeviceInfoReader, NetworkInfoReader, NetworkStatusReader, UrlOpener } from './types.js';
3
3
  export declare function detectReactNativePlatform(): AdaxonPlatform;
4
4
  export declare const reactNativeDeviceReader: DeviceInfoReader;
5
5
  export declare const reactNativeAppInfoReader: AppInfoReader;
6
6
  export declare const reactNativeNetworkInfoReader: NetworkInfoReader;
7
+ export declare const reactNativeNetworkStatusReader: NetworkStatusReader;
7
8
  export declare const reactNativeUrlOpener: UrlOpener;
8
9
  //# sourceMappingURL=reactNative.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import type { AdaxonAppContext, AdaxonDeviceContext, AdaxonNetworkContext, AdaxonPlatform } from '../types/EventTypes.js';
2
2
  export interface DeviceInfoReader {
3
3
  readDeviceInfo(platform: AdaxonPlatform): AdaxonDeviceContext;
4
+ getUniqueId?(): Promise<string>;
4
5
  }
5
6
  export interface AppInfoReader {
6
7
  readAppInfo(): AdaxonAppContext;
@@ -12,4 +13,8 @@ export interface UrlOpener {
12
13
  canOpenURL(url: string): Promise<boolean>;
13
14
  openURL(url: string): Promise<void>;
14
15
  }
16
+ export interface NetworkStatusReader {
17
+ getIsOnline(): Promise<boolean | null>;
18
+ subscribe(listener: (isOnline: boolean | null) => void): () => void;
19
+ }
15
20
  //# sourceMappingURL=types.d.ts.map
@@ -1,10 +1,20 @@
1
1
  import type { DeviceInfoReader } from '../../platform/types.js';
2
+ import type { AdaxonStorageAdapter } from '../../types/AdaxonConfig.js';
2
3
  import type { AdaxonDeviceContext, AdaxonPlatform } from '../../types/EventTypes.js';
3
4
  export declare class DeviceInfoService {
4
5
  private readonly platform;
5
6
  private readonly reader?;
7
+ private readonly storage?;
6
8
  private cached?;
7
- constructor(platform: AdaxonPlatform, reader?: DeviceInfoReader | undefined);
9
+ private uniqueIdPromise?;
10
+ private readonly anonymousIdKey?;
11
+ constructor(platform: AdaxonPlatform, reader?: DeviceInfoReader | undefined, storage?: AdaxonStorageAdapter | undefined, appId?: string);
8
12
  getDeviceInfo(): AdaxonDeviceContext;
13
+ getUniqueId(): Promise<string>;
14
+ private readDeviceInfo;
15
+ private resolveUniqueId;
16
+ private readNativeId;
17
+ private readStoredId;
18
+ private writeStoredId;
9
19
  }
10
20
  //# sourceMappingURL=DeviceInfoService.d.ts.map
@@ -1,7 +1,6 @@
1
1
  import type { AppInfoService } from '../app/AppInfoService.js';
2
2
  import type { DeviceInfoService } from '../device/DeviceInfoService.js';
3
3
  import type { NetworkInfoService } from '../network/NetworkInfoService.js';
4
- import type { AnonymousIdService } from '../../storage/AnonymousIdService.js';
5
4
  import type { EventPayload, TrackEventInput } from '../../types/EventTypes.js';
6
5
  import type { EventValidator } from '../../validation/EventValidator.js';
7
6
  interface EventBuilderConfig {
@@ -9,7 +8,6 @@ interface EventBuilderConfig {
9
8
  deviceInfo: DeviceInfoService;
10
9
  appInfo: AppInfoService;
11
10
  networkInfo: NetworkInfoService;
12
- anonymousId: AnonymousIdService;
13
11
  validator: EventValidator;
14
12
  externalUserId?: string;
15
13
  }
@@ -1,6 +1,6 @@
1
1
  import type { ApiClient } from '../network/ApiClient.js';
2
2
  import type { EventIngestionResponse, RecommendationResponse } from '../../types/ApiTypes.js';
3
- import type { EventPayload, EventProperties, TrackEventInput, TrackEventOptions } from '../../types/EventTypes.js';
3
+ import type { EventPayload, TrackEventInput } from '../../types/EventTypes.js';
4
4
  import type { EventBuilder } from './EventBuilder.js';
5
5
  import type { EventValidator } from '../../validation/EventValidator.js';
6
6
  interface EventServiceConfig {
@@ -13,7 +13,6 @@ export declare class EventService {
13
13
  private readonly builder;
14
14
  private readonly validator;
15
15
  constructor(config: EventServiceConfig);
16
- track(eventName: string, properties?: EventProperties, options?: TrackEventOptions): Promise<EventIngestionResponse>;
17
16
  trackBatch(events: readonly TrackEventInput[]): Promise<EventIngestionResponse>;
18
17
  sendPayloads(payloads: readonly EventPayload[]): Promise<EventIngestionResponse>;
19
18
  getRecommendations(input: TrackEventInput): Promise<RecommendationResponse>;
@@ -0,0 +1,17 @@
1
+ import type { NetworkStatusReader } from '../../platform/types.js';
2
+ export type NetworkState = 'ONLINE' | 'OFFLINE' | 'UNKNOWN';
3
+ export declare class NetworkService {
4
+ private readonly reader?;
5
+ private state;
6
+ private started;
7
+ private unsubscribeReader?;
8
+ private readonly listeners;
9
+ constructor(reader?: NetworkStatusReader | undefined);
10
+ getState(): NetworkState;
11
+ isAvailable(): boolean;
12
+ initialize(): Promise<void>;
13
+ subscribe(listener: () => void): () => void;
14
+ destroy(): void;
15
+ private applyState;
16
+ }
17
+ //# sourceMappingURL=NetworkService.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-test-adx",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Adaxon React Native / Expo SDK for event tracking and offer recommendations.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -1,31 +0,0 @@
1
- "use strict";
2
-
3
- import { ANONYMOUS_ID_KEY_PREFIX, sanitizeStorageId } from "../config/StorageKeys.js";
4
- import { createUuid } from "../utils/uuid.js";
5
- export class AnonymousIdService {
6
- constructor(appId, storage) {
7
- this.appId = appId;
8
- this.storage = storage;
9
- }
10
- getAnonymousId() {
11
- if (!this.anonymousIdPromise) {
12
- this.anonymousIdPromise = this.loadOrCreate().catch(error => {
13
- this.anonymousIdPromise = undefined;
14
- throw error;
15
- });
16
- }
17
- return this.anonymousIdPromise;
18
- }
19
- get storageKey() {
20
- return `${ANONYMOUS_ID_KEY_PREFIX}${sanitizeStorageId(this.appId)}`;
21
- }
22
- async loadOrCreate() {
23
- const stored = await this.storage?.getItem(this.storageKey);
24
- if (stored) {
25
- return stored;
26
- }
27
- const anonymousId = createUuid();
28
- await this.storage?.setItem(this.storageKey, anonymousId);
29
- return anonymousId;
30
- }
31
- }
@@ -1,11 +0,0 @@
1
- import type { AdaxonStorageAdapter } from '../types/AdaxonConfig.js';
2
- export declare class AnonymousIdService {
3
- private readonly appId;
4
- private readonly storage?;
5
- private anonymousIdPromise?;
6
- constructor(appId: string, storage?: AdaxonStorageAdapter | undefined);
7
- getAnonymousId(): Promise<string>;
8
- private get storageKey();
9
- private loadOrCreate;
10
- }
11
- //# sourceMappingURL=AnonymousIdService.d.ts.map