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 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
+ }