@simplr-ai/react-native 1.1.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/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/chunk-FCBLJE3T.js +985 -0
- package/dist/chunk-FCBLJE3T.js.map +1 -0
- package/dist/flags-CwhHmaHQ.d.cts +606 -0
- package/dist/flags-CwhHmaHQ.d.ts +606 -0
- package/dist/hooks/index.cjs +1104 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.cts +65 -0
- package/dist/hooks/index.d.ts +65 -0
- package/dist/hooks/index.js +125 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.cjs +1011 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
7
|
+
}) : x)(function(x) {
|
|
8
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// src/errors.ts
|
|
13
|
+
var SimplrError = class _SimplrError extends Error {
|
|
14
|
+
constructor(message, status, body) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "SimplrError";
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.body = body;
|
|
19
|
+
Object.setPrototypeOf(this, _SimplrError.prototype);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/http.ts
|
|
24
|
+
async function apiRequest(cfg, method, path, body) {
|
|
25
|
+
const fetchImpl = cfg.fetchImpl ?? globalThis.fetch;
|
|
26
|
+
if (typeof fetchImpl !== "function") {
|
|
27
|
+
throw new SimplrError("global fetch is not available in this environment", 0, null);
|
|
28
|
+
}
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetchImpl(`${cfg.baseUrl}${path}`, {
|
|
33
|
+
method,
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
...cfg.authHeaders
|
|
37
|
+
},
|
|
38
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
39
|
+
signal: controller.signal
|
|
40
|
+
});
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
45
|
+
} catch {
|
|
46
|
+
parsed = text;
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const message = parsed && (parsed.message || parsed.error) || `Simplr API error ${res.status}`;
|
|
50
|
+
throw new SimplrError(message, res.status, parsed);
|
|
51
|
+
}
|
|
52
|
+
return parsed && typeof parsed === "object" && "content" in parsed ? parsed.content : parsed;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err instanceof SimplrError) throw err;
|
|
55
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
56
|
+
throw new SimplrError(`Request to ${path} timed out after ${cfg.timeoutMs}ms`, 0, null);
|
|
57
|
+
}
|
|
58
|
+
throw new SimplrError(err instanceof Error ? err.message : "Network error", 0, null);
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function normalizeBaseUrl(url) {
|
|
64
|
+
return url.replace(/\/+$/, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/hash.ts
|
|
68
|
+
function murmurHash3(input, seed = 0) {
|
|
69
|
+
let h1 = seed;
|
|
70
|
+
const c1 = 3432918353;
|
|
71
|
+
const c2 = 461845907;
|
|
72
|
+
for (let i = 0; i < input.length; i++) {
|
|
73
|
+
let k1 = input.charCodeAt(i);
|
|
74
|
+
k1 = Math.imul(k1, c1);
|
|
75
|
+
k1 = k1 << 15 | k1 >>> 17;
|
|
76
|
+
k1 = Math.imul(k1, c2);
|
|
77
|
+
h1 ^= k1;
|
|
78
|
+
h1 = h1 << 13 | h1 >>> 19;
|
|
79
|
+
h1 = Math.imul(h1, 5) + 3864292196;
|
|
80
|
+
}
|
|
81
|
+
h1 ^= input.length;
|
|
82
|
+
h1 ^= h1 >>> 16;
|
|
83
|
+
h1 = Math.imul(h1, 2246822507);
|
|
84
|
+
h1 ^= h1 >>> 13;
|
|
85
|
+
h1 = Math.imul(h1, 3266489909);
|
|
86
|
+
h1 ^= h1 >>> 16;
|
|
87
|
+
return h1 >>> 0;
|
|
88
|
+
}
|
|
89
|
+
function createFingerprintHash(components) {
|
|
90
|
+
const sortedKeys = Object.keys(components).sort();
|
|
91
|
+
const values = sortedKeys.map((key) => `${key}:${components[key] ?? "null"}`);
|
|
92
|
+
return murmurHash3(values.join("|")).toString(16).padStart(8, "0");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/device/storage.ts
|
|
96
|
+
var STORAGE_KEY = "simplr_device_id";
|
|
97
|
+
var STORAGE_TIMESTAMP_KEY = "simplr_device_id_ts";
|
|
98
|
+
var asyncStorage;
|
|
99
|
+
function getAsyncStorage() {
|
|
100
|
+
if (asyncStorage !== void 0) return asyncStorage;
|
|
101
|
+
try {
|
|
102
|
+
const mod = __require("@react-native-async-storage/async-storage");
|
|
103
|
+
const candidate = mod && (mod.default || mod);
|
|
104
|
+
asyncStorage = candidate && typeof candidate.getItem === "function" ? candidate : null;
|
|
105
|
+
} catch {
|
|
106
|
+
asyncStorage = null;
|
|
107
|
+
}
|
|
108
|
+
return asyncStorage;
|
|
109
|
+
}
|
|
110
|
+
var memoryDeviceId = null;
|
|
111
|
+
var memoryCreatedAt = null;
|
|
112
|
+
function generateDeviceId() {
|
|
113
|
+
const timestamp = Date.now().toString(36);
|
|
114
|
+
const r1 = Math.random().toString(36).substring(2, 15);
|
|
115
|
+
const r2 = Math.random().toString(36).substring(2, 15);
|
|
116
|
+
return `${timestamp}-${r1}-${r2}`;
|
|
117
|
+
}
|
|
118
|
+
async function getDeviceId() {
|
|
119
|
+
const store = getAsyncStorage();
|
|
120
|
+
if (store) {
|
|
121
|
+
try {
|
|
122
|
+
let id = await store.getItem(STORAGE_KEY);
|
|
123
|
+
let createdAt = await store.getItem(STORAGE_TIMESTAMP_KEY);
|
|
124
|
+
if (!id) {
|
|
125
|
+
id = generateDeviceId();
|
|
126
|
+
createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
127
|
+
await store.setItem(STORAGE_KEY, id);
|
|
128
|
+
await store.setItem(STORAGE_TIMESTAMP_KEY, createdAt);
|
|
129
|
+
}
|
|
130
|
+
return { id, createdAt: createdAt ?? null };
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!memoryDeviceId) {
|
|
135
|
+
memoryDeviceId = generateDeviceId();
|
|
136
|
+
memoryCreatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
137
|
+
}
|
|
138
|
+
return { id: memoryDeviceId, createdAt: memoryCreatedAt };
|
|
139
|
+
}
|
|
140
|
+
function getReactNative() {
|
|
141
|
+
try {
|
|
142
|
+
const rn = __require("react-native");
|
|
143
|
+
return rn?.default || rn || {};
|
|
144
|
+
} catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function safeTimezone() {
|
|
149
|
+
try {
|
|
150
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
151
|
+
} catch {
|
|
152
|
+
return "UTC";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function safeLocale() {
|
|
156
|
+
try {
|
|
157
|
+
const opts = Intl.DateTimeFormat().resolvedOptions();
|
|
158
|
+
const lang = opts.locale || "en";
|
|
159
|
+
return { language: lang, languages: [lang] };
|
|
160
|
+
} catch {
|
|
161
|
+
return { language: "en", languages: ["en"] };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function collectDeviceSignals() {
|
|
165
|
+
const rn = getReactNative();
|
|
166
|
+
const Platform = rn.Platform;
|
|
167
|
+
const Dimensions = rn.Dimensions;
|
|
168
|
+
const PixelRatio = rn.PixelRatio;
|
|
169
|
+
const osRaw = (Platform?.OS || "unknown").toLowerCase();
|
|
170
|
+
const platform = osRaw === "ios" ? "ios" : "android";
|
|
171
|
+
const osVersion = Platform?.Version !== void 0 && Platform?.Version !== null ? String(Platform.Version) : null;
|
|
172
|
+
const constants = Platform?.constants || {};
|
|
173
|
+
const model = constants.Model || constants.uiMode || constants.Brand ? String(constants.Model || constants.uiMode || "") : null;
|
|
174
|
+
const brand = constants.Brand ? String(constants.Brand) : null;
|
|
175
|
+
const isEmulator = typeof constants.isTesting === "boolean" ? constants.isTesting : null;
|
|
176
|
+
let width = 0;
|
|
177
|
+
let height = 0;
|
|
178
|
+
try {
|
|
179
|
+
const screen = Dimensions?.get("screen") || Dimensions?.get("window");
|
|
180
|
+
if (screen) {
|
|
181
|
+
width = Math.round(screen.width);
|
|
182
|
+
height = Math.round(screen.height);
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
let pixelRatio = 1;
|
|
187
|
+
let fontScale = 1;
|
|
188
|
+
try {
|
|
189
|
+
pixelRatio = PixelRatio?.get() ?? 1;
|
|
190
|
+
fontScale = PixelRatio?.getFontScale() ?? 1;
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
const timezone = safeTimezone();
|
|
194
|
+
const timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
195
|
+
const { language, languages } = safeLocale();
|
|
196
|
+
const screenResolution = `${width}x${height}`;
|
|
197
|
+
const deviceSignature = createFingerprintHash({
|
|
198
|
+
os: platform,
|
|
199
|
+
os_version: osVersion,
|
|
200
|
+
model,
|
|
201
|
+
brand
|
|
202
|
+
});
|
|
203
|
+
const screenSignature = createFingerprintHash({
|
|
204
|
+
resolution: screenResolution,
|
|
205
|
+
pixel_ratio: pixelRatio,
|
|
206
|
+
font_scale: fontScale
|
|
207
|
+
});
|
|
208
|
+
const localeSignature = createFingerprintHash({
|
|
209
|
+
timezone,
|
|
210
|
+
timezone_offset: timezoneOffset,
|
|
211
|
+
language
|
|
212
|
+
});
|
|
213
|
+
const fingerprint = createFingerprintHash({
|
|
214
|
+
device: deviceSignature,
|
|
215
|
+
screen: screenSignature,
|
|
216
|
+
locale: localeSignature
|
|
217
|
+
});
|
|
218
|
+
const { id: deviceId, createdAt } = await getDeviceId();
|
|
219
|
+
return {
|
|
220
|
+
fingerprint,
|
|
221
|
+
fingerprint_components: {
|
|
222
|
+
device_signature: deviceSignature,
|
|
223
|
+
screen_signature: screenSignature,
|
|
224
|
+
locale_signature: localeSignature
|
|
225
|
+
},
|
|
226
|
+
device_id: deviceId,
|
|
227
|
+
device_id_created_at: createdAt,
|
|
228
|
+
platform,
|
|
229
|
+
os: platform,
|
|
230
|
+
os_version: osVersion,
|
|
231
|
+
model,
|
|
232
|
+
brand,
|
|
233
|
+
screen_resolution: screenResolution,
|
|
234
|
+
device_pixel_ratio: pixelRatio,
|
|
235
|
+
font_scale: fontScale,
|
|
236
|
+
timezone,
|
|
237
|
+
timezone_offset: timezoneOffset,
|
|
238
|
+
language,
|
|
239
|
+
languages,
|
|
240
|
+
is_emulator: isEmulator
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/biometrics/touch.ts
|
|
245
|
+
function now() {
|
|
246
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
247
|
+
}
|
|
248
|
+
function mean(values) {
|
|
249
|
+
if (values.length === 0) return 0;
|
|
250
|
+
return values.reduce((s, v) => s + v, 0) / values.length;
|
|
251
|
+
}
|
|
252
|
+
function stdDev(values) {
|
|
253
|
+
if (values.length < 2) return 0;
|
|
254
|
+
const m = mean(values);
|
|
255
|
+
return Math.sqrt(mean(values.map((v) => (v - m) ** 2)));
|
|
256
|
+
}
|
|
257
|
+
var TouchTracker = class {
|
|
258
|
+
constructor() {
|
|
259
|
+
this.samples = [];
|
|
260
|
+
this.touchDurations = [];
|
|
261
|
+
this.tapTimestamps = [];
|
|
262
|
+
this.activeStart = null;
|
|
263
|
+
this.multiTouchCount = 0;
|
|
264
|
+
this.startTime = null;
|
|
265
|
+
this.tracking = false;
|
|
266
|
+
}
|
|
267
|
+
start() {
|
|
268
|
+
this.tracking = true;
|
|
269
|
+
this.reset();
|
|
270
|
+
this.tracking = true;
|
|
271
|
+
}
|
|
272
|
+
stop() {
|
|
273
|
+
this.tracking = false;
|
|
274
|
+
}
|
|
275
|
+
reset() {
|
|
276
|
+
this.samples = [];
|
|
277
|
+
this.touchDurations = [];
|
|
278
|
+
this.tapTimestamps = [];
|
|
279
|
+
this.activeStart = null;
|
|
280
|
+
this.multiTouchCount = 0;
|
|
281
|
+
this.startTime = null;
|
|
282
|
+
}
|
|
283
|
+
extract(e) {
|
|
284
|
+
const ne = e?.nativeEvent || {};
|
|
285
|
+
const x = ne.locationX ?? ne.pageX ?? 0;
|
|
286
|
+
const y = ne.locationY ?? ne.pageY ?? 0;
|
|
287
|
+
const ts = ne.timestamp ?? now();
|
|
288
|
+
return { x, y, timestamp: ts, force: ne.force ?? 0 };
|
|
289
|
+
}
|
|
290
|
+
/** Call on touch/gesture start (e.g. onResponderGrant / onTouchStart). */
|
|
291
|
+
handleTouchStart(e) {
|
|
292
|
+
if (!this.tracking) return;
|
|
293
|
+
const touches = e?.nativeEvent?.touches;
|
|
294
|
+
if (Array.isArray(touches) && touches.length > 1) {
|
|
295
|
+
this.multiTouchCount++;
|
|
296
|
+
}
|
|
297
|
+
const sample = this.extract(e);
|
|
298
|
+
if (this.startTime === null) this.startTime = sample.timestamp;
|
|
299
|
+
this.activeStart = sample.timestamp;
|
|
300
|
+
this.samples.push(sample);
|
|
301
|
+
}
|
|
302
|
+
/** Call on touch/gesture move (e.g. onResponderMove / onTouchMove). */
|
|
303
|
+
handleTouchMove(e) {
|
|
304
|
+
if (!this.tracking) return;
|
|
305
|
+
this.samples.push(this.extract(e));
|
|
306
|
+
if (this.samples.length > 1e3) {
|
|
307
|
+
this.samples = this.samples.slice(-500);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Call on touch/gesture end (e.g. onResponderRelease / onTouchEnd). */
|
|
311
|
+
handleTouchEnd(e) {
|
|
312
|
+
if (!this.tracking) return;
|
|
313
|
+
const sample = this.extract(e);
|
|
314
|
+
this.samples.push(sample);
|
|
315
|
+
if (this.activeStart !== null) {
|
|
316
|
+
this.touchDurations.push(sample.timestamp - this.activeStart);
|
|
317
|
+
this.activeStart = null;
|
|
318
|
+
}
|
|
319
|
+
this.tapTimestamps.push(sample.timestamp);
|
|
320
|
+
}
|
|
321
|
+
/** Handlers ready to spread onto a View's responder/touch props. */
|
|
322
|
+
getEventHandlers() {
|
|
323
|
+
return {
|
|
324
|
+
onTouchStart: this.handleTouchStart.bind(this),
|
|
325
|
+
onTouchMove: this.handleTouchMove.bind(this),
|
|
326
|
+
onTouchEnd: this.handleTouchEnd.bind(this)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
getMetrics() {
|
|
330
|
+
const pressures = this.samples.map((s) => s.force ?? 0).filter((f) => f > 0);
|
|
331
|
+
const velocities = [];
|
|
332
|
+
for (let i = 1; i < this.samples.length; i++) {
|
|
333
|
+
const prev = this.samples[i - 1];
|
|
334
|
+
const curr = this.samples[i];
|
|
335
|
+
const dt = (curr.timestamp - prev.timestamp) / 1e3;
|
|
336
|
+
if (dt > 0 && dt < 1) {
|
|
337
|
+
const dist = Math.sqrt((curr.x - prev.x) ** 2 + (curr.y - prev.y) ** 2);
|
|
338
|
+
velocities.push(dist / dt);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const tapIntervals = [];
|
|
342
|
+
for (let i = 1; i < this.tapTimestamps.length; i++) {
|
|
343
|
+
tapIntervals.push(this.tapTimestamps[i] - this.tapTimestamps[i - 1]);
|
|
344
|
+
}
|
|
345
|
+
const totalDuration = this.samples.length > 1 ? this.samples[this.samples.length - 1].timestamp - this.samples[0].timestamp : 0;
|
|
346
|
+
return {
|
|
347
|
+
avgPressure: mean(pressures),
|
|
348
|
+
pressureStdDev: stdDev(pressures),
|
|
349
|
+
avgSwipeVelocity: mean(velocities),
|
|
350
|
+
touchCount: this.samples.length,
|
|
351
|
+
tapCount: this.tapTimestamps.length,
|
|
352
|
+
avgTapInterval: mean(tapIntervals),
|
|
353
|
+
tapIntervalStdDev: stdDev(tapIntervals),
|
|
354
|
+
avgTouchDuration: mean(this.touchDurations),
|
|
355
|
+
multiTouchCount: this.multiTouchCount,
|
|
356
|
+
totalDuration
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
getRawData() {
|
|
360
|
+
return [...this.samples];
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
function createTouchTracker() {
|
|
364
|
+
return new TouchTracker();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/profiles.ts
|
|
368
|
+
var DEFAULT_BASE_URL = "https://api.simplr.sh";
|
|
369
|
+
var SimplrProfiles = class {
|
|
370
|
+
constructor(config) {
|
|
371
|
+
this.apiKey = "";
|
|
372
|
+
this.baseUrl = DEFAULT_BASE_URL;
|
|
373
|
+
this.timeoutMs = 15e3;
|
|
374
|
+
this.configured = false;
|
|
375
|
+
if (config) this.configure(config);
|
|
376
|
+
}
|
|
377
|
+
configure(config) {
|
|
378
|
+
this.apiKey = config.apiKey;
|
|
379
|
+
this.baseUrl = config.baseUrl ? normalizeBaseUrl(config.baseUrl) : DEFAULT_BASE_URL;
|
|
380
|
+
if (config.timeoutMs !== void 0) this.timeoutMs = config.timeoutMs;
|
|
381
|
+
this.fetchImpl = config.fetchImpl;
|
|
382
|
+
this.configured = true;
|
|
383
|
+
return this;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Set the device-signal collector so identify()/submitOrder() can auto-attach
|
|
387
|
+
* a fingerprint. The main client wires this to its own collector.
|
|
388
|
+
*/
|
|
389
|
+
setDeviceSignalCollector(fn) {
|
|
390
|
+
this.collectDeviceSignalsFn = fn;
|
|
391
|
+
}
|
|
392
|
+
requireConfigured() {
|
|
393
|
+
if (!this.configured || !this.apiKey) {
|
|
394
|
+
throw new Error("SimplrProfiles is not configured. Call configure({ apiKey }) first.");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
get httpConfig() {
|
|
398
|
+
return {
|
|
399
|
+
authHeaders: { "X-API-Key": this.apiKey },
|
|
400
|
+
baseUrl: this.baseUrl,
|
|
401
|
+
timeoutMs: this.timeoutMs,
|
|
402
|
+
fetchImpl: this.fetchImpl
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** Create or update an anonymous profile and link the current device. */
|
|
406
|
+
async identify(externalId, options) {
|
|
407
|
+
this.requireConfigured();
|
|
408
|
+
const { profileType, deviceSignals, ...rest } = options ?? {};
|
|
409
|
+
let signals = deviceSignals;
|
|
410
|
+
if (!signals && this.collectDeviceSignalsFn) {
|
|
411
|
+
try {
|
|
412
|
+
signals = await this.collectDeviceSignalsFn();
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const body = {
|
|
417
|
+
external_id: externalId,
|
|
418
|
+
profile_type: profileType || "customer",
|
|
419
|
+
...rest
|
|
420
|
+
};
|
|
421
|
+
if (signals) body.fingerprint_hash = signals.fingerprint;
|
|
422
|
+
return apiRequest(this.httpConfig, "POST", "/v1/profiles", body);
|
|
423
|
+
}
|
|
424
|
+
/** Submit an order for real-time fraud scoring; auto-attaches a fingerprint. */
|
|
425
|
+
async submitOrder(order) {
|
|
426
|
+
this.requireConfigured();
|
|
427
|
+
let body = order;
|
|
428
|
+
if (!order.fingerprint_hash && this.collectDeviceSignalsFn) {
|
|
429
|
+
try {
|
|
430
|
+
const signals = await this.collectDeviceSignalsFn();
|
|
431
|
+
body = { ...order, fingerprint_hash: signals.fingerprint };
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return apiRequest(this.httpConfig, "POST", "/v1/orders", body);
|
|
436
|
+
}
|
|
437
|
+
/** Get the risk profile for a user. */
|
|
438
|
+
async getProfileRisk(externalId) {
|
|
439
|
+
this.requireConfigured();
|
|
440
|
+
return apiRequest(
|
|
441
|
+
this.httpConfig,
|
|
442
|
+
"GET",
|
|
443
|
+
`/v1/profiles/${encodeURIComponent(externalId)}`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
/** Report a profile as fraud or legitimate. */
|
|
447
|
+
async reportOutcome(externalId, outcome) {
|
|
448
|
+
this.requireConfigured();
|
|
449
|
+
await apiRequest(
|
|
450
|
+
this.httpConfig,
|
|
451
|
+
"POST",
|
|
452
|
+
`/v1/profiles/${encodeURIComponent(externalId)}/outcome`,
|
|
453
|
+
{ outcome }
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
new SimplrProfiles();
|
|
458
|
+
|
|
459
|
+
// src/rum.ts
|
|
460
|
+
var DEFAULT_BASE_URL2 = "https://api.simplr.sh";
|
|
461
|
+
var DEFAULT_BATCH_SIZE = 30;
|
|
462
|
+
var DEFAULT_FLUSH_INTERVAL = 1e4;
|
|
463
|
+
function genId() {
|
|
464
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
465
|
+
}
|
|
466
|
+
var SimplrRUM = class {
|
|
467
|
+
constructor() {
|
|
468
|
+
this.config = null;
|
|
469
|
+
this.initialized = false;
|
|
470
|
+
this.baseUrl = DEFAULT_BASE_URL2;
|
|
471
|
+
this.timeoutMs = 15e3;
|
|
472
|
+
this.batchSize = DEFAULT_BATCH_SIZE;
|
|
473
|
+
this.queue = [];
|
|
474
|
+
this.timer = null;
|
|
475
|
+
this.flushing = false;
|
|
476
|
+
this.sessionId = null;
|
|
477
|
+
this.currentViewId = null;
|
|
478
|
+
this.globalAttributes = {};
|
|
479
|
+
}
|
|
480
|
+
/** Initialize the SDK, start a session, and begin the flush timer. */
|
|
481
|
+
initialize(config) {
|
|
482
|
+
if (this.initialized) return;
|
|
483
|
+
this.config = config;
|
|
484
|
+
this.baseUrl = config.baseUrl ? normalizeBaseUrl(config.baseUrl) : DEFAULT_BASE_URL2;
|
|
485
|
+
if (config.timeoutMs !== void 0) this.timeoutMs = config.timeoutMs;
|
|
486
|
+
this.fetchImpl = config.fetchImpl;
|
|
487
|
+
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
488
|
+
this.sessionId = genId();
|
|
489
|
+
this.initialized = true;
|
|
490
|
+
this.trackEvent("session_start");
|
|
491
|
+
const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
492
|
+
if (interval > 0 && typeof setInterval !== "undefined") {
|
|
493
|
+
this.timer = setInterval(() => {
|
|
494
|
+
void this.flush();
|
|
495
|
+
}, interval);
|
|
496
|
+
this.timer?.unref?.();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
isInitialized() {
|
|
500
|
+
return this.initialized;
|
|
501
|
+
}
|
|
502
|
+
get httpConfig() {
|
|
503
|
+
return {
|
|
504
|
+
authHeaders: { "X-API-Key": this.config.apiKey },
|
|
505
|
+
baseUrl: this.baseUrl,
|
|
506
|
+
timeoutMs: this.timeoutMs,
|
|
507
|
+
fetchImpl: this.fetchImpl
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/** Associate subsequent events with a user. */
|
|
511
|
+
setUser(userId, attributes) {
|
|
512
|
+
this.userId = userId;
|
|
513
|
+
this.userAttributes = attributes;
|
|
514
|
+
}
|
|
515
|
+
clearUser() {
|
|
516
|
+
this.userId = void 0;
|
|
517
|
+
this.userAttributes = void 0;
|
|
518
|
+
}
|
|
519
|
+
addAttribute(key, value) {
|
|
520
|
+
this.globalAttributes[key] = value;
|
|
521
|
+
}
|
|
522
|
+
removeAttribute(key) {
|
|
523
|
+
delete this.globalAttributes[key];
|
|
524
|
+
}
|
|
525
|
+
/** Track a screen view (call from your navigation listener or useTrackView). */
|
|
526
|
+
trackView(name, attributes) {
|
|
527
|
+
if (!this.initialized) return;
|
|
528
|
+
this.currentViewId = genId();
|
|
529
|
+
this.trackEvent("view", { view: { id: this.currentViewId, name }, attributes });
|
|
530
|
+
}
|
|
531
|
+
/** Track a user action (tap, swipe, submit, …). */
|
|
532
|
+
trackAction(name, attributes) {
|
|
533
|
+
if (!this.initialized) return;
|
|
534
|
+
this.trackEvent("action", { action: { name, type: "custom" }, attributes });
|
|
535
|
+
}
|
|
536
|
+
/** Track an error. */
|
|
537
|
+
trackError(error, attributes) {
|
|
538
|
+
if (!this.initialized) return;
|
|
539
|
+
const data = error instanceof Error ? { message: error.message, stack: error.stack, type: error.constructor.name } : error;
|
|
540
|
+
this.trackEvent("error", { error: data, attributes });
|
|
541
|
+
}
|
|
542
|
+
/** Emit a log line. */
|
|
543
|
+
log(level, message, attributes) {
|
|
544
|
+
if (!this.initialized) return;
|
|
545
|
+
this.trackEvent("log", { log: { level, message }, attributes });
|
|
546
|
+
}
|
|
547
|
+
trackEvent(type, data) {
|
|
548
|
+
if (!this.initialized || !this.sessionId) return;
|
|
549
|
+
const event = {
|
|
550
|
+
type,
|
|
551
|
+
timestamp: Date.now(),
|
|
552
|
+
sessionId: this.sessionId,
|
|
553
|
+
viewId: this.currentViewId || void 0,
|
|
554
|
+
userId: this.userId,
|
|
555
|
+
applicationId: this.config.applicationId,
|
|
556
|
+
applicationVersion: this.config?.applicationVersion,
|
|
557
|
+
environment: this.config?.environment,
|
|
558
|
+
platform: "react-native",
|
|
559
|
+
userAttributes: this.userAttributes,
|
|
560
|
+
globalAttributes: Object.keys(this.globalAttributes).length > 0 ? this.globalAttributes : void 0,
|
|
561
|
+
...data
|
|
562
|
+
};
|
|
563
|
+
this.queue.push(event);
|
|
564
|
+
if (this.queue.length >= this.batchSize) void this.flush();
|
|
565
|
+
}
|
|
566
|
+
/** Flush queued events to POST /v1/rum/events. */
|
|
567
|
+
async flush() {
|
|
568
|
+
if (this.flushing || this.queue.length === 0 || !this.config) return;
|
|
569
|
+
this.flushing = true;
|
|
570
|
+
const events = this.queue;
|
|
571
|
+
this.queue = [];
|
|
572
|
+
try {
|
|
573
|
+
await apiRequest(this.httpConfig, "POST", "/v1/rum/events", {
|
|
574
|
+
events,
|
|
575
|
+
sentAt: Date.now()
|
|
576
|
+
});
|
|
577
|
+
} catch {
|
|
578
|
+
this.queue = [...events, ...this.queue];
|
|
579
|
+
} finally {
|
|
580
|
+
this.flushing = false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/** End the session, flush remaining events, and stop the timer. */
|
|
584
|
+
async stopSession() {
|
|
585
|
+
if (!this.initialized) return;
|
|
586
|
+
this.trackEvent("session_end");
|
|
587
|
+
await this.flush();
|
|
588
|
+
if (this.timer) {
|
|
589
|
+
clearInterval(this.timer);
|
|
590
|
+
this.timer = null;
|
|
591
|
+
}
|
|
592
|
+
this.initialized = false;
|
|
593
|
+
}
|
|
594
|
+
getSessionId() {
|
|
595
|
+
return this.sessionId;
|
|
596
|
+
}
|
|
597
|
+
getViewId() {
|
|
598
|
+
return this.currentViewId;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
var simplrRUM = new SimplrRUM();
|
|
602
|
+
|
|
603
|
+
// src/ai.ts
|
|
604
|
+
var DEFAULT_BASE_URL3 = "https://api.simplr.sh";
|
|
605
|
+
function mapDelegation(d) {
|
|
606
|
+
return {
|
|
607
|
+
delegationId: d.delegation_id,
|
|
608
|
+
endUserId: d.end_user_id,
|
|
609
|
+
bindingMode: d.binding_mode,
|
|
610
|
+
status: d.status,
|
|
611
|
+
expiresAt: d.expires_at,
|
|
612
|
+
useCount: d.use_count,
|
|
613
|
+
lastUsedAt: d.last_used_at,
|
|
614
|
+
createdAt: d.created_at
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
var SimplrAI = class {
|
|
618
|
+
constructor(config) {
|
|
619
|
+
this.apiKey = "";
|
|
620
|
+
this.baseUrl = DEFAULT_BASE_URL3;
|
|
621
|
+
this.timeoutMs = 15e3;
|
|
622
|
+
this.configured = false;
|
|
623
|
+
if (config) this.configure(config);
|
|
624
|
+
}
|
|
625
|
+
configure(config) {
|
|
626
|
+
this.apiKey = config.apiKey;
|
|
627
|
+
this.baseUrl = config.baseUrl ? normalizeBaseUrl(config.baseUrl) : DEFAULT_BASE_URL3;
|
|
628
|
+
if (config.timeoutMs !== void 0) this.timeoutMs = config.timeoutMs;
|
|
629
|
+
this.fetchImpl = config.fetchImpl;
|
|
630
|
+
this.configured = true;
|
|
631
|
+
return this;
|
|
632
|
+
}
|
|
633
|
+
requireConfigured() {
|
|
634
|
+
if (!this.configured || !this.apiKey) {
|
|
635
|
+
throw new Error("SimplrAI is not configured. Call configure({ apiKey }) first.");
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
get httpConfig() {
|
|
639
|
+
return {
|
|
640
|
+
authHeaders: { "X-API-Key": this.apiKey },
|
|
641
|
+
baseUrl: this.baseUrl,
|
|
642
|
+
timeoutMs: this.timeoutMs,
|
|
643
|
+
fetchImpl: this.fetchImpl
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/** Create a new AI delegation token for a user. POST /v1/ai/delegations. */
|
|
647
|
+
async createDelegation(options) {
|
|
648
|
+
this.requireConfigured();
|
|
649
|
+
const content = await apiRequest(this.httpConfig, "POST", "/v1/ai/delegations", {
|
|
650
|
+
end_user_id: options.userId,
|
|
651
|
+
end_user_email: options.email,
|
|
652
|
+
binding: options.binding || "any_location",
|
|
653
|
+
expires_in_days: options.expiresInDays || 7,
|
|
654
|
+
session_id: options.sessionId,
|
|
655
|
+
fingerprint_hash: options.fingerprintHash
|
|
656
|
+
});
|
|
657
|
+
const d = content.delegation;
|
|
658
|
+
return {
|
|
659
|
+
token: d.token,
|
|
660
|
+
delegationId: d.delegation_id,
|
|
661
|
+
expiresAt: d.expires_at,
|
|
662
|
+
bindingMode: d.binding_mode
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/** Validate (introspect) an AI delegation token. POST /v1/ai/validate. */
|
|
666
|
+
async validate(token, options) {
|
|
667
|
+
this.requireConfigured();
|
|
668
|
+
try {
|
|
669
|
+
const content = await apiRequest(this.httpConfig, "POST", "/v1/ai/validate", {
|
|
670
|
+
token,
|
|
671
|
+
fingerprint_hash: options?.fingerprintHash,
|
|
672
|
+
ai_provider: options?.aiProvider,
|
|
673
|
+
action: options?.action
|
|
674
|
+
});
|
|
675
|
+
return {
|
|
676
|
+
valid: true,
|
|
677
|
+
sessionType: content.session_type,
|
|
678
|
+
endUserId: content.end_user_id,
|
|
679
|
+
delegation: content.delegation ? {
|
|
680
|
+
delegationId: content.delegation.delegation_id,
|
|
681
|
+
bindingMode: content.delegation.binding_mode,
|
|
682
|
+
expiresAt: content.delegation.expires_at,
|
|
683
|
+
useCount: content.delegation.use_count
|
|
684
|
+
} : void 0
|
|
685
|
+
};
|
|
686
|
+
} catch (err) {
|
|
687
|
+
return { valid: false, error: err instanceof Error ? err.message : "Validation failed" };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/** Revoke a delegation. POST /v1/ai/delegations/{id}/revoke. */
|
|
691
|
+
async revoke(delegationId, reason) {
|
|
692
|
+
this.requireConfigured();
|
|
693
|
+
await apiRequest(
|
|
694
|
+
this.httpConfig,
|
|
695
|
+
"POST",
|
|
696
|
+
`/v1/ai/delegations/${encodeURIComponent(delegationId)}/revoke`,
|
|
697
|
+
{ reason }
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
/** List delegations, optionally filtered by user. GET /v1/ai/delegations. */
|
|
701
|
+
async list(userId) {
|
|
702
|
+
this.requireConfigured();
|
|
703
|
+
const path = userId ? `/v1/ai/delegations?end_user_id=${encodeURIComponent(userId)}` : "/v1/ai/delegations";
|
|
704
|
+
const content = await apiRequest(this.httpConfig, "GET", path);
|
|
705
|
+
return (content.delegations || []).map(mapDelegation);
|
|
706
|
+
}
|
|
707
|
+
/** Get a single delegation. GET /v1/ai/delegations/{id}. */
|
|
708
|
+
async get(delegationId) {
|
|
709
|
+
this.requireConfigured();
|
|
710
|
+
const content = await apiRequest(
|
|
711
|
+
this.httpConfig,
|
|
712
|
+
"GET",
|
|
713
|
+
`/v1/ai/delegations/${encodeURIComponent(delegationId)}`
|
|
714
|
+
);
|
|
715
|
+
return mapDelegation(content.delegation);
|
|
716
|
+
}
|
|
717
|
+
/** Get delegation statistics. GET /v1/ai/stats. */
|
|
718
|
+
async stats() {
|
|
719
|
+
this.requireConfigured();
|
|
720
|
+
const content = await apiRequest(this.httpConfig, "GET", "/v1/ai/stats");
|
|
721
|
+
const s = content.stats;
|
|
722
|
+
return {
|
|
723
|
+
totalDelegations: s.total_delegations,
|
|
724
|
+
activeDelegations: s.active_delegations,
|
|
725
|
+
totalUses: s.total_uses,
|
|
726
|
+
delegationsByBinding: {
|
|
727
|
+
verifiedDevice: s.delegations_by_binding.verified_device,
|
|
728
|
+
anyLocation: s.delegations_by_binding.any_location
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/** Revoke all delegations for a user (e.g. on logout). POST /v1/ai/revoke-all. */
|
|
733
|
+
async revokeAllForUser(userId, reason) {
|
|
734
|
+
this.requireConfigured();
|
|
735
|
+
const content = await apiRequest(this.httpConfig, "POST", "/v1/ai/revoke-all", {
|
|
736
|
+
end_user_id: userId,
|
|
737
|
+
reason
|
|
738
|
+
});
|
|
739
|
+
return content.revoked_count;
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
new SimplrAI();
|
|
743
|
+
|
|
744
|
+
// src/client.ts
|
|
745
|
+
var DEFAULT_BASE_URL4 = "https://api.simplr.sh";
|
|
746
|
+
var SimplrFraud = class {
|
|
747
|
+
constructor(config) {
|
|
748
|
+
this.apiKey = "";
|
|
749
|
+
this.baseUrl = DEFAULT_BASE_URL4;
|
|
750
|
+
this.timeoutMs = 15e3;
|
|
751
|
+
this.formStartTime = null;
|
|
752
|
+
this.configured = false;
|
|
753
|
+
this.touchTracker = createTouchTracker();
|
|
754
|
+
this.profiles = new SimplrProfiles();
|
|
755
|
+
this.rum = new SimplrRUM();
|
|
756
|
+
this.ai = new SimplrAI();
|
|
757
|
+
this.profiles.setDeviceSignalCollector(() => this.getDeviceSignals());
|
|
758
|
+
if (config) this.configure(config);
|
|
759
|
+
}
|
|
760
|
+
/** Configure (or re-configure) the client. */
|
|
761
|
+
configure(config) {
|
|
762
|
+
this.apiKey = config.apiKey;
|
|
763
|
+
this.baseUrl = config.baseUrl ? normalizeBaseUrl(config.baseUrl) : DEFAULT_BASE_URL4;
|
|
764
|
+
if (config.timeoutMs !== void 0) this.timeoutMs = config.timeoutMs;
|
|
765
|
+
this.fetchImpl = config.fetchImpl;
|
|
766
|
+
this.configured = true;
|
|
767
|
+
const sub = {
|
|
768
|
+
apiKey: this.apiKey,
|
|
769
|
+
baseUrl: this.baseUrl,
|
|
770
|
+
timeoutMs: this.timeoutMs,
|
|
771
|
+
fetchImpl: this.fetchImpl
|
|
772
|
+
};
|
|
773
|
+
this.profiles.configure(sub);
|
|
774
|
+
this.ai.configure(sub);
|
|
775
|
+
if (config.autoStart ?? true) {
|
|
776
|
+
this.startTracking();
|
|
777
|
+
}
|
|
778
|
+
return this;
|
|
779
|
+
}
|
|
780
|
+
requireConfigured() {
|
|
781
|
+
if (!this.configured || !this.apiKey) {
|
|
782
|
+
throw new Error("SimplrFraud is not configured. Call configure({ apiKey }) first.");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
get httpConfig() {
|
|
786
|
+
return {
|
|
787
|
+
authHeaders: { "X-API-Key": this.apiKey },
|
|
788
|
+
baseUrl: this.baseUrl,
|
|
789
|
+
timeoutMs: this.timeoutMs,
|
|
790
|
+
fetchImpl: this.fetchImpl
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
// --- Biometrics ---------------------------------------------------------
|
|
794
|
+
/** Start collecting touch biometrics and reset the form timer. */
|
|
795
|
+
startTracking() {
|
|
796
|
+
this.touchTracker.start();
|
|
797
|
+
this.formStartTime = nowMs();
|
|
798
|
+
}
|
|
799
|
+
stopTracking() {
|
|
800
|
+
this.touchTracker.stop();
|
|
801
|
+
}
|
|
802
|
+
reset() {
|
|
803
|
+
this.touchTracker.reset();
|
|
804
|
+
this.formStartTime = nowMs();
|
|
805
|
+
}
|
|
806
|
+
/** The shared touch tracker; attach its handlers to your Views. */
|
|
807
|
+
getTouchTracker() {
|
|
808
|
+
return this.touchTracker;
|
|
809
|
+
}
|
|
810
|
+
collectBehaviorSignals() {
|
|
811
|
+
const signals = {
|
|
812
|
+
touch: this.touchTracker.getMetrics()
|
|
813
|
+
};
|
|
814
|
+
if (this.formStartTime !== null) {
|
|
815
|
+
signals.form_fill_time = nowMs() - this.formStartTime;
|
|
816
|
+
}
|
|
817
|
+
return signals;
|
|
818
|
+
}
|
|
819
|
+
// --- Device signals -----------------------------------------------------
|
|
820
|
+
/** Collect stable device signals from React Native APIs. */
|
|
821
|
+
async getDeviceSignals() {
|
|
822
|
+
return collectDeviceSignals();
|
|
823
|
+
}
|
|
824
|
+
/** Collect device + behavior signals together. */
|
|
825
|
+
async collect() {
|
|
826
|
+
const device = await this.getDeviceSignals();
|
|
827
|
+
return {
|
|
828
|
+
device,
|
|
829
|
+
behavior: this.collectBehaviorSignals(),
|
|
830
|
+
collected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
// --- API calls ----------------------------------------------------------
|
|
834
|
+
/**
|
|
835
|
+
* Run a fraud check. Device + behavior signals are auto-collected and merged
|
|
836
|
+
* into the request unless already present on `input`.
|
|
837
|
+
*/
|
|
838
|
+
async check(input = {}) {
|
|
839
|
+
this.requireConfigured();
|
|
840
|
+
const collected = await this.collect();
|
|
841
|
+
const body = {
|
|
842
|
+
...input,
|
|
843
|
+
device: input.device ?? collected.device,
|
|
844
|
+
behavior: input.behavior ?? collected.behavior
|
|
845
|
+
};
|
|
846
|
+
return apiRequest(this.httpConfig, "POST", "/v1/check", body);
|
|
847
|
+
}
|
|
848
|
+
/** Submit an order for scoring (POST /v1/orders). */
|
|
849
|
+
async submitOrder(order) {
|
|
850
|
+
this.requireConfigured();
|
|
851
|
+
let body = order;
|
|
852
|
+
if (!order.fingerprint_hash) {
|
|
853
|
+
try {
|
|
854
|
+
const device = await this.getDeviceSignals();
|
|
855
|
+
body = { ...order, fingerprint_hash: device.fingerprint };
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return apiRequest(this.httpConfig, "POST", "/v1/orders", body);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Identify/associate the current device with an external profile id.
|
|
863
|
+
* Sends a profile check via POST /v1/check with the device signals attached.
|
|
864
|
+
*/
|
|
865
|
+
async identify(externalId, metadata) {
|
|
866
|
+
this.requireConfigured();
|
|
867
|
+
const device = await this.getDeviceSignals();
|
|
868
|
+
return apiRequest(this.httpConfig, "POST", "/v1/check", {
|
|
869
|
+
event_type: "identify",
|
|
870
|
+
event_id: externalId,
|
|
871
|
+
device,
|
|
872
|
+
metadata
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
function nowMs() {
|
|
877
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
878
|
+
}
|
|
879
|
+
var simplr = new SimplrFraud();
|
|
880
|
+
|
|
881
|
+
// src/flags.ts
|
|
882
|
+
function matchRule(rule, attributes) {
|
|
883
|
+
const actual = attributes[rule.attribute];
|
|
884
|
+
switch (rule.op) {
|
|
885
|
+
case "eq":
|
|
886
|
+
return String(actual) === rule.value;
|
|
887
|
+
case "neq":
|
|
888
|
+
return String(actual) !== rule.value;
|
|
889
|
+
case "contains":
|
|
890
|
+
return String(actual ?? "").includes(rule.value);
|
|
891
|
+
default:
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
var SimplrFlags = class {
|
|
896
|
+
constructor() {
|
|
897
|
+
this.apiKey = "";
|
|
898
|
+
this.baseUrl = "https://api.simplr.sh";
|
|
899
|
+
this.environment = "test";
|
|
900
|
+
this.refreshIntervalMs = 6e4;
|
|
901
|
+
this.flags = {};
|
|
902
|
+
this.timer = null;
|
|
903
|
+
this.ready = false;
|
|
904
|
+
}
|
|
905
|
+
async initialize(config) {
|
|
906
|
+
this.apiKey = config.apiKey;
|
|
907
|
+
if (config.baseUrl) this.baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
908
|
+
if (config.environment) this.environment = config.environment;
|
|
909
|
+
if (config.refreshIntervalMs !== void 0) this.refreshIntervalMs = config.refreshIntervalMs;
|
|
910
|
+
if (config.defaultUserId) this.defaultUserId = config.defaultUserId;
|
|
911
|
+
this.fetchImpl = config.fetchImpl;
|
|
912
|
+
await this.refresh();
|
|
913
|
+
this.ready = true;
|
|
914
|
+
if (this.refreshIntervalMs > 0 && typeof setInterval !== "undefined") {
|
|
915
|
+
this.timer = setInterval(() => this.refresh(), this.refreshIntervalMs);
|
|
916
|
+
this.timer?.unref?.();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
setUser(userId) {
|
|
920
|
+
this.userId = userId;
|
|
921
|
+
}
|
|
922
|
+
/** Sets the fallback identity (typically the persisted device_id). */
|
|
923
|
+
setDefaultUser(userId) {
|
|
924
|
+
this.defaultUserId = userId;
|
|
925
|
+
}
|
|
926
|
+
async refresh() {
|
|
927
|
+
const fetchImpl = this.fetchImpl ?? globalThis.fetch;
|
|
928
|
+
if (typeof fetchImpl !== "function") return;
|
|
929
|
+
try {
|
|
930
|
+
const res = await fetchImpl(
|
|
931
|
+
`${this.baseUrl}/v1/flags?environment=${this.environment}`,
|
|
932
|
+
{ headers: { "X-API-Key": this.apiKey } }
|
|
933
|
+
);
|
|
934
|
+
if (!res.ok) return;
|
|
935
|
+
const json = await res.json();
|
|
936
|
+
const list = json?.content?.flags || json?.flags || [];
|
|
937
|
+
const map = {};
|
|
938
|
+
for (const f of list) map[f.key] = f;
|
|
939
|
+
this.flags = map;
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
isEnabled(key, ctx) {
|
|
944
|
+
const f = this.flags[key];
|
|
945
|
+
if (!f || !f.enabled) return false;
|
|
946
|
+
const uid = ctx?.userId || this.userId || this.defaultUserId || "anonymous";
|
|
947
|
+
if (f.target_user_ids && f.target_user_ids.includes(uid)) return true;
|
|
948
|
+
if (ctx?.attributes && f.rules?.length && f.rules.some((r) => matchRule(r, ctx.attributes)))
|
|
949
|
+
return true;
|
|
950
|
+
if (f.rollout_percentage >= 100) return true;
|
|
951
|
+
if (f.rollout_percentage <= 0) return false;
|
|
952
|
+
const bucket = murmurHash3(`${key}:${uid}`) % 100;
|
|
953
|
+
return bucket < f.rollout_percentage;
|
|
954
|
+
}
|
|
955
|
+
getAll() {
|
|
956
|
+
return { ...this.flags };
|
|
957
|
+
}
|
|
958
|
+
/** Replace the in-memory flag set directly (useful for testing/SSR). */
|
|
959
|
+
setFlags(list) {
|
|
960
|
+
const map = {};
|
|
961
|
+
for (const f of list) map[f.key] = f;
|
|
962
|
+
this.flags = map;
|
|
963
|
+
this.ready = true;
|
|
964
|
+
}
|
|
965
|
+
isReady() {
|
|
966
|
+
return this.ready;
|
|
967
|
+
}
|
|
968
|
+
dispose() {
|
|
969
|
+
if (this.timer) clearInterval(this.timer);
|
|
970
|
+
this.timer = null;
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
var simplrFlags = new SimplrFlags();
|
|
974
|
+
|
|
975
|
+
// src/hooks/index.ts
|
|
976
|
+
function useSimplr(config) {
|
|
977
|
+
const client = react.useMemo(() => simplr, []);
|
|
978
|
+
react.useEffect(() => {
|
|
979
|
+
if (config?.apiKey) {
|
|
980
|
+
client.configure(config);
|
|
981
|
+
}
|
|
982
|
+
}, [config?.apiKey, config?.baseUrl]);
|
|
983
|
+
return client;
|
|
984
|
+
}
|
|
985
|
+
function useDeviceSignals(client) {
|
|
986
|
+
const sdk = client ?? simplr;
|
|
987
|
+
const [signals, setSignals] = react.useState(null);
|
|
988
|
+
const [loading, setLoading] = react.useState(true);
|
|
989
|
+
const [error, setError] = react.useState(null);
|
|
990
|
+
const [tick, setTick] = react.useState(0);
|
|
991
|
+
react.useEffect(() => {
|
|
992
|
+
let mounted = true;
|
|
993
|
+
setLoading(true);
|
|
994
|
+
sdk.getDeviceSignals().then((s) => {
|
|
995
|
+
if (mounted) {
|
|
996
|
+
setSignals(s);
|
|
997
|
+
setError(null);
|
|
998
|
+
}
|
|
999
|
+
}).catch((e) => {
|
|
1000
|
+
if (mounted) setError(e instanceof Error ? e : new Error(String(e)));
|
|
1001
|
+
}).finally(() => {
|
|
1002
|
+
if (mounted) setLoading(false);
|
|
1003
|
+
});
|
|
1004
|
+
return () => {
|
|
1005
|
+
mounted = false;
|
|
1006
|
+
};
|
|
1007
|
+
}, [tick]);
|
|
1008
|
+
const refresh = react.useCallback(() => setTick((t) => t + 1), []);
|
|
1009
|
+
return { signals, loading, error, refresh };
|
|
1010
|
+
}
|
|
1011
|
+
function useFeatureFlag(key, ctx, flags = simplrFlags) {
|
|
1012
|
+
const ctxKey = JSON.stringify(ctx ?? null);
|
|
1013
|
+
const [enabled, setEnabled] = react.useState(() => {
|
|
1014
|
+
try {
|
|
1015
|
+
return flags.isEnabled(key, ctx);
|
|
1016
|
+
} catch {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
react.useEffect(() => {
|
|
1021
|
+
let mounted = true;
|
|
1022
|
+
const evaluate = () => {
|
|
1023
|
+
if (!mounted) return;
|
|
1024
|
+
try {
|
|
1025
|
+
setEnabled(flags.isEnabled(key, ctx));
|
|
1026
|
+
} catch {
|
|
1027
|
+
setEnabled(false);
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
evaluate();
|
|
1031
|
+
const t = typeof setTimeout !== "undefined" ? setTimeout(evaluate, 0) : null;
|
|
1032
|
+
return () => {
|
|
1033
|
+
mounted = false;
|
|
1034
|
+
if (t) clearTimeout(t);
|
|
1035
|
+
};
|
|
1036
|
+
}, [key, ctxKey, flags]);
|
|
1037
|
+
return enabled;
|
|
1038
|
+
}
|
|
1039
|
+
function useTouchBiometrics(client) {
|
|
1040
|
+
const trackerRef = react.useRef(null);
|
|
1041
|
+
if (!trackerRef.current) {
|
|
1042
|
+
trackerRef.current = client ? client.getTouchTracker() : new TouchTracker();
|
|
1043
|
+
}
|
|
1044
|
+
const tracker = trackerRef.current;
|
|
1045
|
+
react.useEffect(() => {
|
|
1046
|
+
tracker.start();
|
|
1047
|
+
return () => tracker.stop();
|
|
1048
|
+
}, []);
|
|
1049
|
+
const handlers = react.useMemo(() => tracker.getEventHandlers(), [tracker]);
|
|
1050
|
+
const getMetrics = react.useCallback(() => tracker.getMetrics(), [tracker]);
|
|
1051
|
+
return { tracker, handlers, getMetrics };
|
|
1052
|
+
}
|
|
1053
|
+
function useRum(config) {
|
|
1054
|
+
const rum = react.useMemo(() => simplrRUM, []);
|
|
1055
|
+
react.useEffect(() => {
|
|
1056
|
+
if (config && !rum.isInitialized()) {
|
|
1057
|
+
rum.initialize(config);
|
|
1058
|
+
}
|
|
1059
|
+
}, [config?.apiKey, config?.applicationId, config?.baseUrl]);
|
|
1060
|
+
return rum;
|
|
1061
|
+
}
|
|
1062
|
+
function useTrackView(name, attributes, rum = simplrRUM) {
|
|
1063
|
+
const attrsKey = JSON.stringify(attributes ?? null);
|
|
1064
|
+
react.useEffect(() => {
|
|
1065
|
+
if (!name) return;
|
|
1066
|
+
try {
|
|
1067
|
+
rum.trackView(name, attributes);
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}, [name, attrsKey, rum]);
|
|
1071
|
+
}
|
|
1072
|
+
function useTrackAction(rum = simplrRUM) {
|
|
1073
|
+
return react.useCallback(
|
|
1074
|
+
(name, attributes) => {
|
|
1075
|
+
try {
|
|
1076
|
+
rum.trackAction(name, attributes);
|
|
1077
|
+
} catch {
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
1080
|
+
[rum]
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
function useTrackError(rum = simplrRUM) {
|
|
1084
|
+
return react.useCallback(
|
|
1085
|
+
(error, attributes) => {
|
|
1086
|
+
try {
|
|
1087
|
+
rum.trackError(error, attributes);
|
|
1088
|
+
} catch {
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
[rum]
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
exports.useDeviceSignals = useDeviceSignals;
|
|
1096
|
+
exports.useFeatureFlag = useFeatureFlag;
|
|
1097
|
+
exports.useRum = useRum;
|
|
1098
|
+
exports.useSimplr = useSimplr;
|
|
1099
|
+
exports.useTouchBiometrics = useTouchBiometrics;
|
|
1100
|
+
exports.useTrackAction = useTrackAction;
|
|
1101
|
+
exports.useTrackError = useTrackError;
|
|
1102
|
+
exports.useTrackView = useTrackView;
|
|
1103
|
+
//# sourceMappingURL=index.cjs.map
|
|
1104
|
+
//# sourceMappingURL=index.cjs.map
|