@followgate/js 0.6.0 → 0.8.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/README.md CHANGED
@@ -10,141 +10,378 @@ FollowGate is inspired by [Hypeddit](https://hypeddit.com) (for musicians), but
10
10
  npm install @followgate/js
11
11
  ```
12
12
 
13
- ## Quick Start
13
+ ## Quick Start (Built-in Modal)
14
+
15
+ Just 10 lines of code - no custom UI needed:
14
16
 
15
17
  ```typescript
16
18
  import { FollowGate } from '@followgate/js';
17
19
 
18
- // Initialize with your API credentials (get them at https://app.followgate.io)
20
+ // Initialize once (e.g., in your app's entry point)
19
21
  FollowGate.init({
20
22
  appId: 'your-app-id',
21
23
  apiKey: 'fg_live_xxx',
24
+ twitter: {
25
+ handle: 'your_twitter_handle',
26
+ tweetId: '1234567890', // Optional: require repost
27
+ },
28
+ onComplete: () => {
29
+ // User completed all actions - redirect to your app
30
+ router.push('/dashboard');
31
+ },
22
32
  });
23
33
 
24
- // Require a Twitter follow before granting access
25
- FollowGate.open({
26
- platform: 'twitter',
27
- action: 'follow',
28
- target: 'yourusername',
29
- userId: 'user-123', // Optional: your app's user ID for tracking
30
- });
34
+ // Show the modal (that's it!)
35
+ FollowGate.show();
31
36
  ```
32
37
 
33
- ## Supported Platforms
38
+ The SDK renders a beautiful, fully-functional modal with:
34
39
 
35
- | Platform | Actions |
36
- | --------- | -------------------------- |
37
- | Twitter/X | `follow`, `repost`, `like` |
38
- | Bluesky | `follow`, `repost`, `like` |
39
- | LinkedIn | `follow` |
40
+ - Username input step
41
+ - Follow action with intent URL
42
+ - Repost action (optional)
43
+ - Confirmation step
44
+ - All styling included (no CSS needed)
40
45
 
41
- ## API Reference
46
+ ## When to Show the Modal
42
47
 
43
- ### `FollowGate.init(config)`
48
+ ```typescript
49
+ // After sign-up (recommended)
50
+ const handleSignUpSuccess = () => {
51
+ FollowGate.show();
52
+ };
53
+
54
+ // Or check if already unlocked
55
+ if (!FollowGate.isUnlocked()) {
56
+ FollowGate.show();
57
+ }
58
+ ```
44
59
 
45
- Initialize the SDK with your credentials.
60
+ ## Handling Success (Important!)
61
+
62
+ **The `onComplete` callback is called ONLY after the user successfully completes all required actions.** This is the right place to:
63
+
64
+ - Create the user account in your database
65
+ - Grant access to premium features
66
+ - Redirect to your app
46
67
 
47
68
  ```typescript
48
69
  FollowGate.init({
49
- appId: 'your-app-id', // Required: Your app ID from dashboard
50
- apiKey: 'fg_live_xxx', // Required: Your API key
51
- apiUrl: 'https://...', // Optional: Custom API URL
52
- debug: false, // Optional: Enable debug logging
70
+ appId: 'your-app-id',
71
+ apiKey: 'fg_live_xxx',
72
+ twitter: { handle: 'your_handle' },
73
+
74
+ onComplete: async () => {
75
+ // ✅ User completed all steps - NOW create the account!
76
+ const user = FollowGate.getUser();
77
+
78
+ await fetch('/api/create-account', {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({
82
+ xUsername: user?.username, // Their X/Twitter username
83
+ platform: user?.platform, // 'twitter'
84
+ }),
85
+ });
86
+
87
+ // Then redirect to your app
88
+ router.push('/dashboard');
89
+ },
53
90
  });
54
91
  ```
55
92
 
56
- ### `FollowGate.open(options)`
93
+ **Why this matters:** Don't create accounts before `onComplete` is called. Users might close the modal without completing the actions. Only when `onComplete` fires, you know they actually followed/reposted.
94
+
95
+ ### Event Listeners for Fine-Grained Control
96
+
97
+ ```typescript
98
+ // Fired when a SINGLE action is completed (follow OR repost)
99
+ FollowGate.on('complete', (data) => {
100
+ console.log('Action completed:', data);
101
+ // { platform: 'twitter', action: 'follow', target: 'your_handle', username: 'their_username' }
102
+ });
103
+
104
+ // Fired when ALL actions are completed (same timing as onComplete)
105
+ FollowGate.on('unlocked', (data) => {
106
+ console.log('Gate unlocked:', data);
107
+ // { username: 'their_username', actions: [{ platform, action, target }, ...] }
108
+ });
109
+ ```
57
110
 
58
- Open a social action popup/intent.
111
+ ## Configuration Options
59
112
 
60
113
  ```typescript
61
- await FollowGate.open({
62
- platform: 'twitter', // 'twitter' | 'bluesky' | 'linkedin'
63
- action: 'follow', // 'follow' | 'repost' | 'like'
64
- target: 'username', // Username or post ID
65
- userId: 'user-123', // Optional: Your app's user ID
114
+ FollowGate.init({
115
+ // Required
116
+ appId: 'your-app-id', // Your App ID from the dashboard
117
+ apiKey: 'fg_live_xxx', // Your API key (starts with fg_live_ or fg_test_)
118
+
119
+ // Twitter/X Configuration
120
+ twitter: {
121
+ handle: 'your_handle', // Your Twitter username (without @)
122
+ tweetId: '1234567890', // Tweet ID to repost (optional)
123
+ },
124
+
125
+ // Callback when user completes all actions
126
+ onComplete: () => {
127
+ // Called after the user finishes all steps and clicks "Got it"
128
+ // Typically used to redirect to your app
129
+ router.push('/dashboard');
130
+ },
131
+
132
+ // Optional
133
+ userId: 'clerk_user_123', // User ID for per-user unlock status (recommended with auth)
134
+ debug: false, // Enable console logging for debugging
135
+ accentColor: '#6366f1', // Customize primary button color (hex)
136
+ theme: 'dark', // 'dark' | 'light' - modal appearance
137
+ apiUrl: 'https://...', // Custom API URL (for self-hosted)
66
138
  });
67
139
  ```
68
140
 
69
- ### `FollowGate.verify(options)`
141
+ ### Handle & Target Formats
142
+
143
+ The SDK automatically normalizes usernames - you can use `@username` or `username`, both work:
70
144
 
71
- Verify if a user completed the action (Pro/Business tiers with OAuth).
145
+ | Platform | Action | Target Format | Example |
146
+ | ------------ | ------ | ----------------------------------- | ------------------------------------ |
147
+ | **Twitter** | follow | Username (without @) | `lukasvanuden` |
148
+ | **Twitter** | repost | Tweet ID (numbers only) | `1234567890123456789` |
149
+ | **Twitter** | like | Tweet ID (numbers only) | `1234567890123456789` |
150
+ | **Bluesky** | follow | Handle (with or without @) | `user.bsky.social` |
151
+ | **Bluesky** | repost | AT URI or post path | `user.bsky.social/post/xyz` |
152
+ | **LinkedIn** | follow | Company name or prefixed identifier | `company:anthropic` or `in:username` |
153
+
154
+ **Tip:** For Twitter, you can find the Tweet ID in the URL: `https://x.com/user/status/1234567890123456789`
155
+
156
+ ## API Reference
157
+
158
+ ### Modal Methods
72
159
 
73
160
  ```typescript
74
- const isVerified = await FollowGate.verify({
161
+ // Show the FollowGate modal
162
+ // If user is already unlocked, calls onComplete immediately
163
+ FollowGate.show();
164
+
165
+ // Hide the modal programmatically
166
+ FollowGate.hide();
167
+
168
+ // Check if user has completed all actions
169
+ FollowGate.isUnlocked(); // true | false
170
+
171
+ // Reset user's session (for testing)
172
+ FollowGate.reset();
173
+ ```
174
+
175
+ ### Advanced Usage
176
+
177
+ For custom UI implementations (when you don't want to use the built-in modal):
178
+
179
+ ```typescript
180
+ // Set username manually (@ is automatically removed)
181
+ FollowGate.setUsername('your_username'); // or '@your_username' - both work!
182
+
183
+ // Check if username is set
184
+ FollowGate.hasUsername(); // true | false
185
+
186
+ // Open intent URLs directly (opens in new window)
187
+ // For follow: target = username (without @)
188
+ await FollowGate.openIntent({
189
+ platform: 'twitter',
190
+ action: 'follow',
191
+ target: 'lukasvanuden', // NOT @lukasvanuden
192
+ });
193
+
194
+ // For repost: target = tweet ID
195
+ await FollowGate.openIntent({
196
+ platform: 'twitter',
197
+ action: 'repost',
198
+ target: '1234567890123456789', // Tweet ID from URL
199
+ });
200
+
201
+ // Mark action as completed (tracks locally + sends to API)
202
+ await FollowGate.complete({
75
203
  platform: 'twitter',
76
204
  action: 'follow',
77
- target: 'yourusername',
78
- userId: 'user-123',
205
+ target: 'lukasvanuden',
79
206
  });
207
+
208
+ // Mark gate as unlocked (user has completed all required actions)
209
+ await FollowGate.unlock();
210
+
211
+ // Get current user info
212
+ FollowGate.getUser();
213
+ // Returns: { username: 'their_username', platform: 'twitter' } | null
214
+
215
+ // Get completed actions
216
+ FollowGate.getCompletedActions();
217
+ // Returns: [{ platform: 'twitter', action: 'follow', target: 'lukasvanuden' }, ...]
218
+
219
+ // Get full unlock status
220
+ FollowGate.getUnlockStatus();
221
+ // Returns: {
222
+ // unlocked: boolean,
223
+ // username?: string,
224
+ // completedActions?: CompleteOptions[]
225
+ // }
80
226
  ```
81
227
 
82
- ### `FollowGate.on(event, callback)`
228
+ ### Server-Side Verification
229
+
230
+ When `userId` is set, completions are automatically saved to the server. You can verify users from your backend:
231
+
232
+ ```typescript
233
+ // Check if user is verified (server-side)
234
+ const isVerified = await FollowGate.isVerified();
235
+ // Returns: true | false
236
+
237
+ // Get full verification status
238
+ const status = await FollowGate.getVerificationStatus();
239
+ // Returns: {
240
+ // verified: boolean,
241
+ // userId: string,
242
+ // completedAt?: string, // ISO date string
243
+ // actions?: [{ action: 'follow', target: 'lukasvanuden' }, ...]
244
+ // }
245
+ ```
83
246
 
84
- Listen for events.
247
+ **Use Case: Conditional Features**
248
+
249
+ ```typescript
250
+ // In your app (e.g., Chrome extension, Electron app)
251
+ FollowGate.init({
252
+ appId: 'your-app-id',
253
+ apiKey: 'fg_live_xxx',
254
+ userId: clerkUserId,
255
+ twitter: { handle: 'lukasvanuden' },
256
+ });
257
+
258
+ const isVerified = await FollowGate.isVerified();
259
+
260
+ if (isVerified) {
261
+ // User completed FollowGate → Full access
262
+ enableAllFeatures();
263
+ } else {
264
+ // User hasn't completed → Limited access
265
+ enableTrialMode();
266
+ }
267
+ ```
268
+
269
+ ### Event Listeners
85
270
 
86
271
  ```typescript
87
272
  FollowGate.on('complete', (data) => {
88
- console.log('User completed action:', data);
89
- // Grant access to your app
273
+ console.log('Action completed:', data);
274
+ });
275
+
276
+ FollowGate.on('unlocked', (data) => {
277
+ console.log('Gate unlocked:', data);
90
278
  });
91
279
 
92
280
  FollowGate.on('error', (error) => {
93
281
  console.error('Error:', error);
94
282
  });
283
+
284
+ // Remove listener
285
+ FollowGate.off('complete', handler);
95
286
  ```
96
287
 
97
- ## Platform-Specific Notes
288
+ ## Supported Platforms
98
289
 
99
- ### Twitter/X
290
+ | Platform | Actions |
291
+ | --------- | -------------------------- |
292
+ | Twitter/X | `follow`, `repost`, `like` |
293
+ | Bluesky | `follow`, `repost`, `like` |
294
+ | LinkedIn | `follow` |
295
+
296
+ ## Framework Examples
100
297
 
101
- - `target` for `follow`: Twitter username (without @)
102
- - `target` for `repost`/`like`: Tweet ID
298
+ ### Next.js (App Router)
103
299
 
104
300
  ```typescript
105
- // Follow
106
- FollowGate.open({ platform: 'twitter', action: 'follow', target: 'elonmusk' });
301
+ // app/welcome/page.tsx
302
+ 'use client';
107
303
 
108
- // Repost a tweet
109
- FollowGate.open({
110
- platform: 'twitter',
111
- action: 'repost',
112
- target: '1234567890',
113
- });
304
+ import { FollowGate } from '@followgate/js';
305
+ import { useRouter } from 'next/navigation';
306
+ import { useEffect } from 'react';
307
+
308
+ export default function WelcomePage() {
309
+ const router = useRouter();
310
+
311
+ useEffect(() => {
312
+ FollowGate.init({
313
+ appId: process.env.NEXT_PUBLIC_FOLLOWGATE_APP_ID!,
314
+ apiKey: process.env.NEXT_PUBLIC_FOLLOWGATE_API_KEY!,
315
+ twitter: { handle: 'your_handle' },
316
+ onComplete: () => router.push('/dashboard'),
317
+ });
318
+
319
+ // Show modal if not already unlocked
320
+ if (!FollowGate.isUnlocked()) {
321
+ FollowGate.show();
322
+ } else {
323
+ router.push('/dashboard');
324
+ }
325
+ }, [router]);
326
+
327
+ return <div>Loading...</div>;
328
+ }
114
329
  ```
115
330
 
116
- ### Bluesky
117
-
118
- - `target` for `follow`: Bluesky handle (e.g., `alice.bsky.social`)
119
- - `target` for `repost`/`like`: Post path (e.g., `alice.bsky.social/post/xxx`)
331
+ ### With Clerk Auth
120
332
 
121
333
  ```typescript
122
- // Follow
123
- FollowGate.open({
124
- platform: 'bluesky',
125
- action: 'follow',
126
- target: 'alice.bsky.social',
127
- });
334
+ // app/welcome/page.tsx
335
+ 'use client';
336
+
337
+ import { FollowGate } from '@followgate/js';
338
+ import { useRouter } from 'next/navigation';
339
+ import { useUser } from '@clerk/nextjs';
340
+ import { useEffect } from 'react';
341
+
342
+ export default function WelcomePage() {
343
+ const router = useRouter();
344
+ const { user } = useUser();
345
+
346
+ useEffect(() => {
347
+ if (!user) return;
348
+
349
+ FollowGate.init({
350
+ appId: process.env.NEXT_PUBLIC_FOLLOWGATE_APP_ID!,
351
+ apiKey: process.env.NEXT_PUBLIC_FOLLOWGATE_API_KEY!,
352
+ userId: user.id, // Per-user unlock status (recommended!)
353
+ twitter: { handle: 'lukasvanuden' },
354
+ onComplete: () => router.push('/dashboard'),
355
+ });
356
+
357
+ if (!FollowGate.isUnlocked()) {
358
+ FollowGate.show();
359
+ } else {
360
+ router.push('/dashboard');
361
+ }
362
+ }, [user, router]);
363
+
364
+ return <div>Loading...</div>;
365
+ }
128
366
  ```
129
367
 
130
- ### LinkedIn
368
+ **Why use `userId`?** Without it, unlock status is stored per-browser. If User A completes the gate on a shared computer, User B would also be "unlocked". With `userId`, each user has their own unlock status.
369
+
370
+ ## TypeScript
131
371
 
132
- - `target` for `follow`: Company name or `in:username` for personal profiles
372
+ Full TypeScript support included:
133
373
 
134
374
  ```typescript
135
- // Follow a company
136
- FollowGate.open({
137
- platform: 'linkedin',
138
- action: 'follow',
139
- target: 'microsoft',
140
- });
141
-
142
- // Follow a personal profile
143
- FollowGate.open({
144
- platform: 'linkedin',
145
- action: 'follow',
146
- target: 'in:satyanadella',
147
- });
375
+ import type {
376
+ Platform,
377
+ SocialAction,
378
+ FollowGateConfig,
379
+ TwitterConfig,
380
+ CompleteOptions,
381
+ UserInfo,
382
+ UnlockStatus,
383
+ VerificationStatus,
384
+ } from '@followgate/js';
148
385
  ```
149
386
 
150
387
  ## Pricing
@@ -156,19 +393,12 @@ FollowGate.open({
156
393
  | Pro | $49/mo | 2,000 | OAuth verification |
157
394
  | Business | $99/mo | 5,000+ | Daily verification |
158
395
 
159
- ## TypeScript
160
-
161
- Full TypeScript support included. Types are exported:
162
-
163
- ```typescript
164
- import type { Platform, SocialAction, FollowGateConfig } from '@followgate/js';
165
- ```
166
-
167
396
  ## Links
168
397
 
169
- - [Dashboard](https://app.followgate.io)
170
- - [Documentation](https://followgate.io/docs)
398
+ - [Dashboard](https://followgate.app)
399
+ - [Documentation](https://docs.followgate.app)
171
400
  - [GitHub](https://github.com/JustFF5/FollowGate)
401
+ - [Live Demo](https://follow-gate-web-demo.vercel.app)
172
402
 
173
403
  ## License
174
404
 
package/dist/index.d.mts CHANGED
@@ -21,6 +21,7 @@ interface FollowGateConfig {
21
21
  apiKey: string;
22
22
  apiUrl?: string;
23
23
  debug?: boolean;
24
+ userId?: string;
24
25
  twitter?: TwitterConfig;
25
26
  onComplete?: () => void;
26
27
  theme?: 'dark' | 'light';
@@ -65,6 +66,18 @@ interface UnlockStatus {
65
66
  username?: string;
66
67
  completedActions?: CompleteOptions[];
67
68
  }
69
+ /**
70
+ * Server-side verification status
71
+ */
72
+ interface VerificationStatus {
73
+ verified: boolean;
74
+ userId: string;
75
+ completedAt?: string;
76
+ actions?: Array<{
77
+ action: string;
78
+ target: string;
79
+ }>;
80
+ }
68
81
  /**
69
82
  * FollowGate SDK Client
70
83
  */
@@ -129,10 +142,32 @@ declare class FollowGateClient {
129
142
  isUnlocked(): boolean;
130
143
  getUnlockStatus(): UnlockStatus;
131
144
  getCompletedActions(): CompleteOptions[];
145
+ /**
146
+ * Check if user is verified server-side
147
+ * Requires userId to be set in config
148
+ * @returns Promise<boolean> - true if verified, false otherwise
149
+ */
150
+ isVerified(): Promise<boolean>;
151
+ /**
152
+ * Get full verification status from server
153
+ * Requires userId to be set in config
154
+ * @returns Promise<VerificationStatus> - full verification details
155
+ */
156
+ getVerificationStatus(): Promise<VerificationStatus>;
132
157
  on(event: EventType, callback: EventCallback): void;
133
158
  off(event: EventType, callback: EventCallback): void;
159
+ /**
160
+ * Get storage key with optional userId suffix
161
+ * This ensures unlock status is per-user, not per-browser
162
+ */
163
+ private getStorageKey;
134
164
  private restoreSession;
135
165
  private saveCompletedActions;
166
+ /**
167
+ * Save completion to server for verification
168
+ * Only works if userId is set in config
169
+ */
170
+ private saveCompletion;
136
171
  private buildIntentUrl;
137
172
  private buildTwitterUrl;
138
173
  private buildBlueskyUrl;
@@ -142,4 +177,4 @@ declare class FollowGateClient {
142
177
  }
143
178
  declare const FollowGate: FollowGateClient;
144
179
 
145
- export { type CompleteOptions, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type Platform, type SocialAction, type TwitterConfig, type UnlockStatus, type UserInfo };
180
+ export { type CompleteOptions, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type Platform, type SocialAction, type TwitterConfig, type UnlockStatus, type UserInfo, type VerificationStatus };
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ interface FollowGateConfig {
21
21
  apiKey: string;
22
22
  apiUrl?: string;
23
23
  debug?: boolean;
24
+ userId?: string;
24
25
  twitter?: TwitterConfig;
25
26
  onComplete?: () => void;
26
27
  theme?: 'dark' | 'light';
@@ -65,6 +66,18 @@ interface UnlockStatus {
65
66
  username?: string;
66
67
  completedActions?: CompleteOptions[];
67
68
  }
69
+ /**
70
+ * Server-side verification status
71
+ */
72
+ interface VerificationStatus {
73
+ verified: boolean;
74
+ userId: string;
75
+ completedAt?: string;
76
+ actions?: Array<{
77
+ action: string;
78
+ target: string;
79
+ }>;
80
+ }
68
81
  /**
69
82
  * FollowGate SDK Client
70
83
  */
@@ -129,10 +142,32 @@ declare class FollowGateClient {
129
142
  isUnlocked(): boolean;
130
143
  getUnlockStatus(): UnlockStatus;
131
144
  getCompletedActions(): CompleteOptions[];
145
+ /**
146
+ * Check if user is verified server-side
147
+ * Requires userId to be set in config
148
+ * @returns Promise<boolean> - true if verified, false otherwise
149
+ */
150
+ isVerified(): Promise<boolean>;
151
+ /**
152
+ * Get full verification status from server
153
+ * Requires userId to be set in config
154
+ * @returns Promise<VerificationStatus> - full verification details
155
+ */
156
+ getVerificationStatus(): Promise<VerificationStatus>;
132
157
  on(event: EventType, callback: EventCallback): void;
133
158
  off(event: EventType, callback: EventCallback): void;
159
+ /**
160
+ * Get storage key with optional userId suffix
161
+ * This ensures unlock status is per-user, not per-browser
162
+ */
163
+ private getStorageKey;
134
164
  private restoreSession;
135
165
  private saveCompletedActions;
166
+ /**
167
+ * Save completion to server for verification
168
+ * Only works if userId is set in config
169
+ */
170
+ private saveCompletion;
136
171
  private buildIntentUrl;
137
172
  private buildTwitterUrl;
138
173
  private buildBlueskyUrl;
@@ -142,4 +177,4 @@ declare class FollowGateClient {
142
177
  }
143
178
  declare const FollowGate: FollowGateClient;
144
179
 
145
- export { type CompleteOptions, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type Platform, type SocialAction, type TwitterConfig, type UnlockStatus, type UserInfo };
180
+ export { type CompleteOptions, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type Platform, type SocialAction, type TwitterConfig, type UnlockStatus, type UserInfo, type VerificationStatus };
package/dist/index.js CHANGED
@@ -507,8 +507,14 @@ var FollowGateClient = class {
507
507
  style.textContent = MODAL_STYLES;
508
508
  document.head.appendChild(style);
509
509
  if (this.config?.accentColor) {
510
- document.documentElement.style.setProperty("--fg-accent", this.config.accentColor);
511
- document.documentElement.style.setProperty("--fg-accent-hover", this.config.accentColor);
510
+ document.documentElement.style.setProperty(
511
+ "--fg-accent",
512
+ this.config.accentColor
513
+ );
514
+ document.documentElement.style.setProperty(
515
+ "--fg-accent-hover",
516
+ this.config.accentColor
517
+ );
512
518
  }
513
519
  this.stylesInjected = true;
514
520
  }
@@ -556,8 +562,12 @@ var FollowGateClient = class {
556
562
  </button>
557
563
  <p class="fg-hint">No login required \u2013 just your username</p>
558
564
  `;
559
- const input = document.getElementById("fg-username-input");
560
- const btn = document.getElementById("fg-username-submit");
565
+ const input = document.getElementById(
566
+ "fg-username-input"
567
+ );
568
+ const btn = document.getElementById(
569
+ "fg-username-submit"
570
+ );
561
571
  input?.addEventListener("input", () => {
562
572
  btn.disabled = !input.value.trim();
563
573
  });
@@ -641,7 +651,9 @@ var FollowGateClient = class {
641
651
  Open again
642
652
  </button>
643
653
  `;
644
- const confirmBtn = document.getElementById("fg-follow-confirm");
654
+ const confirmBtn = document.getElementById(
655
+ "fg-follow-confirm"
656
+ );
645
657
  const retryBtn = document.getElementById("fg-follow-retry");
646
658
  const interval = setInterval(() => {
647
659
  seconds--;
@@ -736,7 +748,9 @@ var FollowGateClient = class {
736
748
  Open tweet again
737
749
  </button>
738
750
  `;
739
- const confirmBtn = document.getElementById("fg-repost-confirm");
751
+ const confirmBtn = document.getElementById(
752
+ "fg-repost-confirm"
753
+ );
740
754
  const retryBtn = document.getElementById("fg-repost-retry");
741
755
  const interval = setInterval(() => {
742
756
  seconds--;
@@ -851,7 +865,10 @@ var FollowGateClient = class {
851
865
  platform
852
866
  };
853
867
  if (typeof localStorage !== "undefined") {
854
- localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
868
+ localStorage.setItem(
869
+ this.getStorageKey("followgate_user"),
870
+ JSON.stringify(this.currentUser)
871
+ );
855
872
  }
856
873
  if (this.config.debug) {
857
874
  console.log("[FollowGate] Username set:", normalizedUsername);
@@ -876,9 +893,9 @@ var FollowGateClient = class {
876
893
  this.currentUser = null;
877
894
  this.completedActions = [];
878
895
  if (typeof localStorage !== "undefined") {
879
- localStorage.removeItem("followgate_user");
880
- localStorage.removeItem("followgate_actions");
881
- localStorage.removeItem("followgate_unlocked");
896
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
897
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
898
+ localStorage.removeItem(this.getStorageKey("followgate_unlocked"));
882
899
  }
883
900
  if (this.config?.debug) {
884
901
  console.log("[FollowGate] Session reset");
@@ -915,7 +932,9 @@ var FollowGateClient = class {
915
932
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
916
933
  }
917
934
  if (!this.currentUser) {
918
- throw new Error("[FollowGate] No username set. Call setUsername() first.");
935
+ throw new Error(
936
+ "[FollowGate] No username set. Call setUsername() first."
937
+ );
919
938
  }
920
939
  const alreadyCompleted = this.completedActions.some(
921
940
  (a) => a.platform === options.platform && a.action === options.action && a.target === options.target
@@ -941,8 +960,9 @@ var FollowGateClient = class {
941
960
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
942
961
  }
943
962
  if (typeof localStorage !== "undefined") {
944
- localStorage.setItem("followgate_unlocked", "true");
963
+ localStorage.setItem(this.getStorageKey("followgate_unlocked"), "true");
945
964
  }
965
+ await this.saveCompletion();
946
966
  await this.trackEvent("gate_unlocked", {
947
967
  username: this.currentUser?.username,
948
968
  actions: this.completedActions
@@ -957,7 +977,7 @@ var FollowGateClient = class {
957
977
  }
958
978
  isUnlocked() {
959
979
  if (typeof localStorage === "undefined") return false;
960
- return localStorage.getItem("followgate_unlocked") === "true";
980
+ return localStorage.getItem(this.getStorageKey("followgate_unlocked")) === "true";
961
981
  }
962
982
  getUnlockStatus() {
963
983
  return {
@@ -970,6 +990,90 @@ var FollowGateClient = class {
970
990
  return [...this.completedActions];
971
991
  }
972
992
  // ============================================
993
+ // Server-Side Verification
994
+ // ============================================
995
+ /**
996
+ * Check if user is verified server-side
997
+ * Requires userId to be set in config
998
+ * @returns Promise<boolean> - true if verified, false otherwise
999
+ */
1000
+ async isVerified() {
1001
+ if (!this.config) {
1002
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
1003
+ }
1004
+ if (!this.config.userId) {
1005
+ if (this.config.debug) {
1006
+ console.warn("[FollowGate] isVerified() requires userId to be set");
1007
+ }
1008
+ return false;
1009
+ }
1010
+ try {
1011
+ const response = await fetch(
1012
+ `${this.config.apiUrl}/api/v1/completions/verify/${encodeURIComponent(this.config.userId)}`,
1013
+ {
1014
+ headers: {
1015
+ Authorization: `Bearer ${this.config.apiKey}`
1016
+ }
1017
+ }
1018
+ );
1019
+ if (!response.ok) {
1020
+ if (this.config.debug) {
1021
+ console.warn("[FollowGate] Verification check failed:", response.status);
1022
+ }
1023
+ return false;
1024
+ }
1025
+ const data = await response.json();
1026
+ return data.data?.verified === true;
1027
+ } catch (error) {
1028
+ if (this.config.debug) {
1029
+ console.warn("[FollowGate] Verification check error:", error);
1030
+ }
1031
+ return false;
1032
+ }
1033
+ }
1034
+ /**
1035
+ * Get full verification status from server
1036
+ * Requires userId to be set in config
1037
+ * @returns Promise<VerificationStatus> - full verification details
1038
+ */
1039
+ async getVerificationStatus() {
1040
+ if (!this.config) {
1041
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
1042
+ }
1043
+ if (!this.config.userId) {
1044
+ return {
1045
+ verified: false,
1046
+ userId: ""
1047
+ };
1048
+ }
1049
+ try {
1050
+ const response = await fetch(
1051
+ `${this.config.apiUrl}/api/v1/completions/verify/${encodeURIComponent(this.config.userId)}`,
1052
+ {
1053
+ headers: {
1054
+ Authorization: `Bearer ${this.config.apiKey}`
1055
+ }
1056
+ }
1057
+ );
1058
+ if (!response.ok) {
1059
+ return {
1060
+ verified: false,
1061
+ userId: this.config.userId
1062
+ };
1063
+ }
1064
+ const data = await response.json();
1065
+ return data.data;
1066
+ } catch (error) {
1067
+ if (this.config.debug) {
1068
+ console.warn("[FollowGate] getVerificationStatus error:", error);
1069
+ }
1070
+ return {
1071
+ verified: false,
1072
+ userId: this.config.userId
1073
+ };
1074
+ }
1075
+ }
1076
+ // ============================================
973
1077
  // Event System
974
1078
  // ============================================
975
1079
  on(event, callback) {
@@ -984,33 +1088,84 @@ var FollowGateClient = class {
984
1088
  // ============================================
985
1089
  // Private Methods
986
1090
  // ============================================
1091
+ /**
1092
+ * Get storage key with optional userId suffix
1093
+ * This ensures unlock status is per-user, not per-browser
1094
+ */
1095
+ getStorageKey(base) {
1096
+ if (this.config?.userId) {
1097
+ return `${base}_${this.config.userId}`;
1098
+ }
1099
+ return base;
1100
+ }
987
1101
  restoreSession() {
988
1102
  if (typeof localStorage === "undefined") return;
989
- const userJson = localStorage.getItem("followgate_user");
1103
+ const userJson = localStorage.getItem(
1104
+ this.getStorageKey("followgate_user")
1105
+ );
990
1106
  if (userJson) {
991
1107
  try {
992
1108
  this.currentUser = JSON.parse(userJson);
993
1109
  } catch {
994
- localStorage.removeItem("followgate_user");
1110
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
995
1111
  }
996
1112
  }
997
- const actionsJson = localStorage.getItem("followgate_actions");
1113
+ const actionsJson = localStorage.getItem(
1114
+ this.getStorageKey("followgate_actions")
1115
+ );
998
1116
  if (actionsJson) {
999
1117
  try {
1000
1118
  this.completedActions = JSON.parse(actionsJson);
1001
1119
  } catch {
1002
- localStorage.removeItem("followgate_actions");
1120
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
1003
1121
  }
1004
1122
  }
1005
1123
  }
1006
1124
  saveCompletedActions() {
1007
1125
  if (typeof localStorage !== "undefined") {
1008
1126
  localStorage.setItem(
1009
- "followgate_actions",
1127
+ this.getStorageKey("followgate_actions"),
1010
1128
  JSON.stringify(this.completedActions)
1011
1129
  );
1012
1130
  }
1013
1131
  }
1132
+ /**
1133
+ * Save completion to server for verification
1134
+ * Only works if userId is set in config
1135
+ */
1136
+ async saveCompletion() {
1137
+ if (!this.config?.userId) {
1138
+ return;
1139
+ }
1140
+ try {
1141
+ const response = await fetch(`${this.config.apiUrl}/api/v1/completions`, {
1142
+ method: "POST",
1143
+ headers: {
1144
+ "Content-Type": "application/json",
1145
+ Authorization: `Bearer ${this.config.apiKey}`
1146
+ },
1147
+ body: JSON.stringify({
1148
+ userId: this.config.userId,
1149
+ platform: this.currentUser?.platform || "twitter",
1150
+ actions: this.completedActions.map((a) => ({
1151
+ action: a.action,
1152
+ target: a.target
1153
+ }))
1154
+ })
1155
+ });
1156
+ if (this.config.debug) {
1157
+ if (response.ok) {
1158
+ console.log("[FollowGate] Completion saved to server");
1159
+ } else {
1160
+ console.warn("[FollowGate] Failed to save completion:", response.status);
1161
+ }
1162
+ }
1163
+ } catch (error) {
1164
+ if (this.config.debug) {
1165
+ console.warn("[FollowGate] Failed to save completion:", error);
1166
+ }
1167
+ }
1168
+ }
1014
1169
  buildIntentUrl(options) {
1015
1170
  const { platform, action, target } = options;
1016
1171
  switch (platform) {
package/dist/index.mjs CHANGED
@@ -481,8 +481,14 @@ var FollowGateClient = class {
481
481
  style.textContent = MODAL_STYLES;
482
482
  document.head.appendChild(style);
483
483
  if (this.config?.accentColor) {
484
- document.documentElement.style.setProperty("--fg-accent", this.config.accentColor);
485
- document.documentElement.style.setProperty("--fg-accent-hover", this.config.accentColor);
484
+ document.documentElement.style.setProperty(
485
+ "--fg-accent",
486
+ this.config.accentColor
487
+ );
488
+ document.documentElement.style.setProperty(
489
+ "--fg-accent-hover",
490
+ this.config.accentColor
491
+ );
486
492
  }
487
493
  this.stylesInjected = true;
488
494
  }
@@ -530,8 +536,12 @@ var FollowGateClient = class {
530
536
  </button>
531
537
  <p class="fg-hint">No login required \u2013 just your username</p>
532
538
  `;
533
- const input = document.getElementById("fg-username-input");
534
- const btn = document.getElementById("fg-username-submit");
539
+ const input = document.getElementById(
540
+ "fg-username-input"
541
+ );
542
+ const btn = document.getElementById(
543
+ "fg-username-submit"
544
+ );
535
545
  input?.addEventListener("input", () => {
536
546
  btn.disabled = !input.value.trim();
537
547
  });
@@ -615,7 +625,9 @@ var FollowGateClient = class {
615
625
  Open again
616
626
  </button>
617
627
  `;
618
- const confirmBtn = document.getElementById("fg-follow-confirm");
628
+ const confirmBtn = document.getElementById(
629
+ "fg-follow-confirm"
630
+ );
619
631
  const retryBtn = document.getElementById("fg-follow-retry");
620
632
  const interval = setInterval(() => {
621
633
  seconds--;
@@ -710,7 +722,9 @@ var FollowGateClient = class {
710
722
  Open tweet again
711
723
  </button>
712
724
  `;
713
- const confirmBtn = document.getElementById("fg-repost-confirm");
725
+ const confirmBtn = document.getElementById(
726
+ "fg-repost-confirm"
727
+ );
714
728
  const retryBtn = document.getElementById("fg-repost-retry");
715
729
  const interval = setInterval(() => {
716
730
  seconds--;
@@ -825,7 +839,10 @@ var FollowGateClient = class {
825
839
  platform
826
840
  };
827
841
  if (typeof localStorage !== "undefined") {
828
- localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
842
+ localStorage.setItem(
843
+ this.getStorageKey("followgate_user"),
844
+ JSON.stringify(this.currentUser)
845
+ );
829
846
  }
830
847
  if (this.config.debug) {
831
848
  console.log("[FollowGate] Username set:", normalizedUsername);
@@ -850,9 +867,9 @@ var FollowGateClient = class {
850
867
  this.currentUser = null;
851
868
  this.completedActions = [];
852
869
  if (typeof localStorage !== "undefined") {
853
- localStorage.removeItem("followgate_user");
854
- localStorage.removeItem("followgate_actions");
855
- localStorage.removeItem("followgate_unlocked");
870
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
871
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
872
+ localStorage.removeItem(this.getStorageKey("followgate_unlocked"));
856
873
  }
857
874
  if (this.config?.debug) {
858
875
  console.log("[FollowGate] Session reset");
@@ -889,7 +906,9 @@ var FollowGateClient = class {
889
906
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
890
907
  }
891
908
  if (!this.currentUser) {
892
- throw new Error("[FollowGate] No username set. Call setUsername() first.");
909
+ throw new Error(
910
+ "[FollowGate] No username set. Call setUsername() first."
911
+ );
893
912
  }
894
913
  const alreadyCompleted = this.completedActions.some(
895
914
  (a) => a.platform === options.platform && a.action === options.action && a.target === options.target
@@ -915,8 +934,9 @@ var FollowGateClient = class {
915
934
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
916
935
  }
917
936
  if (typeof localStorage !== "undefined") {
918
- localStorage.setItem("followgate_unlocked", "true");
937
+ localStorage.setItem(this.getStorageKey("followgate_unlocked"), "true");
919
938
  }
939
+ await this.saveCompletion();
920
940
  await this.trackEvent("gate_unlocked", {
921
941
  username: this.currentUser?.username,
922
942
  actions: this.completedActions
@@ -931,7 +951,7 @@ var FollowGateClient = class {
931
951
  }
932
952
  isUnlocked() {
933
953
  if (typeof localStorage === "undefined") return false;
934
- return localStorage.getItem("followgate_unlocked") === "true";
954
+ return localStorage.getItem(this.getStorageKey("followgate_unlocked")) === "true";
935
955
  }
936
956
  getUnlockStatus() {
937
957
  return {
@@ -944,6 +964,90 @@ var FollowGateClient = class {
944
964
  return [...this.completedActions];
945
965
  }
946
966
  // ============================================
967
+ // Server-Side Verification
968
+ // ============================================
969
+ /**
970
+ * Check if user is verified server-side
971
+ * Requires userId to be set in config
972
+ * @returns Promise<boolean> - true if verified, false otherwise
973
+ */
974
+ async isVerified() {
975
+ if (!this.config) {
976
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
977
+ }
978
+ if (!this.config.userId) {
979
+ if (this.config.debug) {
980
+ console.warn("[FollowGate] isVerified() requires userId to be set");
981
+ }
982
+ return false;
983
+ }
984
+ try {
985
+ const response = await fetch(
986
+ `${this.config.apiUrl}/api/v1/completions/verify/${encodeURIComponent(this.config.userId)}`,
987
+ {
988
+ headers: {
989
+ Authorization: `Bearer ${this.config.apiKey}`
990
+ }
991
+ }
992
+ );
993
+ if (!response.ok) {
994
+ if (this.config.debug) {
995
+ console.warn("[FollowGate] Verification check failed:", response.status);
996
+ }
997
+ return false;
998
+ }
999
+ const data = await response.json();
1000
+ return data.data?.verified === true;
1001
+ } catch (error) {
1002
+ if (this.config.debug) {
1003
+ console.warn("[FollowGate] Verification check error:", error);
1004
+ }
1005
+ return false;
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Get full verification status from server
1010
+ * Requires userId to be set in config
1011
+ * @returns Promise<VerificationStatus> - full verification details
1012
+ */
1013
+ async getVerificationStatus() {
1014
+ if (!this.config) {
1015
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
1016
+ }
1017
+ if (!this.config.userId) {
1018
+ return {
1019
+ verified: false,
1020
+ userId: ""
1021
+ };
1022
+ }
1023
+ try {
1024
+ const response = await fetch(
1025
+ `${this.config.apiUrl}/api/v1/completions/verify/${encodeURIComponent(this.config.userId)}`,
1026
+ {
1027
+ headers: {
1028
+ Authorization: `Bearer ${this.config.apiKey}`
1029
+ }
1030
+ }
1031
+ );
1032
+ if (!response.ok) {
1033
+ return {
1034
+ verified: false,
1035
+ userId: this.config.userId
1036
+ };
1037
+ }
1038
+ const data = await response.json();
1039
+ return data.data;
1040
+ } catch (error) {
1041
+ if (this.config.debug) {
1042
+ console.warn("[FollowGate] getVerificationStatus error:", error);
1043
+ }
1044
+ return {
1045
+ verified: false,
1046
+ userId: this.config.userId
1047
+ };
1048
+ }
1049
+ }
1050
+ // ============================================
947
1051
  // Event System
948
1052
  // ============================================
949
1053
  on(event, callback) {
@@ -958,33 +1062,84 @@ var FollowGateClient = class {
958
1062
  // ============================================
959
1063
  // Private Methods
960
1064
  // ============================================
1065
+ /**
1066
+ * Get storage key with optional userId suffix
1067
+ * This ensures unlock status is per-user, not per-browser
1068
+ */
1069
+ getStorageKey(base) {
1070
+ if (this.config?.userId) {
1071
+ return `${base}_${this.config.userId}`;
1072
+ }
1073
+ return base;
1074
+ }
961
1075
  restoreSession() {
962
1076
  if (typeof localStorage === "undefined") return;
963
- const userJson = localStorage.getItem("followgate_user");
1077
+ const userJson = localStorage.getItem(
1078
+ this.getStorageKey("followgate_user")
1079
+ );
964
1080
  if (userJson) {
965
1081
  try {
966
1082
  this.currentUser = JSON.parse(userJson);
967
1083
  } catch {
968
- localStorage.removeItem("followgate_user");
1084
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
969
1085
  }
970
1086
  }
971
- const actionsJson = localStorage.getItem("followgate_actions");
1087
+ const actionsJson = localStorage.getItem(
1088
+ this.getStorageKey("followgate_actions")
1089
+ );
972
1090
  if (actionsJson) {
973
1091
  try {
974
1092
  this.completedActions = JSON.parse(actionsJson);
975
1093
  } catch {
976
- localStorage.removeItem("followgate_actions");
1094
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
977
1095
  }
978
1096
  }
979
1097
  }
980
1098
  saveCompletedActions() {
981
1099
  if (typeof localStorage !== "undefined") {
982
1100
  localStorage.setItem(
983
- "followgate_actions",
1101
+ this.getStorageKey("followgate_actions"),
984
1102
  JSON.stringify(this.completedActions)
985
1103
  );
986
1104
  }
987
1105
  }
1106
+ /**
1107
+ * Save completion to server for verification
1108
+ * Only works if userId is set in config
1109
+ */
1110
+ async saveCompletion() {
1111
+ if (!this.config?.userId) {
1112
+ return;
1113
+ }
1114
+ try {
1115
+ const response = await fetch(`${this.config.apiUrl}/api/v1/completions`, {
1116
+ method: "POST",
1117
+ headers: {
1118
+ "Content-Type": "application/json",
1119
+ Authorization: `Bearer ${this.config.apiKey}`
1120
+ },
1121
+ body: JSON.stringify({
1122
+ userId: this.config.userId,
1123
+ platform: this.currentUser?.platform || "twitter",
1124
+ actions: this.completedActions.map((a) => ({
1125
+ action: a.action,
1126
+ target: a.target
1127
+ }))
1128
+ })
1129
+ });
1130
+ if (this.config.debug) {
1131
+ if (response.ok) {
1132
+ console.log("[FollowGate] Completion saved to server");
1133
+ } else {
1134
+ console.warn("[FollowGate] Failed to save completion:", response.status);
1135
+ }
1136
+ }
1137
+ } catch (error) {
1138
+ if (this.config.debug) {
1139
+ console.warn("[FollowGate] Failed to save completion:", error);
1140
+ }
1141
+ }
1142
+ }
988
1143
  buildIntentUrl(options) {
989
1144
  const { platform, action, target } = options;
990
1145
  switch (platform) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@followgate/js",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "FollowGate SDK - Grow your audience with every download. Require social actions (follow, repost) before users can access your app.",
5
5
  "author": "FollowGate <hello@followgate.app>",
6
6
  "homepage": "https://followgate.app",