firstusers 1.0.1 → 1.0.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
@@ -1,16 +1,15 @@
1
- # First Users SDK
1
+ # firstusers
2
2
 
3
- **Automatic tester tracking with heartbeat monitoring, offline retry, device fingerprinting, and guest user auto-tracking.**
3
+ Session tracking SDK for React Native/Expo apps. Tracks authenticated user sessions with automatic 30-second heartbeat updates.
4
4
 
5
5
  ## Features
6
6
 
7
- ✅ **GU Auto-Tracking** Automatically generates Guest ID and starts tracking immediately
8
- 🔗 **FU-GU Binding** Links past GU activity when user enters FU ID
9
- 💓 **Global Heartbeat** Persists across route changes, sends every 30 seconds
10
- 📶 **Offline Queue** Failed requests queued and retried on startup
11
- ⏱️ **Session Duration** — Real-time tracking sent with every request
12
- 📱 **Device Info** Brand (Samsung, Xiaomi…) and API level auto-detected
13
- 📦 **Dynamic Package Name** — Passed via props for reuse
7
+ ✅ **Post-Login Tracking** - Only tracks authenticated user sessions (excludes login screen time)
8
+ **Automatic Heartbeat** - Updates session duration every 30 seconds
9
+ **Survives App Kill** - Last heartbeat data already saved (max 30s loss)
10
+ **Zero Configuration** - Expo Config Plugin auto-configures your project
11
+ **Privacy-Focused** - No sensitive data collection
12
+ **Lightweight** - Minimal battery and performance impact
14
13
 
15
14
  ## Installation
16
15
 
@@ -18,168 +17,323 @@
18
17
  npm install firstusers
19
18
  ```
20
19
 
21
- ## Quick Start
20
+ Or with yarn:
22
21
 
23
- ### 1. Import the component
22
+ ```bash
23
+ yarn add firstusers
24
+ ```
24
25
 
25
- ```javascript
26
- import FirstUsersIntegration from 'firstusers';
26
+ ## Configuration
27
+
28
+ ### Step 1: Add Plugin to app.json
29
+
30
+ ```json
31
+ {
32
+ "expo": {
33
+ "plugins": [
34
+ [
35
+ "firstusers",
36
+ {
37
+ "apiUrl": "https://your-api.workers.dev"
38
+ }
39
+ ]
40
+ ]
41
+ }
42
+ }
27
43
  ```
28
44
 
29
- ### 2. Add to your app
45
+ **Plugin Options:**
30
46
 
31
- ```jsx
32
- function App() {
33
- return (
34
- <div>
35
- <h1>My App</h1>
36
-
37
- {/* Add First Users integration */}
38
- <FirstUsersIntegration appPackageName="com.yourcompany.app" />
39
-
40
- {/* Your app content */}
41
- </div>
42
- );
43
- }
47
+ | Option | Type | Required | Default |
48
+ |--------|------|----------|---------|
49
+ | `apiUrl` | string | No | `https://cotester-api.01hunterwl.workers.dev` |
50
+
51
+ ### Step 2: Rebuild Your App
52
+
53
+ The Config Plugin needs a native rebuild to apply changes:
54
+
55
+ ```bash
56
+ # Development build
57
+ npx expo run:android
58
+
59
+ # Or prebuild for custom builds
60
+ npx expo prebuild --clean
44
61
  ```
45
62
 
46
- ### 3. Track feature usage (optional)
63
+ ⚠️ **Important:** You must rebuild the app after installing this package. Expo Go does not support custom native modules.
64
+
65
+ ## Usage
47
66
 
48
- After the component is mounted, you can track specific features:
67
+ ### Basic Setup
49
68
 
50
- ```javascript
51
- // Track button clicks
52
- window.trackFUFeature('button_click');
69
+ ```typescript
70
+ import futracker from 'firstusers';
71
+ import { supabase } from './lib/supabase';
53
72
 
54
- // Track video plays
55
- window.trackFUFeature('video_play');
73
+ // Configure once at app startup
74
+ futracker.configure('https://your-api.workers.dev');
75
+ futracker.setPackageName('com.yourcompany.app');
56
76
 
57
- // Track any custom event
58
- window.trackFUFeature('premium_upgrade');
77
+ // Start tracking after successful login
78
+ supabase.auth.onAuthStateChange((event, session) => {
79
+ if (event === 'SIGNED_IN' && session) {
80
+ futracker.startTracking();
81
+ }
82
+ });
59
83
  ```
60
84
 
61
- ## How It Works
85
+ ### Alternative: Convenience Function
62
86
 
63
- ### Automatic Guest User (GU) Tracking
87
+ ```typescript
88
+ import { startTracking } from 'firstusers';
64
89
 
65
- When users first visit your app:
66
- 1. **Auto-generates GU ID** — System creates a unique `gu-XXXXXXX` ID
67
- 2. **Starts tracking immediately** — All activity is recorded with the GU ID
68
- 3. **Shows yellow banner** — User sees "Anonymous Tracking Active" status
90
+ // One-line start with package name
91
+ if (loginSuccess) {
92
+ startTracking('com.yourcompany.app');
93
+ }
94
+ ```
69
95
 
70
- ### FU ID Binding
96
+ ### Optional: Manual Stop (usually not needed)
71
97
 
72
- When users enter their First Users ID:
73
- 1. **User enters FU ID** — Format: `fu-XXXXX` (8 characters)
74
- 2. **Binds past activity** — All previous GU data is linked to their FU account
75
- 3. **Shows green banner** — User sees "First Users Tester Active" status
76
- 4. **Data preserved** — Full 14-day tracking history is maintained
98
+ ```typescript
99
+ import { stopTracking } from 'firstusers';
100
+
101
+ // Optional: Call on logout
102
+ function handleLogout() {
103
+ stopTracking(); // Not required - heartbeat already saved data
104
+ // ... rest of logout logic
105
+ }
106
+ ```
77
107
 
78
108
  ## API Reference
79
109
 
80
- ### Component Props
110
+ ### `futracker.configure(apiUrl: string)`
111
+
112
+ Configure the API endpoint. Call once at app startup.
81
113
 
82
- | Prop | Type | Required | Default | Description |
83
- |------|------|----------|---------|-------------|
84
- | `appPackageName` | string | Yes | - | Your app's package name (e.g., "com.yourcompany.app") |
85
- | `apiEndpoint` | string | No | First Users API | Custom API endpoint if you're self-hosting |
114
+ **Parameters:**
115
+ - `apiUrl` (string) - Your First Users API endpoint
86
116
 
87
- ### Global Functions
117
+ **Example:**
118
+ ```typescript
119
+ futracker.configure('https://your-api.workers.dev');
120
+ ```
88
121
 
89
- #### `window.trackFUFeature(featureName: string)`
122
+ ### `futracker.setPackageName(packageName: string)`
90
123
 
91
- Track custom feature usage. Available after component mounts.
124
+ Set your app's package name (same as `applicationId` in build.gradle).
92
125
 
93
- ```javascript
94
- window.trackFUFeature('search_product');
95
- window.trackFUFeature('checkout_completed');
126
+ **Parameters:**
127
+ - `packageName` (string) - Your app's package name
128
+
129
+ **Example:**
130
+ ```typescript
131
+ futracker.setPackageName('com.yourcompany.app');
96
132
  ```
