@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 +274 -86
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +44 -18
- package/dist/index.mjs +44 -18
- package/package.json +1 -1
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
|
|
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
|
-
//
|
|
25
|
-
FollowGate.
|
|
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
|
-
|
|
38
|
+
The SDK renders a beautiful, fully-functional modal with:
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
##
|
|
46
|
+
## When to Show the Modal
|
|
42
47
|
|
|
43
|
-
|
|
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
|
-
|
|
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',
|
|
50
|
-
apiKey: 'fg_live_xxx',
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
### Event Listeners for Fine-Grained Control
|
|
59
96
|
|
|
60
97
|
```typescript
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
111
|
+
## Configuration Options
|
|
70
112
|
|
|
71
|
-
|
|
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
|
-
|
|
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: '
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
+
### Event Listeners
|
|
85
229
|
|
|
86
230
|
```typescript
|
|
87
231
|
FollowGate.on('complete', (data) => {
|
|
88
|
-
console.log('
|
|
89
|
-
|
|
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
|
-
##
|
|
247
|
+
## Supported Platforms
|
|
98
248
|
|
|
99
|
-
|
|
249
|
+
| Platform | Actions |
|
|
250
|
+
| --------- | -------------------------- |
|
|
251
|
+
| Twitter/X | `follow`, `repost`, `like` |
|
|
252
|
+
| Bluesky | `follow`, `repost`, `like` |
|
|
253
|
+
| LinkedIn | `follow` |
|
|
100
254
|
|
|
101
|
-
|
|
102
|
-
|
|
255
|
+
## Framework Examples
|
|
256
|
+
|
|
257
|
+
### Next.js (App Router)
|
|
103
258
|
|
|
104
259
|
```typescript
|
|
105
|
-
//
|
|
106
|
-
|
|
260
|
+
// app/welcome/page.tsx
|
|
261
|
+
'use client';
|
|
107
262
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
###
|
|
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
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
+
## TypeScript
|
|
133
330
|
|
|
134
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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://
|
|
170
|
-
- [Documentation](https://followgate.
|
|
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(
|
|
511
|
-
|
|
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(
|
|
560
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
485
|
-
|
|
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(
|
|
534
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|