@followgate/js 0.5.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
@@ -6,6 +6,13 @@ type Platform = 'twitter' | 'bluesky' | 'linkedin';
6
6
  * Supported social actions
7
7
  */
8
8
  type SocialAction = 'follow' | 'repost' | 'like';
9
+ /**
10
+ * Twitter/X configuration
11
+ */
12
+ interface TwitterConfig {
13
+ handle: string;
14
+ tweetId?: string;
15
+ }
9
16
  /**
10
17
  * SDK Configuration
11
18
  */
@@ -14,6 +21,11 @@ interface FollowGateConfig {
14
21
  apiKey: string;
15
22
  apiUrl?: string;
16
23
  debug?: boolean;
24
+ userId?: string;
25
+ twitter?: TwitterConfig;
26
+ onComplete?: () => void;
27
+ theme?: 'dark' | 'light';
28
+ accentColor?: string;
17
29
  }
18
30
  /**
19
31
  * SDK Error class with helpful messages
@@ -56,28 +68,45 @@ interface UnlockStatus {
56
68
  }
57
69
  /**
58
70
  * FollowGate SDK Client
59
- *
60
- * Simple username-based flow:
61
- * 1. User enters username
62
- * 2. User clicks intent URLs to follow/repost
63
- * 3. User confirms they did it
64
- * 4. App is unlocked
65
- *
66
- * No OAuth required!
67
71
  */
68
72
  declare class FollowGateClient {
69
73
  private config;
70
74
  private listeners;
71
75
  private currentUser;
72
76
  private completedActions;
77
+ private modalElement;
78
+ private stylesInjected;
73
79
  /**
74
80
  * Initialize the SDK
75
- * @throws {FollowGateError} If configuration is invalid
76
81
  */
77
82
  init(config: FollowGateConfig): void;
83
+ /**
84
+ * Show the FollowGate modal
85
+ * If user is already unlocked, calls onComplete immediately
86
+ */
87
+ show(): void;
88
+ /**
89
+ * Hide the modal
90
+ */
91
+ hide(): void;
92
+ private injectStyles;
93
+ private createModal;
94
+ private getContentElement;
95
+ private renderUsernameStep;
96
+ private handleUsernameSubmit;
97
+ private renderFollowStep;
98
+ private handleFollowClick;
99
+ private showFollowConfirmation;
100
+ private handleFollowConfirm;
101
+ private renderRepostStep;
102
+ private handleRepostClick;
103
+ private showRepostConfirmation;
104
+ private handleRepostConfirm;
105
+ private renderConfirmStep;
106
+ private handleFinish;
107
+ private renderStepIndicator;
78
108
  /**
79
109
  * Set the user's social username
80
- * This is the main entry point - no OAuth needed!
81
110
  */
82
111
  setUsername(username: string, platform?: Platform): void;
83
112
  /**
@@ -92,73 +121,31 @@ declare class FollowGateClient {
92
121
  * Clear stored session
93
122
  */
94
123
  reset(): void;
95
- /**
96
- * Get follow intent URL for a platform
97
- */
98
124
  getFollowUrl(platform: Platform, target: string): string;
99
- /**
100
- * Get repost/retweet intent URL for a platform
101
- */
102
125
  getRepostUrl(platform: Platform, target: string): string;
103
- /**
104
- * Get like intent URL for a platform
105
- */
106
126
  getLikeUrl(platform: Platform, target: string): string;
107
- /**
108
- * Open intent URL in new window
109
- */
110
127
  openIntent(options: CompleteOptions): Promise<void>;
111
- /**
112
- * Mark an action as completed (trust-first)
113
- * Call this when user confirms they did the action
114
- */
115
128
  complete(options: CompleteOptions): Promise<void>;
116
- /**
117
- * Mark the gate as unlocked
118
- * Call this when all required actions are done
119
- */
120
129
  unlock(): Promise<void>;
121
- /**
122
- * Check if gate is unlocked
123
- */
124
130
  isUnlocked(): boolean;
125
- /**
126
- * Get unlock status with details
127
- */
128
131
  getUnlockStatus(): UnlockStatus;
129
- /**
130
- * Get completed actions
131
- */
132
132
  getCompletedActions(): CompleteOptions[];
133
- /**
134
- * Register event listener
135
- */
136
133
  on(event: EventType, callback: EventCallback): void;
137
- /**
138
- * Remove event listener
139
- */
140
134
  off(event: EventType, callback: EventCallback): void;
141
135
  /**
142
- * Restore session from localStorage
136
+ * Get storage key with optional userId suffix
137
+ * This ensures unlock status is per-user, not per-browser
143
138
  */
139
+ private getStorageKey;
144
140
  private restoreSession;
145
- /**
146
- * Save completed actions to localStorage
147
- */
148
141
  private saveCompletedActions;
149
- /**
150
- * Build intent URL for platform
151
- */
152
142
  private buildIntentUrl;
153
143
  private buildTwitterUrl;
154
144
  private buildBlueskyUrl;
155
145
  private buildLinkedInUrl;
156
- /**
157
- * Track analytics event
158
- */
159
146
  private trackEvent;
160
147
  private emit;
161
148
  }
162
149
  declare const FollowGate: FollowGateClient;
163
150
 
164
- export { type CompleteOptions, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type Platform, type SocialAction, type UnlockStatus, type UserInfo };
151
+ export { type CompleteOptions, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type Platform, type SocialAction, type TwitterConfig, type UnlockStatus, type UserInfo };