97
133
 
98
- ### Exported Utilities
134
+ ### `futracker.startTracking(packageName?: string)`
135
+
136
+ Start tracking user session. Call after successful login.
99
137
 
100
- #### `FUHeartbeat`
138
+ **Parameters:**
139
+ - `packageName` (string, optional) - Override package name for this call
101
140
 
102
- Global heartbeat manager singleton.
141
+ **Example:**
142
+ ```typescript
143
+ // Use pre-configured package name
144
+ futracker.startTracking();
103
145
 
104
- ```javascript
105
- import { FUHeartbeat } from 'firstusers';
146
+ // Or pass directly
147
+ futracker.startTracking('com.yourcompany.app');
148
+ ```
106
149
 
107
- // Start heartbeat manually (usually handled by component)
108
- FUHeartbeat.start('fu-12345', trackFunction);
150
+ ### `futracker.stopTracking()`
109
151
 
110
- // Get current session duration in seconds
111
- const duration = FUHeartbeat.getDuration();
152
+ Stop tracking session. **Optional** - heartbeat auto-saves data every 30s.
112
153
 
113
- // Stop heartbeat
114
- FUHeartbeat.stop();
154
+ **Example:**
155
+ ```typescript
156
+ futracker.stopTracking();
115
157
  ```
116
158
 
117
- #### `generateGuestId()`
159
+ ### `futracker.setDebug(enabled: boolean)`
118
160
 
119
- Generate a unique Guest User ID.
161
+ Enable or disable debug logging. Debug mode is enabled by default.
120
162
 
121
- ```javascript
122
- import { generateGuestId } from 'firstusers';
163
+ **Parameters:**
164
+ - `enabled` (boolean) - Whether to enable debug logs
123
165
 
124
- const guestId = generateGuestId(); // Returns: "gu-abc123xyz"
166
+ **Example:**
167
+ ```typescript
168
+ // Disable debug logs in production
169
+ futracker.setDebug(false);
125
170
  ```
126
171
 
127
- #### `getOrCreateUserId()`
172
+ ### `futracker.isActive()`
128
173
 
129
- Get existing user ID from localStorage or create new GU ID.
174
+ Check if tracking is currently active.
175
+
176
+ **Returns:** boolean indicating if tracking is active
177
+
178
+ **Example:**
179
+ ```typescript
180
+ if (futracker.isActive()) {
181
+ console.log('Tracking is running');
182
+ }
183
+ ```
130
184
 
131
- ```javascript
132
- import { getOrCreateUserId } from 'firstusers';
185
+ ## How It Works
133
186
 
134
- const userId = getOrCreateUserId(); // Returns: "fu-12345" or "gu-abc123"
187
+ 1. **Initial Record**: When `startTracking()` is called, creates one record in `fu_tracking` table with `session_duration = 0`
188
+ 2. **Heartbeat Updates**: Every 30 seconds, UPDATE the same record's `session_duration` to current elapsed time
189
+ 3. **App Kill Safety**: If app is killed, last heartbeat data is already saved (accurate within ~30 seconds)
190
+ 4. **No Data Loss**: No need to call `stopTracking()` - heartbeat already persisted the duration
191
+
192
+ ## Database Schema
193
+
194
+ The tracker sends data to the `fu_tracking` table:
195
+
196
+ ```sql
197
+ CREATE TABLE fu_tracking (
198
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
199
+ fu_id TEXT NOT NULL, -- Guest ID (gu-XXXXX) or User ID (fu-XXXXX)
200
+ app_package TEXT NOT NULL, -- Your app's package name
201
+ timestamp TEXT NOT NULL, -- ISO 8601 UTC timestamp
202
+ action TEXT NOT NULL, -- 'session_start'
203
+ device_brand TEXT, -- Device manufacturer
204
+ device_api_level TEXT, -- Android version
205
+ user_agent TEXT, -- Platform info
206
+ session_id TEXT NOT NULL, -- Unique session identifier
207
+ session_duration INTEGER NOT NULL -- Seconds since session start
208
+ );
135
209
  ```
136
210
 
137
- #### `getDeviceInfo()`
211
+ ## Data Collected
212
+
213
+ **Session Data:**
214
+ - Session start time (UTC timestamp)
215
+ - Session duration (seconds)
216
+ - Session ID (unique per app open)
217
+
218
+ **Device Info:**
219
+ - Device manufacturer (e.g., "Google", "Samsung")
220
+ - Android API level (e.g., "Android 33")
221
+ - Platform (e.g., "Android/13")
138
222
 
139
- Extract device information from user agent.
223
+ **No Personal Data:**
224
+ - ❌ No user location
225
+ - ❌ No contact information
226
+ - ❌ No sensitive permissions
140
227
 
141
- ```javascript
142
- import { getDeviceInfo } from 'firstusers';
228
+ ## Troubleshooting
143
229
 
144
- const device = getDeviceInfo();
145
- // Returns: {
146
- // device_brand: "Samsung",
147
- // device_api_level: "Android 13",
148
- // user_agent: "Mozilla/5.0..."
149
- // }
230
+ ### Module not found error
231
+
232
+ **Error:** `The package 'firstusers' doesn't seem to be linked`
233
+
234
+ **Solution:** You must rebuild the native app:
235
+ ```bash
236
+ npx expo run:android
150
237
  ```
