react-native-test-adx 0.1.2 → 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,8 +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
- `expo-secure-store` is **not** required. The SDK does not depend on it.
40
-
41
39
  ## Quick start (React Native)
42
40
 
43
41
  Wrap the app in `AdaxonProvider`. Screens use `useAdaxon()`.
@@ -52,7 +50,7 @@ The SDK owns the tracker host and API paths (`/event/tracking`, `/api/mts/recomm
52
50
  |---|---|---|
53
51
  | `appId` | **Yes** | None. MTS app id (`adx_…`). |
54
52
  | `sdkKey` | **Yes** | None. Sent as the `x-sdk-key` header. |
55
- | `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. |
56
54
  | `timeoutMs` | No | `10000` |
57
55
  | `testMode` | No | `false` (`sdk_version` gets a `-test` suffix when true) |
58
56
  | `externalUserId` | No | Unset |
@@ -146,40 +144,20 @@ function HomeScreen() {
146
144
 
147
145
  Do **not** pass device or app fields. The SDK attaches `device` and `app` automatically on every event.
148
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
+
149
151
  ### Storage
150
152
 
151
- `storage` is optional. You do not have to install `expo-secure-store` (or any other store) for the SDK to run. Only `appId` and `sdkKey` are required.
153
+ `storage` is optional. It persists the **event queue** (offline / pending events) and the **anonymous id**. Only `appId` and `sdkKey` are required.
152
154
 
153
155
  | What you pass | What happens |
154
156
  |---|---|
155
- | No `storage` | SDK uses built-in in-memory storage. Tracking works. Anonymous id and queued events are lost when the app closes. |
156
- | A storage adapter | Anonymous id and queued events survive restart. |
157
-
158
- The adapter is only `getItem` / `setItem` / `removeItem`. You do not implement the queue. Any store works: Expo SecureStore, AsyncStorage, MMKV, or your own.
159
-
160
- Optional SecureStore helper (not required):
161
-
162
- ```sh
163
- npx expo install expo-secure-store
164
- ```
165
-
166
- ```tsx
167
- import * as SecureStore from 'expo-secure-store';
168
-
169
- <AdaxonProvider
170
- configuration={{
171
- appId: 'adx_your_app_id',
172
- sdkKey: 'your_sdk_key',
173
- storage: {
174
- getItem: (key) => SecureStore.getItemAsync(key),
175
- setItem: (key, value) => SecureStore.setItemAsync(key, value),
176
- removeItem: (key) => SecureStore.deleteItemAsync(key),
177
- },
178
- }}
179
- >
180
- ```
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. |
181
159
 
182
- SecureStore has a small per-key size limit. For a large event queue, prefer AsyncStorage (or similar) and keep SecureStore for the anonymous id if you want.
160
+ The adapter is only `getItem` / `setItem` / `removeItem`. You do not implement the queue.
183
161
 
184
162
  ### Non-component boot
185
163
 
@@ -289,7 +267,7 @@ Values come from React Native `Platform` (`src/platform/reactNative.ts`). They a
289
267
  "app": {
290
268
  "package_name": "com.example.app",
291
269
  "app_version": "2.4.1",
292
- "sdk_version": "0.1.2"
270
+ "sdk_version": "0.1.3"
293
271
  }
294
272
  }
295
273
  ```
@@ -325,7 +303,7 @@ MTS tracker
325
303
  | `src/platform/` | Android/iOS fields. **Only `react-native` import** |
326
304
  | `src/config/` | Tracker host, API paths, timeouts, storage key rules |
327
305
  | `src/validation/` | Event name, property, and batch-size rules |
328
- | `src/storage/` | Anonymous user id and persistent event queue |
306
+ | `src/storage/` | Persistent event queue |
329
307
  | `example/src/` | Demo app: `App.tsx`, `screens/`, `components/` |
330
308
 
331
309
  Screens belong in the **host app**, not in the SDK.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- export const SDK_VERSION = '0.1.2';
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) {
@@ -11,7 +11,6 @@ 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
13
  import { NetworkService } from "../services/network/NetworkService.js";
14
- import { AnonymousIdService } from "../storage/AnonymousIdService.js";
15
14
  import { EventQueueStorage } from "../storage/EventQueueStorage.js";
16
15
  import { EventValidator } from "../validation/EventValidator.js";
17
16
  export class AdaxonClient {
@@ -27,10 +26,9 @@ export class AdaxonClient {
27
26
  const networkInfo = new NetworkInfoService(config.networkInfoReader);
28
27
  this.builder = new EventBuilder({
29
28
  appId: auth.appId,
30
- deviceInfo: new DeviceInfoService(config.platform ?? 'web', config.deviceInfoReader),
29
+ deviceInfo: new DeviceInfoService(config.platform ?? 'web', config.deviceInfoReader, config.storage, auth.appId),
31
30
  appInfo: new AppInfoService(Boolean(config.testMode), config.appInfoReader),
32
31
  networkInfo,
33
- anonymousId: new AnonymousIdService(auth.appId, config.storage),
34
32
  validator: this.validator,
35
33
  externalUserId: config.externalUserId
36
34
  });
@@ -7,6 +7,28 @@ import { appFieldsFromNative } from "./app.js";
7
7
  import { deviceFieldsFromAndroid } from "./android.js";
8
8
  import { deviceFieldsFromIos } from "./ios.js";
9
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,45 +49,21 @@ 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 reactNativeNetworkStatusReader = {
40
- getIsOnline: readIsOnlineFromNative,
41
- subscribe: subscribeNativeNetworkStatus
42
- };
43
- export const reactNativeUrlOpener = Linking;
52
+ async function readUniqueIdFromNative() {
53
+ const deviceInfo = nativeModule('RNDeviceInfo');
54
+ return nonEmptyString(await callNative(deviceInfo?.getUniqueId)) ?? '';
55
+ }
44
56
  function readNativeAppConstants() {
45
- const modules = NativeModules ?? {};
46
- const expoApp = modules.ExpoApplication;
47
- const expoConstants = modules.ExponentConstants;
48
- const deviceInfo = modules.RNDeviceInfo;
49
57
  return {
50
- ...deviceInfo,
51
- ...expoConstants,
52
- ...expoApp
58
+ ...nativeModule('RNDeviceInfo'),
59
+ ...nativeModule('ExponentConstants'),
60
+ ...nativeModule('ExpoApplication')
53
61
  };
54
62
  }
55
63
  function nativeNetworkModules() {
56
- const modules = NativeModules ?? {};
57
64
  return {
58
- netInfo: modules.RNCNetInfo,
59
- expoNetwork: modules.ExpoNetwork
60
- };
61
- }
62
- function safeUnsubscribe(remove) {
63
- return () => {
64
- try {
65
- remove?.();
66
- } catch {
67
- // Native unsubscribe can throw after the module is torn down.
68
- }
65
+ netInfo: nativeModule('RNCNetInfo'),
66
+ expoNetwork: nativeModule('ExpoNetwork')
69
67
  };
70
68
  }
71
69
  async function readIsOnlineFromNative() {
@@ -73,18 +71,11 @@ async function readIsOnlineFromNative() {
73
71
  netInfo,
74
72
  expoNetwork
75
73
  } = nativeNetworkModules();
76
- const getCurrentState = netInfo?.getCurrentState;
77
- if (typeof getCurrentState === 'function') {
78
- const online = isOnlineFromNativeState(await readOptionalNative(() => getCurrentState(null)));
79
- if (online !== null) {
80
- return online;
81
- }
74
+ const fromNetInfo = isOnlineFromNativeState(await callNative(netInfo?.getCurrentState, null));
75
+ if (fromNetInfo !== null) {
76
+ return fromNetInfo;
82
77
  }
83
- const getNetworkState = expoNetwork?.getNetworkStateAsync;
84
- if (typeof getNetworkState === 'function') {
85
- return isOnlineFromNativeState(await readOptionalNative(getNetworkState));
86
- }
87
- return null;
78
+ return isOnlineFromNativeState(await callNative(expoNetwork?.getNetworkStateAsync));
88
79
  }
89
80
  function subscribeNativeNetworkStatus(listener) {
90
81
  const {
@@ -99,9 +90,8 @@ function subscribeNativeNetworkStatus(listener) {
99
90
  const subscription = new NativeEventEmitter(NativeModules.RNCNetInfo).addListener('netInfo.networkStatusDidChange', onState);
100
91
  return safeUnsubscribe(() => subscription.remove());
101
92
  }
102
- const addListener = expoNetwork?.addListener;
103
- if (typeof addListener === 'function') {
104
- const subscription = addListener.call(expoNetwork, 'onNetworkStateChanged', onState);
93
+ if (expoNetwork && typeof expoNetwork.addListener === 'function') {
94
+ const subscription = expoNetwork.addListener('onNetworkStateChanged', onState);
105
95
  return safeUnsubscribe(() => subscription?.remove?.());
106
96
  }
107
97
  } catch (error) {
@@ -109,32 +99,35 @@ function subscribeNativeNetworkStatus(listener) {
109
99
  }
110
100
  return () => undefined;
111
101
  }
112
- async function readOptionalNative(read) {
113
- try {
114
- return await read();
115
- } catch {
116
- return undefined;
117
- }
118
- }
119
102
  async function collectFromNativeNetwork() {
120
103
  const network = {};
121
104
  const {
122
105
  netInfo,
123
106
  expoNetwork
124
107
  } = nativeNetworkModules();
125
- const getCurrentState = netInfo?.getCurrentState;
126
- if (typeof getCurrentState === 'function') {
127
- const state = await readOptionalNative(() => getCurrentState(null));
128
- setIfPresent(network, 'connection_type', connectionTypeFromNativeState(state));
129
- setIfPresent(network, 'ip_address', ipAddressFromNativeState(state));
130
- }
131
- const getNetworkState = expoNetwork?.getNetworkStateAsync;
132
- if (!network.connection_type && typeof getNetworkState === 'function') {
133
- setIfPresent(network, 'connection_type', connectionTypeFromNativeState(await readOptionalNative(getNetworkState)));
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)));
134
113
  }
135
- const getIpAddress = expoNetwork?.getIpAddressAsync;
136
- if (!network.ip_address && typeof getIpAddress === 'function') {
137
- 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))));
138
116
  }
139
117
  return network;
140
- }
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 {
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "0.1.2";
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,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;
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-test-adx",
3
- "version": "0.1.2",
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