firstusers 1.0.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 +21 -0
- package/README.md +185 -0
- package/dist/index.js +304 -0
- package/index.d.ts +59 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# First Users SDK
|
|
2
|
+
|
|
3
|
+
**Automatic tester tracking with heartbeat monitoring, offline retry, device fingerprinting, and guest user auto-tracking.**
|
|
4
|
+
|
|
5
|
+
## Features
|
|
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
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install firstusers
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Import the component
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
import FirstUsersIntegration from 'firstusers';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Add to your app
|
|
30
|
+
|
|
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
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Track feature usage (optional)
|
|
47
|
+
|
|
48
|
+
After the component is mounted, you can track specific features:
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
// Track button clicks
|
|
52
|
+
window.trackFUFeature('button_click');
|
|
53
|
+
|
|
54
|
+
// Track video plays
|
|
55
|
+
window.trackFUFeature('video_play');
|
|
56
|
+
|
|
57
|
+
// Track any custom event
|
|
58
|
+
window.trackFUFeature('premium_upgrade');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## How It Works
|
|
62
|
+
|
|
63
|
+
### Automatic Guest User (GU) Tracking
|
|
64
|
+
|
|
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
|
|
69
|
+
|
|
70
|
+
### FU ID Binding
|
|
71
|
+
|
|
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
|
|
77
|
+
|
|
78
|
+
## API Reference
|
|
79
|
+
|
|
80
|
+
### Component Props
|
|
81
|
+
|
|
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 |
|
|
86
|
+
|
|
87
|
+
### Global Functions
|
|
88
|
+
|
|
89
|
+
#### `window.trackFUFeature(featureName: string)`
|
|
90
|
+
|
|
91
|
+
Track custom feature usage. Available after component mounts.
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
window.trackFUFeature('search_product');
|
|
95
|
+
window.trackFUFeature('checkout_completed');
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Exported Utilities
|
|
99
|
+
|
|
100
|
+
#### `FUHeartbeat`
|
|
101
|
+
|
|
102
|
+
Global heartbeat manager singleton.
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
import { FUHeartbeat } from 'firstusers';
|
|
106
|
+
|
|
107
|
+
// Start heartbeat manually (usually handled by component)
|
|
108
|
+
FUHeartbeat.start('fu-12345', trackFunction);
|
|
109
|
+
|
|
110
|
+
// Get current session duration in seconds
|
|
111
|
+
const duration = FUHeartbeat.getDuration();
|
|
112
|
+
|
|
113
|
+
// Stop heartbeat
|
|
114
|
+
FUHeartbeat.stop();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### `generateGuestId()`
|
|
118
|
+
|
|
119
|
+
Generate a unique Guest User ID.
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
import { generateGuestId } from 'firstusers';
|
|
123
|
+
|
|
124
|
+
const guestId = generateGuestId(); // Returns: "gu-abc123xyz"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### `getOrCreateUserId()`
|
|
128
|
+
|
|
129
|
+
Get existing user ID from localStorage or create new GU ID.
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
import { getOrCreateUserId } from 'firstusers';
|
|
133
|
+
|
|
134
|
+
const userId = getOrCreateUserId(); // Returns: "fu-12345" or "gu-abc123"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### `getDeviceInfo()`
|
|
138
|
+
|
|
139
|
+
Extract device information from user agent.
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
import { getDeviceInfo } from 'firstusers';
|
|
143
|
+
|
|
144
|
+
const device = getDeviceInfo();
|
|
145
|
+
// Returns: {
|
|
146
|
+
// device_brand: "Samsung",
|
|
147
|
+
// device_api_level: "Android 13",
|
|
148
|
+
// user_agent: "Mozilla/5.0..."
|
|
149
|
+
// }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## UI States
|
|
153
|
+
|
|
154
|
+
### 1. Initial State (Rare)
|
|
155
|
+
Gray banner asking user to enter FU ID (only shown if GU auto-generation fails).
|
|
156
|
+
|
|
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
|
|
163
|
+
|
|
164
|
+
### 3. Active Tracking (FU Active)
|
|
165
|
+
Green banner showing:
|
|
166
|
+
- FU ID is active
|
|
167
|
+
- Session duration
|
|
168
|
+
|
|
169
|
+
## Data Privacy
|
|
170
|
+
|
|
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
|
|
175
|
+
|
|
176
|
+
## Support
|
|
177
|
+
|
|
178
|
+
For issues or questions:
|
|
179
|
+
- Email: support@firstusers.com
|
|
180
|
+
- Documentation: https://docs.firstusers.com
|
|
181
|
+
- GitHub: https://github.com/firstusers/firstusers-sdk
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
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
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
15
|
+
"prepublishOnly": "echo 'Publishing firstusers package...' && ls -la dist/"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"firstusers",
|
|
19
|
+
"tracking",
|
|
20
|
+
"analytics",
|
|
21
|
+
"tester",
|
|
22
|
+
"heartbeat",
|
|
23
|
+
"offline",
|
|
24
|
+
"react",
|
|
25
|
+
"guest-user"
|
|
26
|
+
],
|
|
27
|
+
"author": "First Users Team",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/01hunterwl-commits/fucallback.git"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=16.8.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"npm-packlist": "^10.0.3"
|
|
41
|
+
}
|
|
42
|
+
}
|