151
238
 
152
- ## UI States
239
+ ### Tracking not starting
153
240
 
154
- ### 1. Initial State (Rare)
155
- Gray banner asking user to enter FU ID (only shown if GU auto-generation fails).
241
+ **Check:**
242
+ 1. Did you call `setPackageName()` before `startTracking()`?
243
+ 2. Is the package name correct? (Check `android/app/build.gradle` → `applicationId`)
244
+ 3. Did you rebuild after installing the package?
156
245
 
157
- ### 2. Anonymous Tracking (GU Active)
158
- Yellow banner showing:
159
- - Current Guest ID
160
- - Session duration
161
- - Input field to enter FU ID
162
- - "Link FU ID" button
246
+ **Debug:**
247
+ ```typescript
248
+ // Enable console logs
249
+ futracker.configure('https://your-api.workers.dev');
250
+ futracker.setPackageName('com.yourcompany.app');
251
+ futracker.startTracking();
252
+ // Check console for "[FirstUsers]" logs
253
+ ```
254
+
255
+ ### Build errors
163
256
 
164
- ### 3. Active Tracking (FU Active)
165
- Green banner showing:
166
- - FU ID is active
167
- - Session duration
257
+ **Error:** `Plugin [id: 'expo-module-gradle-plugin'] was not found`
168
258
 
169
- ## Data Privacy
259
+ **Solution:** Clean and rebuild:
260
+ ```bash
261
+ cd android
262
+ ./gradlew clean
263
+ cd ..
264
+ npx expo prebuild --clean
265
+ npx expo run:android
266
+ ```
170
267
 
171
- - All data is stored in First Users' secure database
172
- - User IDs are anonymized (GU or FU format)
173
- - Device info helps identify unique testers
174
- - Session data used for 14-day activity tracking
268
+ ## Requirements
269
+
270
+ - **React Native:** 0.70+
271
+ - **Expo:** 50+
272
+ - **Android:** API 24+ (Android 7.0)
273
+ - **Kotlin:** 1.8+
274
+
275
+ ## Example Integration
276
+
277
+ Complete example with Supabase authentication:
278
+
279
+ ```typescript
280
+ // App.tsx
281
+ import React, { useEffect } from 'react';
282
+ import futracker from 'firstusers';
283
+ import { supabase } from './lib/supabase';
284
+
285
+ export default function App() {
286
+ useEffect(() => {
287
+ // Configure tracker once
288
+ futracker.configure('https://cotester-api.01hunterwl.workers.dev');
289
+ futracker.setPackageName('net.woodlogos.app.firstusers');
290
+
291
+ // Listen for auth changes
292
+ const { data: authListener } = supabase.auth.onAuthStateChange(
293
+ async (event, session) => {
294
+ if (event === 'SIGNED_IN' && session) {
295
+ // Start tracking after successful login
296
+ futracker.startTracking();
297
+ console.log('✅ Tracking started for authenticated user');
298
+ }
299
+ }
300
+ );
301
+
302
+ return () => {
303
+ authListener?.subscription.unsubscribe();
304
+ };
305
+ }, []);
175
306
 
176
- ## Support
307
+ return (
308
+ // Your app components
309
+ );
310
+ }
311
+ ```
312
+
313
+ ## Privacy & Compliance
177
314
 
178
- For issues or questions:
179
- - Email: support@firstusers.com
180
- - Documentation: https://docs.firstusers.com
181
- - GitHub: https://github.com/firstusers/firstusers-sdk
315
+ **GDPR Compliant:** Only tracks usage duration and basic device info (no personal data)
316
+
317
+ **Disclose in Privacy Policy:**
318
+ ```
319
+ This app collects anonymous usage data for beta testing:
320
+ - Session duration (start/end time)
321
+ - Device model and Android version
322
+ - App package name
323
+
324
+ No personal information is collected.
325
+ ```
182
326
 
183
327
  ## License
184
328
 
185
329
  MIT
