@followgate/js 0.6.0 → 0.7.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,336 @@ 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
+ ```
59
+
60
+ ## Handling Success (Important!)
44
61
 
45
- Initialize the SDK with your credentials.
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.
57
94
 
58
- Open a social action popup/intent.
95
+ ### Event Listeners for Fine-Grained Control
59
96
 
60
97
  ```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
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 }, ...] }
66
108
  });
67
109
  ```
68
110
 
69
- ### `FollowGate.verify(options)`
111
+ ## Configuration Options
70
112
 
71
- Verify if a user completed the action (Pro/Business tiers with OAuth).
113
+ ```typescript
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)
138
+ });
139
+ ```
140
+
141
+ ### Handle & Target Formats
142
+
143
+ The SDK automatically normalizes usernames - you can use `@username` or `username`, both work:
144
+
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({
75
189
  platform: 'twitter',
76
190
  action: 'follow',
77
- target: 'yourusername',
78
- userId: 'user-123',
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
79
199
  });
80
- ```
81
200
 
82
- ### `FollowGate.on(event, callback)`
201
+ // Mark action as completed (tracks locally + sends to API)
202
+ await FollowGate.complete({
203
+ platform: 'twitter',
204
+ action: 'follow',
205
+ target: 'lukasvanuden',
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
+ // }
226
+ ```
83
227
 
84
- Listen for events.
228
+ ### Event Listeners
85
229
 
86
230
  ```typescript
87
231
  FollowGate.on('complete', (data) => {
88
- console.log('User completed action:', data);
89
- // Grant access to your app
232
+ console.log('Action completed:', data);
233
+ });
234
+
235
+ FollowGate.on('unlocked', (data) => {
236
+ console.log('Gate unlocked:', data);
90
237
  });
91
238
 
92
239
  FollowGate.on('error', (error) => {
93
240
  console.error('Error:', error);
94
241
  });
242
+
243
+ // Remove listener
244
+ FollowGate.off('complete', handler);
95
245
  ```
96
246
 
97
- ## Platform-Specific Notes
247
+ ## Supported Platforms
98
248
 
99
- ### Twitter/X
249
+ | Platform | Actions |
250
+ | --------- | -------------------------- |
251
+ | Twitter/X | `follow`, `repost`, `like` |
252
+ | Bluesky | `follow`, `repost`, `like` |
253
+ | LinkedIn | `follow` |
100
254
 
101
- - `target` for `follow`: Twitter username (without @)
102
- - `target` for `repost`/`like`: Tweet ID
255
+ ## Framework Examples
256
+
257
+ ### Next.js (App Router)
103
258
 
104
259
  ```typescript
105
- // Follow
106
- FollowGate.open({ platform: 'twitter', action: 'follow', target: 'elonmusk' });
260
+ // app/welcome/page.tsx
261
+ 'use client';
107
262
 
108
- // Repost a tweet
109
- FollowGate.open({
110
- platform: 'twitter',
111
- action: 'repost',
112
- target: '1234567890',
113
- });
263
+ import { FollowGate } from '@followgate/js';
264
+ import { useRouter } from 'next/navigation';
265
+ import { useEffect } from 'react';
266
+
267
+ export default function WelcomePage() {
268
+ const router = useRouter();
269
+
270
+ useEffect(() => {
271
+ FollowGate.init({
272
+ appId: process.env.NEXT_PUBLIC_FOLLOWGATE_APP_ID!,
273
+ apiKey: process.env.NEXT_PUBLIC_FOLLOWGATE_API_KEY!,
274
+ twitter: { handle: 'your_handle' },
275
+ onComplete: () => router.push('/dashboard'),
276
+ });
277
+
278
+ // Show modal if not already unlocked
279
+ if (!FollowGate.isUnlocked()) {
280
+ FollowGate.show();
281
+ } else {
282
+ router.push('/dashboard');
283
+ }
284
+ }, [router]);
285
+
286
+ return <div>Loading...</div>;
287
+ }
114
288
  ```
115
289
 
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`)
290
+ ### With Clerk Auth
120
291
 
121
292
  ```typescript
122
- // Follow
123
- FollowGate.open({
124
- platform: 'bluesky',
125
- action: 'follow',
126
- target: 'alice.bsky.social',
127
- });
293
+ // app/welcome/page.tsx
294
+ 'use client';
295
+
296
+ import { FollowGate } from '@followgate/js';
297
+ import { useRouter } from 'next/navigation';
298
+ import { useUser } from '@clerk/nextjs';
299
+ import { useEffect } from 'react';
300
+
301
+ export default function WelcomePage() {
302
+ const router = useRouter();
303
+ const { user } = useUser();
304
+
305
+ useEffect(() => {
306
+ if (!user) return;
307
+
308
+ FollowGate.init({
309
+ appId: process.env.NEXT_PUBLIC_FOLLOWGATE_APP_ID!,
310
+ apiKey: process.env.NEXT_PUBLIC_FOLLOWGATE_API_KEY!,
311
+ userId: user.id, // Per-user unlock status (recommended!)
312
+ twitter: { handle: 'lukasvanuden' },
313
+ onComplete: () => router.push('/dashboard'),
314
+ });
315
+
316
+ if (!FollowGate.isUnlocked()) {
317
+ FollowGate.show();
318
+ } else {
319
+ router.push('/dashboard');
320
+ }
321
+ }, [user, router]);
322
+
323
+ return <div>Loading...</div>;
324
+ }
128
325
  ```
129
326
 
130
- ### LinkedIn
327
+ **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.
131
328
 
132
- - `target` for `follow`: Company name or `in:username` for personal profiles
329
+ ## TypeScript
133
330
 
134
- ```typescript
135
- // Follow a company
136
- FollowGate.open({
137
- platform: 'linkedin',
138
- action: 'follow',
139
- target: 'microsoft',
140
- });
331
+ Full TypeScript support included:
141
332
 
142
- // Follow a personal profile
143
- FollowGate.open({
144
- platform: 'linkedin',
145
- action: 'follow',
146
- target: 'in:satyanadella',
147
- });
333
+ ```typescript
334
+ import type {
335
+ Platform,
336
+ SocialAction,
337
+ FollowGateConfig,
338
+ TwitterConfig,
339
+ CompleteOptions,
340
+ UserInfo,
341
+ UnlockStatus,
342
+ } from '@followgate/js';
148
343
  ```
149
344
 
150
345
  ## Pricing
@@ -156,19 +351,12 @@ FollowGate.open({
156
351
  | Pro | $49/mo | 2,000 | OAuth verification |
157
352
  | Business | $99/mo | 5,000+ | Daily verification |
158
353
 
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
354
  ## Links
168
355
 
169
- - [Dashboard](https://app.followgate.io)
170
- - [Documentation](https://followgate.io/docs)
356
+ - [Dashboard](https://followgate.app)
357
+ - [Documentation](https://docs.followgate.app)
171
358
  - [GitHub](https://github.com/JustFF5/FollowGate)
359
+ - [Live Demo](https://follow-gate-web-demo.vercel.app)
172
360
 
173
361
  ## License
174
362
 
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';
@@ -131,6 +132,11 @@ declare class FollowGateClient {
131
132
  getCompletedActions(): CompleteOptions[];
132
133
  on(event: EventType, callback: EventCallback): void;
133
134
  off(event: EventType, callback: EventCallback): void;
135
+ /**
136
+ * Get storage key with optional userId suffix
137
+ * This ensures unlock status is per-user, not per-browser
138
+ */
139
+ private getStorageKey;
134
140
  private restoreSession;
135
141
  private saveCompletedActions;
136
142
  private buildIntentUrl;
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';
@@ -131,6 +132,11 @@ declare class FollowGateClient {
131
132
  getCompletedActions(): CompleteOptions[];
132
133
  on(event: EventType, callback: EventCallback): void;
133
134
  off(event: EventType, callback: EventCallback): void;
135
+ /**
136
+ * Get storage key with optional userId suffix
137
+ * This ensures unlock status is per-user, not per-browser
138
+ */
139
+ private getStorageKey;
134
140
  private restoreSession;
135
141
  private saveCompletedActions;
136
142
  private buildIntentUrl;
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,7 @@ 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(this.getStorageKey("followgate_user"), JSON.stringify(this.currentUser));
855
869
  }
856
870
  if (this.config.debug) {
857
871
  console.log("[FollowGate] Username set:", normalizedUsername);
@@ -876,9 +890,9 @@ var FollowGateClient = class {
876
890
  this.currentUser = null;
877
891
  this.completedActions = [];
878
892
  if (typeof localStorage !== "undefined") {
879
- localStorage.removeItem("followgate_user");
880
- localStorage.removeItem("followgate_actions");
881
- localStorage.removeItem("followgate_unlocked");
893
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
894
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
895
+ localStorage.removeItem(this.getStorageKey("followgate_unlocked"));
882
896
  }
883
897
  if (this.config?.debug) {
884
898
  console.log("[FollowGate] Session reset");
@@ -915,7 +929,9 @@ var FollowGateClient = class {
915
929
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
916
930
  }
917
931
  if (!this.currentUser) {
918
- throw new Error("[FollowGate] No username set. Call setUsername() first.");
932
+ throw new Error(
933
+ "[FollowGate] No username set. Call setUsername() first."
934
+ );
919
935
  }
920
936
  const alreadyCompleted = this.completedActions.some(
921
937
  (a) => a.platform === options.platform && a.action === options.action && a.target === options.target
@@ -941,7 +957,7 @@ var FollowGateClient = class {
941
957
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
942
958
  }
943
959
  if (typeof localStorage !== "undefined") {
944
- localStorage.setItem("followgate_unlocked", "true");
960
+ localStorage.setItem(this.getStorageKey("followgate_unlocked"), "true");
945
961
  }
946
962
  await this.trackEvent("gate_unlocked", {
947
963
  username: this.currentUser?.username,
@@ -957,7 +973,7 @@ var FollowGateClient = class {
957
973
  }
958
974
  isUnlocked() {
959
975
  if (typeof localStorage === "undefined") return false;
960
- return localStorage.getItem("followgate_unlocked") === "true";
976
+ return localStorage.getItem(this.getStorageKey("followgate_unlocked")) === "true";
961
977
  }
962
978
  getUnlockStatus() {
963
979
  return {
@@ -984,29 +1000,39 @@ var FollowGateClient = class {
984
1000
  // ============================================
985
1001
  // Private Methods
986
1002
  // ============================================
1003
+ /**
1004
+ * Get storage key with optional userId suffix
1005
+ * This ensures unlock status is per-user, not per-browser
1006
+ */
1007
+ getStorageKey(base) {
1008
+ if (this.config?.userId) {
1009
+ return `${base}_${this.config.userId}`;
1010
+ }
1011
+ return base;
1012
+ }
987
1013
  restoreSession() {
988
1014
  if (typeof localStorage === "undefined") return;
989
- const userJson = localStorage.getItem("followgate_user");
1015
+ const userJson = localStorage.getItem(this.getStorageKey("followgate_user"));
990
1016
  if (userJson) {
991
1017
  try {
992
1018
  this.currentUser = JSON.parse(userJson);
993
1019
  } catch {
994
- localStorage.removeItem("followgate_user");
1020
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
995
1021
  }
996
1022
  }
997
- const actionsJson = localStorage.getItem("followgate_actions");
1023
+ const actionsJson = localStorage.getItem(this.getStorageKey("followgate_actions"));
998
1024
  if (actionsJson) {
999
1025
  try {
1000
1026
  this.completedActions = JSON.parse(actionsJson);
1001
1027
  } catch {
1002
- localStorage.removeItem("followgate_actions");
1028
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
1003
1029
  }
1004
1030
  }
1005
1031
  }
1006
1032
  saveCompletedActions() {
1007
1033
  if (typeof localStorage !== "undefined") {
1008
1034
  localStorage.setItem(
1009
- "followgate_actions",
1035
+ this.getStorageKey("followgate_actions"),
1010
1036
  JSON.stringify(this.completedActions)
1011
1037
  );
1012
1038
  }
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,7 @@ 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(this.getStorageKey("followgate_user"), JSON.stringify(this.currentUser));
829
843
  }
830
844
  if (this.config.debug) {
831
845
  console.log("[FollowGate] Username set:", normalizedUsername);
@@ -850,9 +864,9 @@ var FollowGateClient = class {
850
864
  this.currentUser = null;
851
865
  this.completedActions = [];
852
866
  if (typeof localStorage !== "undefined") {
853
- localStorage.removeItem("followgate_user");
854
- localStorage.removeItem("followgate_actions");
855
- localStorage.removeItem("followgate_unlocked");
867
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
868
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
869
+ localStorage.removeItem(this.getStorageKey("followgate_unlocked"));
856
870
  }
857
871
  if (this.config?.debug) {
858
872
  console.log("[FollowGate] Session reset");
@@ -889,7 +903,9 @@ var FollowGateClient = class {
889
903
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
890
904
  }
891
905
  if (!this.currentUser) {
892
- throw new Error("[FollowGate] No username set. Call setUsername() first.");
906
+ throw new Error(
907
+ "[FollowGate] No username set. Call setUsername() first."
908
+ );
893
909
  }
894
910
  const alreadyCompleted = this.completedActions.some(
895
911
  (a) => a.platform === options.platform && a.action === options.action && a.target === options.target
@@ -915,7 +931,7 @@ var FollowGateClient = class {
915
931
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
916
932
  }
917
933
  if (typeof localStorage !== "undefined") {
918
- localStorage.setItem("followgate_unlocked", "true");
934
+ localStorage.setItem(this.getStorageKey("followgate_unlocked"), "true");
919
935
  }
920
936
  await this.trackEvent("gate_unlocked", {
921
937
  username: this.currentUser?.username,
@@ -931,7 +947,7 @@ var FollowGateClient = class {
931
947
  }
932
948
  isUnlocked() {
933
949
  if (typeof localStorage === "undefined") return false;
934
- return localStorage.getItem("followgate_unlocked") === "true";
950
+ return localStorage.getItem(this.getStorageKey("followgate_unlocked")) === "true";
935
951
  }
936
952
  getUnlockStatus() {
937
953
  return {
@@ -958,29 +974,39 @@ var FollowGateClient = class {
958
974
  // ============================================
959
975
  // Private Methods
960
976
  // ============================================
977
+ /**
978
+ * Get storage key with optional userId suffix
979
+ * This ensures unlock status is per-user, not per-browser
980
+ */
981
+ getStorageKey(base) {
982
+ if (this.config?.userId) {
983
+ return `${base}_${this.config.userId}`;
984
+ }
985
+ return base;
986
+ }
961
987
  restoreSession() {
962
988
  if (typeof localStorage === "undefined") return;
963
- const userJson = localStorage.getItem("followgate_user");
989
+ const userJson = localStorage.getItem(this.getStorageKey("followgate_user"));
964
990
  if (userJson) {
965
991
  try {
966
992
  this.currentUser = JSON.parse(userJson);
967
993
  } catch {
968
- localStorage.removeItem("followgate_user");
994
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
969
995
  }
970
996
  }
971
- const actionsJson = localStorage.getItem("followgate_actions");
997
+ const actionsJson = localStorage.getItem(this.getStorageKey("followgate_actions"));
972
998
  if (actionsJson) {
973
999
  try {
974
1000
  this.completedActions = JSON.parse(actionsJson);
975
1001
  } catch {
976
- localStorage.removeItem("followgate_actions");
1002
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
977
1003
  }
978
1004
  }
979
1005
  }
980
1006
  saveCompletedActions() {
981
1007
  if (typeof localStorage !== "undefined") {
982
1008
  localStorage.setItem(
983
- "followgate_actions",
1009
+ this.getStorageKey("followgate_actions"),
984
1010
  JSON.stringify(this.completedActions)
985
1011
  );
986
1012
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@followgate/js",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",