react-native-test-adx 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/LICENSE +20 -0
- package/README.md +333 -0
- package/lib/module/AdaxonSdk.js +120 -0
- package/lib/module/components/AdaxonProvider.js +46 -0
- package/lib/module/config/ApiConstants.js +18 -0
- package/lib/module/config/StorageKeys.js +12 -0
- package/lib/module/core/AdaxonClient.js +88 -0
- package/lib/module/errors/AdaxonSdkError.js +23 -0
- package/lib/module/hooks/useAdaxon.js +35 -0
- package/lib/module/index.js +10 -0
- package/lib/module/package.json +1 -0
- package/lib/module/platform/android.js +10 -0
- package/lib/module/platform/app.js +18 -0
- package/lib/module/platform/ios.js +11 -0
- package/lib/module/platform/network.js +61 -0
- package/lib/module/platform/reactNative.js +78 -0
- package/lib/module/platform/types.js +3 -0
- package/lib/module/services/app/AppInfoService.js +25 -0
- package/lib/module/services/authentication/AuthenticationService.js +26 -0
- package/lib/module/services/device/DeviceInfoService.js +22 -0
- package/lib/module/services/event/BatchProcessor.js +64 -0
- package/lib/module/services/event/EventBuilder.js +43 -0
- package/lib/module/services/event/EventQueueService.js +35 -0
- package/lib/module/services/event/EventService.js +42 -0
- package/lib/module/services/event/normalizeProperties.js +42 -0
- package/lib/module/services/event/predefined.js +13 -0
- package/lib/module/services/network/ApiClient.js +62 -0
- package/lib/module/services/network/NetworkInfoService.js +69 -0
- package/lib/module/state/AdaxonContext.js +7 -0
- package/lib/module/storage/AnonymousIdService.js +31 -0
- package/lib/module/storage/EventQueueStorage.js +143 -0
- package/lib/module/storage/memoryStorage.js +16 -0
- package/lib/module/types/AdaxonConfig.js +3 -0
- package/lib/module/types/ApiTypes.js +1 -0
- package/lib/module/types/EventTypes.js +1 -0
- package/lib/module/types/QueuedEvent.js +3 -0
- package/lib/module/utils/locale.js +15 -0
- package/lib/module/utils/strings.js +17 -0
- package/lib/module/utils/uuid.js +13 -0
- package/lib/module/validation/EventValidator.js +63 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AdaxonSdk.d.ts +28 -0
- package/lib/typescript/src/components/AdaxonProvider.d.ts +11 -0
- package/lib/typescript/src/config/ApiConstants.d.ts +17 -0
- package/lib/typescript/src/config/StorageKeys.d.ts +5 -0
- package/lib/typescript/src/core/AdaxonClient.d.ts +29 -0
- package/lib/typescript/src/errors/AdaxonSdkError.d.ts +15 -0
- package/lib/typescript/src/hooks/useAdaxon.d.ts +19 -0
- package/lib/typescript/src/index.d.ts +19 -0
- package/lib/typescript/src/platform/android.d.ts +10 -0
- package/lib/typescript/src/platform/app.d.ts +14 -0
- package/lib/typescript/src/platform/ios.d.ts +8 -0
- package/lib/typescript/src/platform/network.d.ts +6 -0
- package/lib/typescript/src/platform/reactNative.d.ts +8 -0
- package/lib/typescript/src/platform/types.d.ts +15 -0
- package/lib/typescript/src/services/app/AppInfoService.d.ts +11 -0
- package/lib/typescript/src/services/authentication/AuthenticationService.d.ts +10 -0
- package/lib/typescript/src/services/device/DeviceInfoService.d.ts +10 -0
- package/lib/typescript/src/services/event/BatchProcessor.d.ts +19 -0
- package/lib/typescript/src/services/event/EventBuilder.d.ts +25 -0
- package/lib/typescript/src/services/event/EventQueueService.d.ts +13 -0
- package/lib/typescript/src/services/event/EventService.d.ts +23 -0
- package/lib/typescript/src/services/event/normalizeProperties.d.ts +26 -0
- package/lib/typescript/src/services/event/predefined.d.ts +14 -0
- package/lib/typescript/src/services/network/ApiClient.d.ts +14 -0
- package/lib/typescript/src/services/network/NetworkInfoService.d.ts +14 -0
- package/lib/typescript/src/state/AdaxonContext.d.ts +9 -0
- package/lib/typescript/src/storage/AnonymousIdService.d.ts +11 -0
- package/lib/typescript/src/storage/EventQueueStorage.d.ts +21 -0
- package/lib/typescript/src/storage/memoryStorage.d.ts +3 -0
- package/lib/typescript/src/types/AdaxonConfig.d.ts +30 -0
- package/lib/typescript/src/types/ApiTypes.d.ts +31 -0
- package/lib/typescript/src/types/EventTypes.d.ts +65 -0
- package/lib/typescript/src/types/QueuedEvent.d.ts +16 -0
- package/lib/typescript/src/utils/locale.d.ts +3 -0
- package/lib/typescript/src/utils/strings.d.ts +3 -0
- package/lib/typescript/src/utils/uuid.d.ts +3 -0
- package/lib/typescript/src/validation/EventValidator.d.ts +13 -0
- package/package.json +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ritesh Rana
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# react-native-adaxon
|
|
2
|
+
|
|
3
|
+
Adaxon React Native / Expo SDK for event tracking and offer recommendations.
|
|
4
|
+
|
|
5
|
+
The SDK talks to the MTS tracker over HTTPS. The app does not call Kafka or MySQL.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Mobile app → Adaxon SDK → HTTPS → MTS tracker → Kafka → MTS API → MySQL
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
### Production apps
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install react-native-adaxon
|
|
17
|
+
# or
|
|
18
|
+
yarn add react-native-adaxon
|
|
19
|
+
# or
|
|
20
|
+
npx expo install react-native-adaxon
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { AdaxonProvider, useAdaxon } from 'react-native-adaxon';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Testing / staging apps
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install react-native-test-adx
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { AdaxonProvider, useAdaxon } from 'react-native-test-adx';
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Both packages expose the same API. Only the npm name differs.
|
|
38
|
+
|
|
39
|
+
Recommended storage helper:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npx expo install expo-secure-store
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start (React Native)
|
|
46
|
+
|
|
47
|
+
Wrap the app in `AdaxonProvider`. Screens use `useAdaxon()`.
|
|
48
|
+
|
|
49
|
+
Tracking does **not** write React state, so `logEvent` / `track` will not re-render the tree.
|
|
50
|
+
|
|
51
|
+
### Configuration
|
|
52
|
+
|
|
53
|
+
The SDK owns the tracker host and API paths (`/event/tracking`, `/api/mts/recommendations`, `/click/tracking`). The app only provides credentials.
|
|
54
|
+
|
|
55
|
+
| Field | Required | Default |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `appId` | **Yes** | None. MTS app id (`adx_…`). |
|
|
58
|
+
| `sdkKey` | **Yes** | None. Sent as the `x-sdk-key` header. |
|
|
59
|
+
| `storage` | No | In-memory. Queue and anonymous id are lost on app restart. |
|
|
60
|
+
| `timeoutMs` | No | `10000` |
|
|
61
|
+
| `testMode` | No | `false` (`sdk_version` gets a `-test` suffix when true) |
|
|
62
|
+
| `externalUserId` | No | Unset |
|
|
63
|
+
|
|
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`.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import {
|
|
68
|
+
AdaxonProvider,
|
|
69
|
+
AdxEvent,
|
|
70
|
+
useAdaxon,
|
|
71
|
+
type RecommendationOffer,
|
|
72
|
+
} from 'react-native-adaxon';
|
|
73
|
+
import * as SecureStore from 'expo-secure-store';
|
|
74
|
+
import { Button, Text, View } from 'react-native';
|
|
75
|
+
|
|
76
|
+
function App() {
|
|
77
|
+
return (
|
|
78
|
+
<AdaxonProvider
|
|
79
|
+
configuration={{
|
|
80
|
+
appId: 'adx_your_app_id',
|
|
81
|
+
sdkKey: 'your_sdk_key',
|
|
82
|
+
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
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<HomeScreen />
|
|
91
|
+
</AdaxonProvider>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function HomeScreen() {
|
|
96
|
+
const {
|
|
97
|
+
isInitialized,
|
|
98
|
+
logEvent,
|
|
99
|
+
trackPurchase,
|
|
100
|
+
trackAddToCart,
|
|
101
|
+
loadRecommendations,
|
|
102
|
+
showOffer,
|
|
103
|
+
} = useAdaxon();
|
|
104
|
+
|
|
105
|
+
if (!isInitialized) {
|
|
106
|
+
return <Text>Initializing…</Text>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<View>
|
|
111
|
+
<Button
|
|
112
|
+
title="Custom event"
|
|
113
|
+
onPress={() =>
|
|
114
|
+
logEvent('level_completed', {
|
|
115
|
+
level_number: 10,
|
|
116
|
+
coins_used: 50,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
/>
|
|
120
|
+
<Button
|
|
121
|
+
title="Purchase"
|
|
122
|
+
onPress={() =>
|
|
123
|
+
trackPurchase({
|
|
124
|
+
order_id: 'ORD123',
|
|
125
|
+
amount: 999,
|
|
126
|
+
currency: 'INR',
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
/>
|
|
130
|
+
<Button
|
|
131
|
+
title="Add to cart"
|
|
132
|
+
onPress={() =>
|
|
133
|
+
trackAddToCart({
|
|
134
|
+
product_id: 'P123',
|
|
135
|
+
price: 499,
|
|
136
|
+
currency: 'INR',
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
140
|
+
<Button
|
|
141
|
+
title="Load offers"
|
|
142
|
+
onPress={async () => {
|
|
143
|
+
const offers: RecommendationOffer[] = await loadRecommendations(
|
|
144
|
+
'recommendation_view',
|
|
145
|
+
{ placement: 'home' }
|
|
146
|
+
);
|
|
147
|
+
if (offers[0]) {
|
|
148
|
+
await showOffer(offers[0]);
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
</View>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Do **not** pass device or app fields. The SDK attaches `device` and `app` automatically on every event.
|
|
158
|
+
|
|
159
|
+
### Non-component boot
|
|
160
|
+
|
|
161
|
+
`Adaxon.initialize()` is still available if you are not inside a React tree. Prefer the provider in React Native apps.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { Adaxon } from 'react-native-adaxon';
|
|
165
|
+
|
|
166
|
+
await Adaxon.initialize({
|
|
167
|
+
appId: 'adx_your_app_id',
|
|
168
|
+
sdkKey: 'your_sdk_key',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await Adaxon.logEvent('level_completed', { level_number: 10 });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## API
|
|
175
|
+
|
|
176
|
+
| Method | What it does |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `AdaxonProvider` | Starts the SDK once when the app mounts |
|
|
179
|
+
| `useAdaxon()` | Hook for screens: track, recommendations, offers |
|
|
180
|
+
| `logEvent(name, values)` | Custom or predefined event |
|
|
181
|
+
| `trackPurchase(params)` | Sends `adx_purchase` |
|
|
182
|
+
| `trackAddToCart(params)` | Sends `adx_add_to_cart` |
|
|
183
|
+
| `trackLogin(params)` | Sends `adx_login` |
|
|
184
|
+
| `trackCompleteRegistration(params)` | Sends `adx_complete_registration` |
|
|
185
|
+
| `track` / `trackBatch` | Send immediately. `trackBatch` accepts 1–100 events |
|
|
186
|
+
| `enqueueEvent(name, properties)` | Store an event locally; does not send yet |
|
|
187
|
+
| `processPendingEvents()` | Send queued events in batches of up to 100 |
|
|
188
|
+
| `loadRecommendations(eventName, properties)` | POST `/api/mts/recommendations` |
|
|
189
|
+
| `showOffer(offer)` | Opens the offer URL and tracks `offer_opened` |
|
|
190
|
+
| `setExternalUserId(id)` | Attaches `external_user_id` on later events |
|
|
191
|
+
|
|
192
|
+
Predefined names: `AdxEvent.PURCHASE`, `AdxEvent.ADD_TO_CART`, `AdxEvent.LOGIN`, `AdxEvent.COMPLETE_REGISTRATION`.
|
|
193
|
+
|
|
194
|
+
## Event queue and batch processing
|
|
195
|
+
|
|
196
|
+
`track` and `trackBatch` send immediately. Use `enqueueEvent` / `processPendingEvents` when events should wait in the local queue.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const { enqueueEvent, processPendingEvents } = useAdaxon();
|
|
200
|
+
|
|
201
|
+
await enqueueEvent('level_completed', { level_number: 10 });
|
|
202
|
+
const result = await processPendingEvents();
|
|
203
|
+
// result.status: 'completed' | 'stopped' | 'already_processing'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
How it works:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
Persistent event queue
|
|
210
|
+
↓
|
|
211
|
+
Get next ≤ 100 events (FIFO)
|
|
212
|
+
↓
|
|
213
|
+
POST /event/tracking
|
|
214
|
+
↓
|
|
215
|
+
Success → remove those events → next batch
|
|
216
|
+
Failure → keep events → stop (retry later)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Rules:
|
|
220
|
+
|
|
221
|
+
- Pending events are stored through the `storage` adapter, so they survive app restart.
|
|
222
|
+
- Events are processed in creation order (FIFO).
|
|
223
|
+
- Each request contains at most **100** events. A queue of 250 is sent as 100 + 100 + 50.
|
|
224
|
+
- Events are removed only after the API confirms success (`success: true`). On network, HTTP, or unknown errors they stay in the queue.
|
|
225
|
+
- Only one processor runs at a time for the same app. A second `processPendingEvents()` call returns `already_processing`.
|
|
226
|
+
- 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.
|
|
228
|
+
- Pass `removeItem` on the storage adapter so successful events can be deleted. If it is omitted, the SDK writes an empty value instead.
|
|
229
|
+
|
|
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.
|
|
231
|
+
|
|
232
|
+
## Automatic device information
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"device": {
|
|
237
|
+
"platform": "android",
|
|
238
|
+
"os_version": "14",
|
|
239
|
+
"brand": "Samsung",
|
|
240
|
+
"model": "S23",
|
|
241
|
+
"language": "en",
|
|
242
|
+
"timezone": "Asia/Kolkata"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Values come from React Native `Platform` (`src/platform/reactNative.ts`). They are read **once** and cached. Missing optional fields are omitted and never block the request.
|
|
248
|
+
|
|
249
|
+
## Automatic app information
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"app": {
|
|
254
|
+
"package_name": "com.example.app",
|
|
255
|
+
"app_version": "2.4.1",
|
|
256
|
+
"sdk_version": "0.1.1"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`package_name` and `app_version` are read from native application APIs (Android package / iOS bundle). `sdk_version` is always set by the SDK. Do **not** pass these fields in `logEvent`, `track`, or `trackBatch` — every event, including each item in a batch, receives the same `app` object automatically. Missing optional fields are omitted and never block the request.
|
|
262
|
+
|
|
263
|
+
## How the folders work
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
App screen
|
|
267
|
+
│ useAdaxon().logEvent(...) or enqueueEvent(...) → processPendingEvents()
|
|
268
|
+
▼
|
|
269
|
+
components/ + hooks/ + state/ React layer (init only)
|
|
270
|
+
▼
|
|
271
|
+
AdaxonSdk.ts Public methods
|
|
272
|
+
▼
|
|
273
|
+
core/AdaxonClient.ts Wires services
|
|
274
|
+
▼
|
|
275
|
+
services/ Validate, build JSON, queue, batch, HTTPS
|
|
276
|
+
▼
|
|
277
|
+
MTS tracker
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
| Folder | Role |
|
|
281
|
+
|---|---|
|
|
282
|
+
| `src/index.ts` | Public exports (`Adaxon`, `AdaxonProvider`, `useAdaxon`) |
|
|
283
|
+
| `src/AdaxonSdk.ts` | SDK facade (`initialize`, `logEvent`, queue, offers) |
|
|
284
|
+
| `src/components/` | `AdaxonProvider` |
|
|
285
|
+
| `src/hooks/` | `useAdaxon()` |
|
|
286
|
+
| `src/state/` | Init status only — no event list in React state |
|
|
287
|
+
| `src/core/` | `AdaxonClient` — connects auth, events, queue, HTTP |
|
|
288
|
+
| `src/services/` | Auth, device, app, event build/send, batch processor, network |
|
|
289
|
+
| `src/platform/` | Android/iOS fields. **Only `react-native` import** |
|
|
290
|
+
| `src/config/` | Tracker host, API paths, timeouts, storage key rules |
|
|
291
|
+
| `src/validation/` | Event name, property, and batch-size rules |
|
|
292
|
+
| `src/storage/` | Anonymous user id and persistent event queue |
|
|
293
|
+
| `example/src/` | Demo app: `App.tsx`, `screens/`, `components/` |
|
|
294
|
+
|
|
295
|
+
Screens belong in the **host app**, not in the SDK.
|
|
296
|
+
|
|
297
|
+
Longer walkthrough for managers and teammates: [ARCHITECTURE.md](./ARCHITECTURE.md).
|
|
298
|
+
Framework cost vs vanilla JS is checked by `src/__tests__/performance.framework.test.tsx`.
|
|
299
|
+
|
|
300
|
+
## Android notes
|
|
301
|
+
|
|
302
|
+
Credential `platform` in Adaxon must allow Android (`android` or `react_native`).
|
|
303
|
+
|
|
304
|
+
## Local development (this repo)
|
|
305
|
+
|
|
306
|
+
```sh
|
|
307
|
+
cd adaxon-sdk
|
|
308
|
+
corepack enable
|
|
309
|
+
yarn
|
|
310
|
+
yarn typecheck
|
|
311
|
+
yarn test --watchman=false
|
|
312
|
+
yarn prepare
|
|
313
|
+
yarn example android:emulator
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Publish to npm
|
|
317
|
+
|
|
318
|
+
```sh
|
|
319
|
+
npm login
|
|
320
|
+
yarn publish:test:dry # dry-run testing package
|
|
321
|
+
yarn publish:test # publishes react-native-test-adx
|
|
322
|
+
yarn publish:prod:dry # dry-run production package
|
|
323
|
+
yarn publish:prod # publishes react-native-adaxon
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
| Environment | Install |
|
|
327
|
+
|---|---|
|
|
328
|
+
| Testing | `npm install react-native-test-adx` |
|
|
329
|
+
| Production | `npm install react-native-adaxon` |
|
|
330
|
+
|
|
331
|
+
## License
|
|
332
|
+
|
|
333
|
+
MIT
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { AdaxonClient } from "./core/AdaxonClient.js";
|
|
4
|
+
import { MEDIATION_PROVIDER, OFFER_OPENED_EVENT } from "./config/ApiConstants.js";
|
|
5
|
+
import { AdaxonSdkError } from "./errors/AdaxonSdkError.js";
|
|
6
|
+
import { detectReactNativePlatform, reactNativeAppInfoReader, reactNativeDeviceReader, reactNativeNetworkInfoReader, reactNativeUrlOpener } from "./platform/reactNative.js";
|
|
7
|
+
import { buildAddToCartProperties, buildPurchaseProperties, normalizeEventValues } from "./services/event/normalizeProperties.js";
|
|
8
|
+
import { AdxEvent } from "./services/event/predefined.js";
|
|
9
|
+
export class AdaxonSdk {
|
|
10
|
+
static sharedInstance = new AdaxonSdk();
|
|
11
|
+
client = null;
|
|
12
|
+
testMode = false;
|
|
13
|
+
static get shared() {
|
|
14
|
+
return AdaxonSdk.sharedInstance;
|
|
15
|
+
}
|
|
16
|
+
isInitialized() {
|
|
17
|
+
return this.client !== null;
|
|
18
|
+
}
|
|
19
|
+
async initialize(configuration, listener) {
|
|
20
|
+
try {
|
|
21
|
+
this.client = new AdaxonClient({
|
|
22
|
+
...configuration,
|
|
23
|
+
platform: detectReactNativePlatform(),
|
|
24
|
+
deviceInfoReader: reactNativeDeviceReader,
|
|
25
|
+
appInfoReader: reactNativeAppInfoReader,
|
|
26
|
+
networkInfoReader: reactNativeNetworkInfoReader
|
|
27
|
+
});
|
|
28
|
+
this.testMode = Boolean(configuration.testMode);
|
|
29
|
+
listener?.onInitializationComplete();
|
|
30
|
+
this.client.processPendingEvents().catch(() => undefined);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
this.reset();
|
|
33
|
+
const message = AdaxonSdkError.messageOf(error, 'Initialization failed.');
|
|
34
|
+
const code = error instanceof AdaxonSdkError && error.code === 'INVALID_CONFIG' ? 'INVALID_CONFIG' : 'UNKNOWN';
|
|
35
|
+
listener?.onInitializationFailed(code, message);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async track(eventName, properties = {}, options = {}) {
|
|
40
|
+
return this.requireClient().track(eventName, properties, options);
|
|
41
|
+
}
|
|
42
|
+
async trackBatch(events) {
|
|
43
|
+
return this.requireClient().trackBatch(events);
|
|
44
|
+
}
|
|
45
|
+
async enqueueEvent(eventName, properties = {}, options = {}) {
|
|
46
|
+
await this.requireClient().enqueueEvent(eventName, properties, options);
|
|
47
|
+
}
|
|
48
|
+
async processPendingEvents() {
|
|
49
|
+
return this.requireClient().processPendingEvents();
|
|
50
|
+
}
|
|
51
|
+
async logEvent(eventName, values = {}, options = {}) {
|
|
52
|
+
return this.track(eventName, normalizeEventValues(values), options);
|
|
53
|
+
}
|
|
54
|
+
async trackPurchase(params, options = {}) {
|
|
55
|
+
return this.logEvent(AdxEvent.PURCHASE, buildPurchaseProperties(params), options);
|
|
56
|
+
}
|
|
57
|
+
async trackAddToCart(params, options = {}) {
|
|
58
|
+
return this.logEvent(AdxEvent.ADD_TO_CART, buildAddToCartProperties(params), options);
|
|
59
|
+
}
|
|
60
|
+
async trackLogin(params = {}, options = {}) {
|
|
61
|
+
return this.logEvent(AdxEvent.LOGIN, params, options);
|
|
62
|
+
}
|
|
63
|
+
async trackCompleteRegistration(params = {}, options = {}) {
|
|
64
|
+
return this.logEvent(AdxEvent.COMPLETE_REGISTRATION, params, options);
|
|
65
|
+
}
|
|
66
|
+
async loadRecommendations(eventName, properties = {}, listener) {
|
|
67
|
+
try {
|
|
68
|
+
const response = await this.requireClient().getRecommendations({
|
|
69
|
+
eventName,
|
|
70
|
+
properties: {
|
|
71
|
+
...properties,
|
|
72
|
+
mediation_provider: MEDIATION_PROVIDER,
|
|
73
|
+
test_mode: this.testMode
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
listener?.onRecommendationsLoaded(response.offers);
|
|
77
|
+
return response.offers;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const message = AdaxonSdkError.messageOf(error, 'Failed to load recommendations.');
|
|
80
|
+
const code = error instanceof AdaxonSdkError ? error.code : undefined;
|
|
81
|
+
listener?.onRecommendationsFailedToLoad(message, code);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async showOffer(offer, listener) {
|
|
86
|
+
try {
|
|
87
|
+
const client = this.requireClient();
|
|
88
|
+
const targetUrl = offer.offer_url;
|
|
89
|
+
const canOpen = await reactNativeUrlOpener.canOpenURL(targetUrl);
|
|
90
|
+
if (!canOpen) {
|
|
91
|
+
throw new AdaxonSdkError('VALIDATION_ERROR', `Cannot open offer URL: ${targetUrl}`);
|
|
92
|
+
}
|
|
93
|
+
await reactNativeUrlOpener.openURL(targetUrl);
|
|
94
|
+
await client.track(OFFER_OPENED_EVENT, {
|
|
95
|
+
offer_id: offer.offer_id,
|
|
96
|
+
offer_name: offer.offer_name,
|
|
97
|
+
ad_type: offer.ad_type,
|
|
98
|
+
mediation_provider: MEDIATION_PROVIDER
|
|
99
|
+
});
|
|
100
|
+
listener?.onOfferOpened(offer);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const message = AdaxonSdkError.messageOf(error, 'Failed to open offer.');
|
|
103
|
+
listener?.onOfferFailedToOpen(offer, message);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
setExternalUserId(externalUserId) {
|
|
108
|
+
this.requireClient().setExternalUserId(externalUserId);
|
|
109
|
+
}
|
|
110
|
+
requireClient() {
|
|
111
|
+
if (!this.client) {
|
|
112
|
+
throw new AdaxonSdkError('INVALID_CONFIG', 'Adaxon SDK is not initialized. Call Adaxon.initialize(...) first.');
|
|
113
|
+
}
|
|
114
|
+
return this.client;
|
|
115
|
+
}
|
|
116
|
+
reset() {
|
|
117
|
+
this.client = null;
|
|
118
|
+
this.testMode = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { AdaxonSdk } from "../AdaxonSdk.js";
|
|
5
|
+
import { AdaxonSdkError } from "../errors/AdaxonSdkError.js";
|
|
6
|
+
import { AdaxonContext } from "../state/AdaxonContext.js";
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
export function AdaxonProvider({
|
|
9
|
+
configuration,
|
|
10
|
+
listener,
|
|
11
|
+
sdk = AdaxonSdk.shared,
|
|
12
|
+
children
|
|
13
|
+
}) {
|
|
14
|
+
const [isInitialized, setIsInitialized] = useState(sdk.isInitialized());
|
|
15
|
+
const [initializationError, setInitializationError] = useState(null);
|
|
16
|
+
const configurationRef = useRef(configuration);
|
|
17
|
+
const listenerRef = useRef(listener);
|
|
18
|
+
configurationRef.current = configuration;
|
|
19
|
+
listenerRef.current = listener;
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
sdk.initialize(configurationRef.current, listenerRef.current).then(() => {
|
|
23
|
+
if (!cancelled) {
|
|
24
|
+
setIsInitialized(true);
|
|
25
|
+
setInitializationError(null);
|
|
26
|
+
}
|
|
27
|
+
}).catch(error => {
|
|
28
|
+
if (!cancelled) {
|
|
29
|
+
setIsInitialized(false);
|
|
30
|
+
setInitializationError(AdaxonSdkError.messageOf(error, 'Initialization failed.'));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return () => {
|
|
34
|
+
cancelled = true;
|
|
35
|
+
};
|
|
36
|
+
}, [sdk]);
|
|
37
|
+
const value = useMemo(() => ({
|
|
38
|
+
isInitialized,
|
|
39
|
+
initializationError,
|
|
40
|
+
sdk
|
|
41
|
+
}), [initializationError, isInitialized, sdk]);
|
|
42
|
+
return /*#__PURE__*/_jsx(AdaxonContext.Provider, {
|
|
43
|
+
value: value,
|
|
44
|
+
children: children
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export const SDK_VERSION = '0.1.1';
|
|
4
|
+
export const MEDIATION_PROVIDER = 'mts';
|
|
5
|
+
export const BASE_URL = 'http://localhost:3002';
|
|
6
|
+
export const EVENT_TRACKING_PATH = '/event/tracking';
|
|
7
|
+
export const RECOMMENDATIONS_PATH = '/api/mts/recommendations';
|
|
8
|
+
export const CLICK_TRACKING_PATH = '/click/tracking';
|
|
9
|
+
export const OFFER_OPENED_EVENT = 'offer_opened';
|
|
10
|
+
export const DEFAULT_TIMEOUT_MS = 10_000;
|
|
11
|
+
export const PUBLIC_IP_LOOKUP_URL = 'https://api.ipify.org?format=json';
|
|
12
|
+
export const PUBLIC_IP_LOOKUP_TIMEOUT_MS = 2_000;
|
|
13
|
+
export const MAX_BATCH_SIZE = 100;
|
|
14
|
+
export const MAX_EVENT_NAME_LENGTH = 100;
|
|
15
|
+
export const MAX_PROPERTIES = 50;
|
|
16
|
+
export const MAX_PROPERTY_KEY_LENGTH = 100;
|
|
17
|
+
export const MAX_PROPERTY_VALUE_LENGTH = 1000;
|
|
18
|
+
export const EVENT_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export const ANONYMOUS_ID_KEY_PREFIX = 'adaxon_sdk.anonymous_id.';
|
|
4
|
+
export const EVENT_QUEUE_META_KEY_PREFIX = 'adaxon_sdk.event_queue.meta.';
|
|
5
|
+
export const EVENT_QUEUE_ITEM_KEY_PREFIX = 'adaxon_sdk.event_queue.item.';
|
|
6
|
+
|
|
7
|
+
// Expo SecureStore and Keychain-backed adapters only accept alphanumerics
|
|
8
|
+
// plus `.`, `-` and `_`, so app ids are sanitized before use as a key.
|
|
9
|
+
const UNSAFE_STORAGE_KEY_CHARS = /[^A-Za-z0-9._-]/g;
|
|
10
|
+
export function sanitizeStorageId(appId) {
|
|
11
|
+
return appId.replace(UNSAFE_STORAGE_KEY_CHARS, '_');
|
|
12
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { BASE_URL, CLICK_TRACKING_PATH } from "../config/ApiConstants.js";
|
|
4
|
+
import { AuthenticationService } from "../services/authentication/AuthenticationService.js";
|
|
5
|
+
import { AppInfoService } from "../services/app/AppInfoService.js";
|
|
6
|
+
import { DeviceInfoService } from "../services/device/DeviceInfoService.js";
|
|
7
|
+
import { BatchProcessor } from "../services/event/BatchProcessor.js";
|
|
8
|
+
import { EventBuilder } from "../services/event/EventBuilder.js";
|
|
9
|
+
import { EventQueueService } from "../services/event/EventQueueService.js";
|
|
10
|
+
import { EventService } from "../services/event/EventService.js";
|
|
11
|
+
import { ApiClient } from "../services/network/ApiClient.js";
|
|
12
|
+
import { NetworkInfoService } from "../services/network/NetworkInfoService.js";
|
|
13
|
+
import { AnonymousIdService } from "../storage/AnonymousIdService.js";
|
|
14
|
+
import { EventQueueStorage } from "../storage/EventQueueStorage.js";
|
|
15
|
+
import { EventValidator } from "../validation/EventValidator.js";
|
|
16
|
+
export class AdaxonClient {
|
|
17
|
+
constructor(config) {
|
|
18
|
+
const auth = new AuthenticationService(config);
|
|
19
|
+
this.validator = new EventValidator();
|
|
20
|
+
const api = new ApiClient({
|
|
21
|
+
sdkKey: auth.sdkKey,
|
|
22
|
+
timeoutMs: auth.timeoutMs,
|
|
23
|
+
fetch: config.fetch
|
|
24
|
+
});
|
|
25
|
+
const networkInfo = new NetworkInfoService(config.networkInfoReader);
|
|
26
|
+
this.builder = new EventBuilder({
|
|
27
|
+
appId: auth.appId,
|
|
28
|
+
deviceInfo: new DeviceInfoService(config.platform ?? 'web', config.deviceInfoReader),
|
|
29
|
+
appInfo: new AppInfoService(Boolean(config.testMode), config.appInfoReader),
|
|
30
|
+
networkInfo,
|
|
31
|
+
anonymousId: new AnonymousIdService(auth.appId, config.storage),
|
|
32
|
+
validator: this.validator,
|
|
33
|
+
externalUserId: config.externalUserId
|
|
34
|
+
});
|
|
35
|
+
this.events = new EventService({
|
|
36
|
+
api,
|
|
37
|
+
builder: this.builder,
|
|
38
|
+
validator: this.validator
|
|
39
|
+
});
|
|
40
|
+
this.eventQueue = new EventQueueService(new EventQueueStorage(auth.appId, config.storage));
|
|
41
|
+
this.batchProcessor = new BatchProcessor({
|
|
42
|
+
queue: this.eventQueue,
|
|
43
|
+
sendBatch: payloads => this.events.sendPayloads(payloads)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
track(eventName, properties = {}, options = {}) {
|
|
47
|
+
return this.events.track(eventName, properties, options);
|
|
48
|
+
}
|
|
49
|
+
trackBatch(events) {
|
|
50
|
+
return this.events.trackBatch(events);
|
|
51
|
+
}
|
|
52
|
+
async enqueueEvent(eventName, properties = {}, options = {}) {
|
|
53
|
+
const payload = await this.builder.build({
|
|
54
|
+
eventName,
|
|
55
|
+
properties,
|
|
56
|
+
...options
|
|
57
|
+
});
|
|
58
|
+
await this.eventQueue.add(payload);
|
|
59
|
+
}
|
|
60
|
+
processPendingEvents() {
|
|
61
|
+
return this.batchProcessor.process();
|
|
62
|
+
}
|
|
63
|
+
getRecommendations(input) {
|
|
64
|
+
return this.events.getRecommendations(input);
|
|
65
|
+
}
|
|
66
|
+
createClickUrl(attribution) {
|
|
67
|
+
this.validator.validateClickAttribution(attribution);
|
|
68
|
+
const query = this.toQueryString({
|
|
69
|
+
offer_id: attribution.offerId,
|
|
70
|
+
affiliate_id: attribution.affiliateId,
|
|
71
|
+
advertiser_id: attribution.advertiserId,
|
|
72
|
+
aff_click_id: attribution.affClickId,
|
|
73
|
+
source: attribution.source,
|
|
74
|
+
aff_sub: attribution.affSub,
|
|
75
|
+
aff_sub2: attribution.affSub2,
|
|
76
|
+
aff_sub3: attribution.affSub3,
|
|
77
|
+
aff_sub4: attribution.affSub4,
|
|
78
|
+
aff_sub5: attribution.affSub5
|
|
79
|
+
});
|
|
80
|
+
return `${BASE_URL}${CLICK_TRACKING_PATH}?${query}`;
|
|
81
|
+
}
|
|
82
|
+
setExternalUserId(externalUserId) {
|
|
83
|
+
this.builder.setExternalUserId(externalUserId);
|
|
84
|
+
}
|
|
85
|
+
toQueryString(params) {
|
|
86
|
+
return Object.entries(params).filter(([, value]) => value !== undefined).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join('&');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export class AdaxonSdkError extends Error {
|
|
4
|
+
constructor(code, message, options = {}) {
|
|
5
|
+
super(message, {
|
|
6
|
+
cause: options.cause
|
|
7
|
+
});
|
|
8
|
+
this.name = 'AdaxonSdkError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = options.status;
|
|
11
|
+
this.details = options.details;
|
|
12
|
+
}
|
|
13
|
+
static messageOf(error, fallback) {
|
|
14
|
+
return error instanceof Error ? error.message : fallback;
|
|
15
|
+
}
|
|
16
|
+
static fromHttp(status, body) {
|
|
17
|
+
const code = status === 401 ? 'AUTHENTICATION_ERROR' : 'HTTP_ERROR';
|
|
18
|
+
return new AdaxonSdkError(code, body?.message || `Adaxon request failed with HTTP ${status}.`, {
|
|
19
|
+
status,
|
|
20
|
+
details: body?.errors ?? body
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|