330
+
331
+ ## Support
332
+
333
+ - **Issues:** [GitHub Issues](https://github.com/firstusers/react-native-tracker/issues)
334
+ - **Documentation:** [GitHub Wiki](https://github.com/firstusers/react-native-tracker/wiki)
335
+ - **Email:** support@firstusers.io
336
+
337
+ ---
338
+
339
+ **Made with ❤️ by First Users Team**
@@ -0,0 +1,32 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+ apply plugin: 'expo-module-gradle-plugin'
4
+
5
+ android {
6
+ namespace "com.firstusers.tracker"
7
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
8
+
9
+ defaultConfig {
10
+ minSdkVersion safeExtGet("minSdkVersion", 24)
11
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
12
+ }
13
+
14
+ compileOptions {
15
+ sourceCompatibility JavaVersion.VERSION_17
16
+ targetCompatibility JavaVersion.VERSION_17
17
+ }
18
+
19
+ kotlinOptions {
20
+ jvmTarget = "17"
21
+ }
22
+ }
23
+
24
+ dependencies {
25
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
26
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
27
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
28
+ }
29
+
30
+ def safeExtGet(prop, fallback) {
31
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
32
+ }
@@ -0,0 +1,113 @@
1
+ package com.firstusers.tracker
2
+
3
+ import android.content.Context
4
+ import android.os.Build
5
+ import kotlinx.coroutines.*
6
+ import org.json.JSONObject
7
+ import java.net.HttpURLConnection
8
+ import java.net.URL
9
+ import java.text.SimpleDateFormat
10
+ import java.util.*
11
+
12
+ object FirstUsers {
13
+ private const val P = "fu_prefs"
14
+ private const val K = "fu_device_id"
15
+ private var apiUrl = "https://cotester-api.01hunterwl.workers.dev"
16
+ private var sid = ""
17
+ private var t0 = 0L
18
+ private var pkg = ""
19
+ private var tmr: Timer? = null
20
+ private var isTracking = false
21
+
22
+ fun configure(url: String) {
23
+ apiUrl = url
24
+ }
25
+
26
+ fun start(c: Context, ap: String) {
27
+ // Prevent duplicate tracking sessions
28
+ if (isTracking) {
29
+ android.util.Log.d("FirstUsers", "Already tracking, ignoring duplicate start call")
30
+ return
31
+ }
32
+ isTracking = true
33
+
34
+ // Cancel previous timer if exists (prevent duplicate timers)
35
+ tmr?.cancel()
36
+ tmr = null
37
+
38
+ pkg = ap
39
+ sid = "s_" + UUID.randomUUID().toString().take(8)
40
+ t0 = System.currentTimeMillis()
41
+
42
+ // Create initial record (session_duration = 0)
43
+ post("$apiUrl/api/fu/track",
44
+ JSONObject()
45
+ .put("fu_id", gid(c))
46
+ .put("app_package", pkg)
47
+ .put("timestamp", ts())
48
+ .put("action","session_start")
49
+ .put("device_brand", Build.MANUFACTURER)
50
+ .put("device_api_level", "Android "+Build.VERSION.SDK_INT)
51
+ .put("user_agent", "Android/"+Build.VERSION.RELEASE)
52
+ .put("session_id", sid)
53
+ .put("session_duration", 0))
54
+
55
+ // Heartbeat every 30s to update session_duration
56
+ tmr = Timer()
57
+ tmr?.schedule(object:TimerTask(){
58
+ override fun run() { hb() }
59
+ }, 30000, 30000)
60
+ }
61
+
62
+ fun stop(c: Context) {
63
+ if (!isTracking) return
64
+ isTracking = false
65
+ tmr?.cancel()
66
+ tmr = null
67
+ hb() // final update
68
+ }
69
+
70
+ private fun dur() = ((System.currentTimeMillis()-t0)/1000).toInt()
71
+
72
+ private fun hb() {
73
+ post("$apiUrl/api/fu/track/heartbeat",
74
+ JSONObject()
75
+ .put("session_id", sid)
76
+ .put("session_duration",dur()))
77
+ }
78
+
79
+ private fun ts(): String {
80
+ val f = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
81
+ f.timeZone = TimeZone.getTimeZone("UTC")
82
+ return f.format(Date())
83
+ }
84
+
85
+ private fun gid(c: Context): String {
86
+ val sp = c.getSharedPreferences(P, Context.MODE_PRIVATE)
87
+ var id = sp.getString(K, null)
88
+ if (id == null) {
89
+ id = "gu-" + UUID.randomUUID().toString().take(5)
90
+ sp.edit().putString(K, id).apply()
91
+ }
92
+ return id
93
+ }
94
+
95
+ private fun post(u: String, j: JSONObject) {
96
+ CoroutineScope(Dispatchers.IO).launch {
97
+ try {
98
+ val c = URL(u).openConnection() as HttpURLConnection
99
+ c.requestMethod = "POST"
100
+ c.setRequestProperty("Content-Type", "application/json")
101
+ c.doOutput = true
102
+ c.connectTimeout = 5000
103
+ c.outputStream.use {
104
+ it.write(j.toString().toByteArray())
105
+ }
106
+ c.responseCode
107
+ c.disconnect()
108
+ } catch (e: Exception) {
109
+ android.util.Log.w("FirstUsers", "Network request failed: ${e.message}")
110
+ }
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,29 @@
1
+ package com.firstusers.tracker
2
+
3
+ import android.content.Context
4
+ import expo.modules.kotlin.modules.Module
5
+ import expo.modules.kotlin.modules.ModuleDefinition
6
+
7
+ class FirstUsersModule : Module() {
8
+ override fun definition() = ModuleDefinition {
9
+ Name("FirstUsersTracker")
10
+
11
+ Function("configure") { apiUrl: String ->
12
+ FirstUsers.configure(apiUrl)
13
+ }
14
+
15
+ Function("start") { packageName: String ->
16
+ val context = appContext.reactContext as? Context
17
+ context?.let {
18
+ FirstUsers.start(it, packageName)
19
+ }
20
+ }
21
+
22
+ Function("stop") {
23
+ val context = appContext.reactContext as? Context
24
+ context?.let {
25
+ FirstUsers.stop(it)
26
+ }
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,48 @@
1
+ export declare class futracker {
2
+ private static apiUrl;
3
+ private static packageName;
4
+ private static isTracking;
5
+ private static debugMode;
6
+ /**
7
+ * Configure the tracker with custom API URL
8
+ * @param apiUrl - Your First Users API endpoint
9
+ */
10
+ static configure(apiUrl: string): void;
11
+ /**
12
+ * Enable or disable debug logging
13
+ * @param enabled - Whether to enable debug logs
14
+ */
15
+ static setDebug(enabled: boolean): void;
16
+ /**
17
+ * Set the package name (usually from build.gradle applicationId)
18
+ * @param packageName - Your app's package name
19
+ */
20
+ static setPackageName(packageName: string): void;
21
+ /**
22
+ * Start tracking user session
23
+ * Call this after successful user login
24
+ * @param packageName - Optional: override package name
25
+ */
26
+ static startTracking(packageName?: string): void;
27
+ /**
28
+ * Stop tracking user session
29
+ * Optional: Can be called on logout, but heartbeat already auto-saves every 30s
30
+ */
31
+ static stopTracking(): void;
32
+ /**
33
+ * Check if tracking is currently active
34
+ * @returns boolean indicating if tracking is active
35
+ */
36
+ static isActive(): boolean;
37
+ }
38
+ /**
39
+ * Convenience function for quick start
40
+ * @param packageName - Your app's package name
41
+ * @param apiUrl - Optional: custom API URL
42
+ */
43
+ export declare function startTracking(packageName: string, apiUrl?: string): void;
44
+ /**
45
+ * Convenience function for stop
46
+ */
47
+ export declare function stopTracking(): void;
48
+ export default futracker;
package/build/index.js ADDED
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.futracker = void 0;
4
+ exports.startTracking = startTracking;
5
+ exports.stopTracking = stopTracking;
6
+ const react_native_1 = require("react-native");
7
+ const LINKING_ERROR = `The package 'firstusers' doesn't seem to be linked. Make sure:\n\n` +
8
+ react_native_1.Platform.select({ ios: "- Run 'pod install'\n", default: '' }) +
9
+ '- You rebuilt the app after installing the package\n' +
10
+ '- You are not using Expo Go (this module requires custom native code)\n';
11
+ const FirstUsersTrackerModule = react_native_1.NativeModules.FirstUsersTracker
12
+ ? react_native_1.NativeModules.FirstUsersTracker
13
+ : new Proxy({}, {
14
+ get() {
15
+ throw new Error(LINKING_ERROR);
16
+ },
17
+ });
18
+ class futracker {
19
+ /**
20
+ * Configure the tracker with custom API URL
21
+ * @param apiUrl - Your First Users API endpoint
22
+ */
23
+ static configure(apiUrl) {
24
+ this.apiUrl = apiUrl;
25
+ FirstUsersTrackerModule.configure(apiUrl);
26
+ if (this.debugMode)
27
+ console.log('[FirstUsers] Configured with API:', apiUrl);
28
+ }
29
+ /**
30
+ * Enable or disable debug logging
31
+ * @param enabled - Whether to enable debug logs
32
+ */
33
+ static setDebug(enabled) {
34
+ this.debugMode = enabled;
35
+ }
36
+ /**
37
+ * Set the package name (usually from build.gradle applicationId)
38
+ * @param packageName - Your app's package name
39
+ */
40
+ static setPackageName(packageName) {
41
+ this.packageName = packageName;
42
+ }
43
+ /**
44
+ * Start tracking user session
45
+ * Call this after successful user login
46
+ * @param packageName - Optional: override package name
47
+ */
48
+ static startTracking(packageName) {
49
+ const pkg = packageName || this.packageName;
50
+ if (!pkg) {
51
+ console.error('[FirstUsers] Package name not set. Call setPackageName() or pass it as argument.');
52
+ return;
53
+ }
54
+ if (this.isTracking) {
55
+ if (this.debugMode)
56
+ console.log('[FirstUsers] Already tracking, ignoring duplicate call');
57
+ return;
58
+ }
59
+ try {
60
+ FirstUsersTrackerModule.start(pkg);
61
+ this.isTracking = true;
62
+ if (this.debugMode)
63
+ console.log('[FirstUsers] Tracking started for:', pkg);
64
+ }
65
+ catch (error) {
66
+ console.error('[FirstUsers] Failed to start tracking:', error);
67
+ }
68
+ }
69
+ /**
70
+ * Stop tracking user session
71
+ * Optional: Can be called on logout, but heartbeat already auto-saves every 30s
72
+ */
73
+ static stopTracking() {
74
+ if (!this.isTracking) {
75
+ if (this.debugMode)
76
+ console.log('[FirstUsers] Not currently tracking');
77
+ return;
78
+ }
79
+ try {
80
+ FirstUsersTrackerModule.stop();
81
+ this.isTracking = false;
82
+ if (this.debugMode)
83
+ console.log('[FirstUsers] Tracking stopped');
84
+ }
85
+ catch (error) {
86
+ console.error('[FirstUsers] Failed to stop tracking:', error);
87
+ }
88
+ }
89
+ /**
90
+ * Check if tracking is currently active
91
+ * @returns boolean indicating if tracking is active
92
+ */
93
+ static isActive() {
94
+ return this.isTracking;
95
+ }
96
+ }
97
+ exports.futracker = futracker;
98
+ futracker.apiUrl = 'https://cotester-api.01hunterwl.workers.dev';
99
+ futracker.packageName = '';
100
+ futracker.isTracking = false;
101
+ futracker.debugMode = true;
102
+ /**
103
+ * Convenience function for quick start
104
+ * @param packageName - Your app's package name
105
+ * @param apiUrl - Optional: custom API URL
106
+ */
107
+ function startTracking(packageName, apiUrl) {
108
+ if (apiUrl) {
109
+ futracker.configure(apiUrl);
110
+ }
111
+ futracker.startTracking(packageName);
112
+ }
113
+ /**
114
+ * Convenience function for stop
115
+ */
116
+ function stopTracking() {
117
+ futracker.stopTracking();
118
+ }
119
+ exports.default = futracker;
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["android"],
3
+ "android": {
4
+ "modules": ["com.firstusers.tracker.FirstUsersModule"]
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,42 +1,47 @@
1
1
  {
2
2
  "name": "firstusers",
3
- "version": "1.0.1",
4
- "description": "First Users - Automatic tester tracking with heartbeat monitoring, offline retry, device fingerprinting, and guest user auto-tracking",
5
- "main": "dist/index.js",
6
- "types": "index.d.ts",
7
- "files": [
8
- "dist",
9
- "index.d.ts",
10
- "README.md",
11
- "LICENSE"
12
- ],
3
+ "version": "1.0.3",
4
+ "description": "Usage tracking SDK for React Native/Expo apps. Tracks authenticated user sessions with 30s heartbeat to First Users platform.",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "app.plugin": "./plugin/src/withFirstUsers.js",
13
8
  "scripts": {
14
- "test": "echo \"Error: no test specified\" && exit 1",
15
- "prepublishOnly": "echo 'Publishing firstusers package...' && ls -la dist/"
9
+ "build": "tsc",
10
+ "prepare": "npm run build"
16
11
  },
17
12
  "keywords": [
18
- "firstusers",
19
- "tracking",
13
+ "react-native",
14
+ "expo",
20
15
  "analytics",
21
- "tester",
22
- "heartbeat",
23
- "offline",
24
- "react",
25
- "guest-user"
16
+ "tracking",
17
+ "session",
18
+ "beta-testing",
19
+ "first-users"
26
20
  ],
27
21
  "author": "First Users Team",
28
22
  "license": "MIT",
23
+ "peerDependencies": {
24
+ "expo": ">=50.0.0",
25
+ "react": "*",
26
+ "react-native": "*"
27
+ },
28
+ "devDependencies": {
29
+ "@expo/config-plugins": "^9.0.0",
30
+ "@types/react": "^19.0.0",
31
+ "typescript": "^5.3.0"
32
+ },
29
33
  "repository": {
30
34
  "type": "git",
31
- "url": "https://github.com/01hunterwl-commits/fucallback.git"
32
- },
33
- "peerDependencies": {
34
- "react": ">=16.8.0"
35
+ "url": "https://github.com/firstusers/react-native-tracker.git"
35
36
  },
36
- "engines": {
37
- "node": ">=14.0.0"
37
+ "bugs": {
38
+ "url": "https://github.com/firstusers/react-native-tracker/issues"
38
39
  },
39
- "devDependencies": {
40
- "npm-packlist": "^10.0.3"
41
- }
40
+ "homepage": "https://github.com/firstusers/react-native-tracker#readme",
41
+ "files": [
42
+ "build/",
43
+ "android/",
44
+ "plugin/",
45
+ "expo-module.config.json"
46
+ ]
42
47
  }
@@ -0,0 +1,60 @@
1
+ const { withAndroidManifest, withAppBuildGradle } = require('@expo/config-plugins');
2
+
3
+ /**
4
+ * Expo Config Plugin for First Users Tracker
5
+ * Automatically configures Android project for session tracking
6
+ */
7
+ function withFirstUsers(config, props = {}) {
8
+ const apiUrl = props.apiUrl || 'https://cotester-api.01hunterwl.workers.dev';
9
+
10
+ // Add INTERNET and DETECT_SCREEN_CAPTURE permissions to AndroidManifest.xml
11
+ config = withAndroidManifest(config, (config) => {
12
+ const androidManifest = config.modResults;
13
+ const mainApplication = androidManifest.manifest;
14
+
15
+ // Ensure permissions array exists
16
+ if (!mainApplication['uses-permission']) {
17
+ mainApplication['uses-permission'] = [];
18
+ }
19
+
20
+ const permissions = mainApplication['uses-permission'];
21
+
22
+ // Add INTERNET permission if not exists
23
+ const hasInternet = permissions.some(
24
+ (perm) => perm.$?.['android:name'] === 'android.permission.INTERNET'
25
+ );
26
+ if (!hasInternet) {
27
+ permissions.push({
28
+ $: { 'android:name': 'android.permission.INTERNET' }
29
+ });
30
+ }
31
+
32
+ return config;
33
+ });
34
+
35
+ // Add Kotlin Coroutines dependencies to app/build.gradle
36
+ config = withAppBuildGradle(config, (config) => {
37
+ const buildGradle = config.modResults.contents;
38
+
39
+ // Check if coroutines dependencies already exist
40
+ if (!buildGradle.includes('kotlinx-coroutines-android')) {
41
+ // Add dependencies after the dependencies block starts
42
+ const dependenciesRegex = /dependencies\s*{/;
43
+ if (dependenciesRegex.test(buildGradle)) {
44
+ config.modResults.contents = buildGradle.replace(
45
+ dependenciesRegex,
46
+ `dependencies {
47
+ // First Users Tracker - Kotlin Coroutines
48
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
49
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'`
50
+ );
51
+ }
52
+ }
53
+
54
+ return config;
55
+ });
56
+
57
+ return config;
58
+ }
59
+
60
+ module.exports = withFirstUsers;
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 First Users Team
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/dist/index.js DELETED
@@ -1,304 +0,0 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
-
3
- // ---- Global Heartbeat Manager (persists across route changes) ----
4
- const FUHeartbeat = {
5
- _interval: null,
6
- _startTime: null,
7
- _fuId: null,
8
- _trackFn: null,
9
- start(fuId, trackFn) {
10
- if (this._interval) return; // already running
11
- this._fuId = fuId;
12
- this._trackFn = trackFn;
13
- this._startTime = Date.now();
14
- this._interval = setInterval(() => {
15
- const dur = Math.floor((Date.now() - this._startTime) / 1000);
16
- this._trackFn(this._fuId, 'heartbeat', { session_duration: dur });
17
- }, 30000);
18
- if (typeof window !== 'undefined') {
19
- window.addEventListener('beforeunload', this._onUnload);
20
- }
21
- },
22
- _onUnload() {
23
- FUHeartbeat.stop(true);
24
- },
25
- stop(sendEnd = false) {
26
- if (!this._interval) return;
27
- clearInterval(this._interval);
28
- if (sendEnd && this._trackFn && this._fuId) {
29
- const dur = Math.floor((Date.now() - this._startTime) / 1000);
30
- this._trackFn(this._fuId, 'session_end', { session_duration: dur });
31
- }
32
- if (typeof window !== 'undefined') {
33
- window.removeEventListener('beforeunload', this._onUnload);
34
- }
35
- this._interval = null;
36
- },
37
- getDuration() {
38
- if (!this._startTime) return 0;
39
- return Math.floor((Date.now() - this._startTime) / 1000);
40
- }
41
- };
42
-
43
- // ---- GU (Guest User) Auto-Generation ----
44
- function generateGuestId() {
45
- const randomStr = Math.random().toString(36).substr(2, 9);
46
- return 'gu-' + randomStr;
47
- }
48
-
49
- function getOrCreateUserId() {
50
- if (typeof localStorage === 'undefined') return null;
51
- let userId = localStorage.getItem('fu_tester_id');
52
- if (!userId) {
53
- // Auto-generate GU ID for anonymous tracking
54
- userId = generateGuestId();
55
- localStorage.setItem('fu_tester_id', userId);
56
- console.log('Auto-generated Guest User ID:', userId);
57
- }
58
- return userId;
59
- }
60
-
61
- // ---- Device Info Helper ----
62
- function getDeviceInfo() {
63
- const ua = (typeof navigator !== 'undefined' ? navigator.userAgent : '') || '';
64
- let device_brand = 'unknown';
65
- let device_api_level = 'unknown';
66
- // Detect brand from common UA patterns
67
- const brands = ['Samsung','Xiaomi','Huawei','OnePlus','Oppo','Vivo','Realme',
68
- 'Motorola','LG','Sony','Google','Pixel','Nokia','Asus','ZTE','Lenovo'];
69
- for (const b of brands) {
70
- if (ua.toLowerCase().includes(b.toLowerCase())) { device_brand = b; break; }
71
- }
72
- // Detect Android API level
73
- const androidMatch = ua.match(/Android\s([\d.]+)/);
74
- if (androidMatch) device_api_level = 'Android ' + androidMatch[1];
75
- const iosMatch = ua.match(/OS\s([\d_]+)\slike/);
76
- if (iosMatch) device_api_level = 'iOS ' + iosMatch[1].replace(/_/g, '.');
77
- return { device_brand, device_api_level, user_agent: ua };
78
- }
79
-
80
- // ---- Offline Queue ----
81
- function queueFailedRequest(data) {
82
- if (typeof localStorage === 'undefined') return;
83
- try {
84
- const q = JSON.parse(localStorage.getItem('fu_failed_requests') || '[]');
85
- q.push({ ...data, queued_at: new Date().toISOString() });
86
- localStorage.setItem('fu_failed_requests', JSON.stringify(q));
87
- } catch (e) { console.error('Queue error:', e); }
88
- }
89
-
90
- async function processFailedRequests(trackFn) {
91
- if (typeof localStorage === 'undefined') return;
92
- try {
93
- const q = JSON.parse(localStorage.getItem('fu_failed_requests') || '[]');
94
- if (!q.length) return;
95
- const remaining = [];
96
- for (const req of q) {
97
- const ok = await trackFn(req.fu_id, req.action, req, false);
98
- if (!ok) remaining.push(req);
99
- }
100
- localStorage.setItem('fu_failed_requests', JSON.stringify(remaining));
101
- } catch (e) { console.error('Retry error:', e); }
102
- }
103
-
104
- /**
105
- * First Users Integration Component
106
- * @param {Object} props
107
- * @param {string} props.appPackageName - Your app's package name (e.g., "com.yourcompany.app")
108
- * @param {string} [props.apiEndpoint] - Custom API endpoint (default: First Users API)
109
- */
110
- function FirstUsersIntegration({ appPackageName = 'your.package.name', apiEndpoint = 'https://cotester-api.01hunterwl.workers.dev' }) {
111
- const [fuId, setFuId] = useState('');
112
- const [isSubmitting, setIsSubmitting] = useState(false);
113
- const [isTracked, setIsTracked] = useState(false);
114
- const [sessionDuration, setSessionDuration] = useState(0);
115
- const timerRef = useRef(null);
116
- const sessionId = React.useMemo(() =>
117
- 'session_' + Math.random().toString(36).substr(2, 9), []
118
- );
119
-
120
- const trackActivity = async (fuIdToTrack, action = 'app_usage', extraData = {}, shouldQueue = true) => {
121
- try {
122
- const device = getDeviceInfo();
123
- const requestData = {
124
- fu_id: fuIdToTrack,
125
- app_package: appPackageName,
126
- timestamp: new Date().toISOString(),
127
- action,
128
- page_url: typeof window !== 'undefined' ? (window.location?.pathname + window.location?.search) : '',
129
- user_agent: device.user_agent,
130
- device_brand: device.device_brand,
131
- device_api_level: device.device_api_level,
132
- session_id: sessionId,
133
- session_duration: FUHeartbeat.getDuration(),
134
- ...extraData
135
- };
136
- const resp = await fetch(`${apiEndpoint}/api/fu/track`, {
137
- method: 'POST',
138
- headers: { 'Content-Type': 'application/json' },
139
- body: JSON.stringify(requestData),
140
- });
141
- const result = await resp.json();
142
- if (result.success) { console.log('FU tracked:', result); return true; }
143
- if (shouldQueue) queueFailedRequest(requestData);
144
- return false;
145
- } catch (e) {
146
- console.error('FU error:', e);
147
- if (shouldQueue) queueFailedRequest({ fu_id: fuIdToTrack, app_package: appPackageName, action, ...extraData });
148
- return false;
149
- }
150
- };
151
-
152
- // On mount: restore tracked state, process offline queue, start heartbeat
153
- useEffect(() => {
154
- const userId = getOrCreateUserId(); // Auto-generate GU if needed
155
- if (!userId) return; // Skip if localStorage unavailable
156
-
157
- setFuId(userId);
158
-
159
- // Determine if this is a real FU or auto-generated GU
160
- const isRealFU = userId.startsWith('fu-');
161
- setIsTracked(isRealFU);
162
-
163
- // Start tracking immediately with either FU or GU
164
- trackActivity(userId, 'page_visit');
165
- FUHeartbeat.start(userId, trackActivity);
166
-
167
- // Process any queued offline requests on startup
168
- processFailedRequests(trackActivity);
169
- // Update duration display every second
170
- timerRef.current = setInterval(() => setSessionDuration(FUHeartbeat.getDuration()), 1000);
171
- return () => {
172
- if (timerRef.current) clearInterval(timerRef.current);
173
- };
174
- }, []);
175
-
176
- // Bind GU to FU when user submits their FU ID
177
- const bindGuestToFU = async (guId, fuId) => {
178
- try {
179
- const resp = await fetch(`${apiEndpoint}/api/fu/bind`, {
180
- method: 'POST',
181
- headers: { 'Content-Type': 'application/json' },
182
- body: JSON.stringify({ gu_id: guId, fu_id: fuId }),
183
- });
184
- const result = await resp.json();
185
- return result.success;
186
- } catch (e) {
187
- console.error('Bind error:', e);
188
- return false;
189
- }
190
- };
191
-
192
- const handleSubmitFuId = async () => {
193
- if (!fuId.startsWith('fu-') || fuId.length !== 8) {
194
- alert('Please enter a valid FU ID (format: fu-XXXXX)');
195
- return;
196
- }
197
- setIsSubmitting(true);
198
-
199
- // Get current ID (might be GU)
200
- const currentId = typeof localStorage !== 'undefined' ? localStorage.getItem('fu_tester_id') : null;
201
- const isGU = currentId && currentId.startsWith('gu-');
202
-
203
- // If upgrading from GU to FU, bind them first
204
- if (isGU) {
205
- const bindSuccess = await bindGuestToFU(currentId, fuId);
206
- if (!bindSuccess) {
207
- alert('Failed to link your previous activity. Please try again.');
208
- setIsSubmitting(false);
209
- return;
210
- }
211
- console.log(`Successfully bound ${currentId} to ${fuId}`);
212
- }
213
-
214
- // Register the FU ID
215
- const ok = await trackActivity(fuId, 'tester_registration');
216
- if (ok) {
217
- alert('Your testing activity is now being tracked with your FU ID.');
218
- if (typeof localStorage !== 'undefined') {
219
- localStorage.setItem('fu_tester_id', fuId);
220
- }
221
- setIsTracked(true);
222
- // Restart heartbeat with new FU ID
223
- FUHeartbeat.stop();
224
- FUHeartbeat.start(fuId, trackActivity);
225
- } else {
226
- alert('Failed to register. Will retry automatically.');
227
- }
228
- setIsSubmitting(false);
229
- };
230
-
231
- // Expose global feature tracker
232
- useEffect(() => {
233
- if (typeof window !== 'undefined') {
234
- window.trackFUFeature = (name) => {
235
- const id = localStorage.getItem('fu_tester_id');
236
- if (id) trackActivity(id, `feature_${name}`);
237
- };
238
- }
239
- }, []);
240
-
241
- const currentId = typeof localStorage !== 'undefined' ? localStorage.getItem('fu_tester_id') : '';
242
- const isGU = currentId && currentId.startsWith('gu-');
243
-
244
- if (isTracked) {
245
- return (
246
- <div style={{ padding:'15px', backgroundColor:'#dcfce7', borderRadius:'8px',
247
- margin:'20px 0', border:'1px solid #bbf7d0' }}>
248
- <h4 style={{ color:'#16a34a', margin:'0 0 5px' }}>First Users Tester Active</h4>
249
- <p style={{ margin:0, fontSize:'14px', color:'#15803d' }}>
250
- Tracking active with FU ID. Session: {Math.floor(sessionDuration/60)}m {sessionDuration%60}s
251
- </p>
252
- </div>
253
- );
254
- }
255
-
256
- if (isGU) {
257
- return (
258
- <div style={{ padding:'20px', backgroundColor:'#fff3cd', borderRadius:'8px',
259
- margin:'20px 0', border:'1px solid #ffc107' }}>
260
- <h3 style={{ color:'#856404', marginTop:0 }}>Anonymous Tracking Active</h3>
261
- <p style={{ fontSize:'14px', color:'#856404', margin:'10px 0' }}>
262
- Currently tracking with Guest ID: <code>{currentId}</code>
263
- </p>
264
- <p style={{ fontSize:'14px', color:'#856404', margin:'10px 0' }}>
265
- Session: {Math.floor(sessionDuration/60)}m {sessionDuration%60}s
266
- </p>
267
- <p style={{ fontSize:'14px', color:'#856404', fontWeight:'bold', margin:'10px 0' }}>
268
- Enter your FU ID to link your activity:
269
- </p>
270
- <input type="text" placeholder="fu-12345" value={fuId}
271
- onChange={(e) => setFuId(e.target.value)}
272
- style={{ padding:'10px', marginRight:'10px', border:'1px solid #ddd',
273
- borderRadius:'4px', minWidth:'120px' }} />
274
- <button onClick={handleSubmitFuId} disabled={isSubmitting}
275
- style={{ padding:'10px 20px', backgroundColor: isSubmitting?'#9ca3af':'#3b82f6',
276
- color:'white', border:'none', borderRadius:'4px',
277
- cursor: isSubmitting?'not-allowed':'pointer' }}>
278
- {isSubmitting ? 'Linking...' : 'Link FU ID'}
279
- </button>
280
- </div>
281
- );
282
- }
283
-
284
- return (
285
- <div style={{ padding:'20px', backgroundColor:'#f5f5f5', borderRadius:'8px',
286
- margin:'20px 0', border:'1px solid #d1d5db' }}>
287
- <h3>First Users Tester</h3>
288
- <p>Enter your FU ID to start tracking:</p>
289
- <input type="text" placeholder="fu-12345" value={fuId}
290
- onChange={(e) => setFuId(e.target.value)}
291
- style={{ padding:'10px', marginRight:'10px', border:'1px solid #ddd',
292
- borderRadius:'4px', minWidth:'120px' }} />
293
- <button onClick={handleSubmitFuId} disabled={isSubmitting}
294
- style={{ padding:'10px 20px', backgroundColor: isSubmitting?'#9ca3af':'#3b82f6',
295
- color:'white', border:'none', borderRadius:'4px',
296
- cursor: isSubmitting?'not-allowed':'pointer' }}>
297
- {isSubmitting ? 'Registering...' : 'Start Tracking'}
298
- </button>
299
- </div>
300
- );
301
- }
302
-
303
- export default FirstUsersIntegration;
304
- export { FUHeartbeat, generateGuestId, getOrCreateUserId, getDeviceInfo };
package/index.d.ts DELETED
@@ -1,59 +0,0 @@
1
- import React from 'react';
2
-
3
- export interface FirstUsersIntegrationProps {
4
- /**
5
- * Your application's package name (e.g., "com.yourcompany.app")
6
- */
7
- appPackageName: string;
8
-
9
- /**
10
- * Custom API endpoint (optional, defaults to First Users API)
11
- */
12
- apiEndpoint?: string;
13
- }
14
-
15
- /**
16
- * First Users Integration Component
17
- * Provides automatic tester tracking with heartbeat monitoring, offline retry,
18
- * device fingerprinting, and guest user auto-tracking.
19
- */
20
- declare const FirstUsersIntegration: React.FC<FirstUsersIntegrationProps>;
21
-
22
- export default FirstUsersIntegration;
23
-
24
- /**
25
- * Global heartbeat manager singleton
26
- */
27
- export const FUHeartbeat: {
28
- start(fuId: string, trackFn: (id: string, action: string, data?: any) => Promise<boolean>): void;
29
- stop(sendEnd?: boolean): void;
30
- getDuration(): number;
31
- };
32
-
33
- /**
34
- * Generate a unique Guest User ID
35
- */
36
- export function generateGuestId(): string;
37
-
38
- /**
39
- * Get existing user ID or create new Guest User ID
40
- */
41
- export function getOrCreateUserId(): string | null;
42
-
43
- /**
44
- * Extract device information from user agent
45
- */
46
- export function getDeviceInfo(): {
47
- device_brand: string;
48
- device_api_level: string;
49
- user_agent: string;
50
- };
51
-
52
- /**
53
- * Global feature tracking function (available after component mount)
54
- */
55
- declare global {
56
- interface Window {
57
- trackFUFeature?: (featureName: string) => void;
58
- }
59
- }