@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 +315 -85
- package/dist/index.d.mts +36 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +173 -18
- package/dist/index.mjs +173 -18
- package/package.json +1 -1
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
|
|
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
|
+
```
|
|
44
59
|
|
|
45
|
-
|
|
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',
|
|
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.
|
|
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
|
-
|
|
111
|
+
## Configuration Options
|
|
59
112
|
|
|
60
113
|
```typescript
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
###
|
|
141
|
+
### Handle & Target Formats
|
|
142
|
+
|
|
143
|
+
The SDK automatically normalizes usernames - you can use `@username` or `username`, both work:
|
|
70
144
|
|
|
71
|
-
|
|
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({
|
|
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: '
|
|
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
|
-
###
|
|
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
|
-
|
|
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('
|
|
89
|
-
|
|
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
|
-
##
|
|
288
|
+
## Supported Platforms
|
|
98
289
|
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
- `target` for `repost`/`like`: Tweet ID
|
|
298
|
+
### Next.js (App Router)
|
|
103
299
|
|
|
104
300
|
```typescript
|
|
105
|
-
//
|
|
106
|
-
|
|
301
|
+
// app/welcome/page.tsx
|
|
302
|
+
'use client';
|
|
107
303
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
###
|
|
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
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
+
Full TypeScript support included:
|
|
133
373
|
|
|
134
374
|
```typescript
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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://
|
|
170
|
-
- [Documentation](https://followgate.
|
|
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(
|
|
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,10 @@ var FollowGateClient = class {
|
|
|
851
865
|
platform
|
|
852
866
|
};
|
|
853
867
|
if (typeof localStorage !== "undefined") {
|
|
854
|
-
localStorage.setItem(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,10 @@ var FollowGateClient = class {
|
|
|
825
839
|
platform
|
|
826
840
|
};
|
|
827
841
|
if (typeof localStorage !== "undefined") {
|
|
828
|
-
localStorage.setItem(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|