@followgate/js 0.4.0 → 0.6.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/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/index.ts
2
2
  var DEFAULT_API_URL = "https://api.followgate.app";
3
+ var WAIT_TIME_SECONDS = 3;
3
4
  var FollowGateError = class extends Error {
4
5
  constructor(message, code, hint) {
5
6
  super(message);
@@ -11,335 +12,979 @@ var FollowGateError = class extends Error {
11
12
  function isValidApiKeyFormat(apiKey) {
12
13
  return /^fg_(live|test)_[a-zA-Z0-9_-]+$/.test(apiKey);
13
14
  }
15
+ var MODAL_STYLES = `
16
+ .fg-modal-backdrop {
17
+ position: fixed;
18
+ inset: 0;
19
+ z-index: 99999;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ background: rgba(0, 0, 0, 0.8);
24
+ backdrop-filter: blur(4px);
25
+ opacity: 0;
26
+ transition: opacity 0.3s ease;
27
+ }
28
+
29
+ .fg-modal-backdrop.fg-visible {
30
+ opacity: 1;
31
+ }
32
+
33
+ .fg-modal {
34
+ position: relative;
35
+ width: 100%;
36
+ max-width: 420px;
37
+ margin: 16px;
38
+ background: #0f172a;
39
+ border-radius: 16px;
40
+ border: 1px solid #334155;
41
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
42
+ padding: 32px;
43
+ transform: scale(0.95);
44
+ transition: transform 0.3s ease;
45
+ }
46
+
47
+ .fg-modal-backdrop.fg-visible .fg-modal {
48
+ transform: scale(1);
49
+ }
50
+
51
+ .fg-icon-box {
52
+ width: 64px;
53
+ height: 64px;
54
+ margin: 0 auto 24px;
55
+ background: #1e293b;
56
+ border-radius: 16px;
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ border: 1px solid #334155;
61
+ }
62
+
63
+ .fg-icon-box svg {
64
+ width: 32px;
65
+ height: 32px;
66
+ fill: currentColor;
67
+ color: white;
68
+ }
69
+
70
+ .fg-icon-box.fg-success {
71
+ background: rgba(34, 197, 94, 0.1);
72
+ border-color: rgba(34, 197, 94, 0.3);
73
+ }
74
+
75
+ .fg-icon-box.fg-success svg {
76
+ color: #22c55e;
77
+ }
78
+
79
+ .fg-title {
80
+ font-size: 24px;
81
+ font-weight: 700;
82
+ color: white;
83
+ text-align: center;
84
+ margin: 0 0 8px;
85
+ font-family: system-ui, -apple-system, sans-serif;
86
+ }
87
+
88
+ .fg-subtitle {
89
+ font-size: 14px;
90
+ color: #94a3b8;
91
+ text-align: center;
92
+ margin: 0 0 24px;
93
+ font-family: system-ui, -apple-system, sans-serif;
94
+ }
95
+
96
+ .fg-user-badge {
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ gap: 8px;
101
+ padding: 6px 12px;
102
+ background: #1e293b;
103
+ border-radius: 9999px;
104
+ width: fit-content;
105
+ margin: 0 auto 16px;
106
+ }
107
+
108
+ .fg-user-badge-dot {
109
+ width: 8px;
110
+ height: 8px;
111
+ background: #22c55e;
112
+ border-radius: 50%;
113
+ }
114
+
115
+ .fg-user-badge-text {
116
+ font-size: 14px;
117
+ color: #cbd5e1;
118
+ font-family: system-ui, -apple-system, sans-serif;
119
+ }
120
+
121
+ .fg-input-wrapper {
122
+ position: relative;
123
+ margin-bottom: 16px;
124
+ }
125
+
126
+ .fg-input-prefix {
127
+ position: absolute;
128
+ left: 16px;
129
+ top: 50%;
130
+ transform: translateY(-50%);
131
+ color: #64748b;
132
+ font-family: system-ui, -apple-system, sans-serif;
133
+ }
134
+
135
+ .fg-input {
136
+ width: 100%;
137
+ padding: 16px 16px 16px 36px;
138
+ background: #1e293b;
139
+ border: 1px solid #334155;
140
+ border-radius: 12px;
141
+ color: white;
142
+ font-size: 16px;
143
+ outline: none;
144
+ transition: border-color 0.2s, box-shadow 0.2s;
145
+ font-family: system-ui, -apple-system, sans-serif;
146
+ box-sizing: border-box;
147
+ }
148
+
149
+ .fg-input:focus {
150
+ border-color: var(--fg-accent, #6366f1);
151
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
152
+ }
153
+
154
+ .fg-input::placeholder {
155
+ color: #475569;
156
+ }
157
+
158
+ .fg-btn {
159
+ width: 100%;
160
+ padding: 16px 24px;
161
+ border-radius: 12px;
162
+ font-size: 16px;
163
+ font-weight: 600;
164
+ cursor: pointer;
165
+ transition: all 0.2s;
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ gap: 12px;
170
+ border: none;
171
+ font-family: system-ui, -apple-system, sans-serif;
172
+ box-sizing: border-box;
173
+ }
174
+
175
+ .fg-btn:disabled {
176
+ opacity: 0.5;
177
+ cursor: not-allowed;
178
+ }
179
+
180
+ .fg-btn-primary {
181
+ background: var(--fg-accent, #6366f1);
182
+ color: white;
183
+ }
184
+
185
+ .fg-btn-primary:hover:not(:disabled) {
186
+ background: var(--fg-accent-hover, #4f46e5);
187
+ transform: scale(1.02);
188
+ }
189
+
190
+ .fg-btn-primary:active:not(:disabled) {
191
+ transform: scale(0.98);
192
+ }
193
+
194
+ .fg-btn-dark {
195
+ background: #000;
196
+ color: white;
197
+ border: 1px solid #334155;
198
+ }
199
+
200
+ .fg-btn-dark:hover:not(:disabled) {
201
+ background: #111;
202
+ transform: scale(1.02);
203
+ }
204
+
205
+ .fg-btn-green {
206
+ background: #16a34a;
207
+ color: white;
208
+ }
209
+
210
+ .fg-btn-green:hover:not(:disabled) {
211
+ background: #15803d;
212
+ transform: scale(1.02);
213
+ }
214
+
215
+ .fg-btn-secondary {
216
+ background: transparent;
217
+ color: #94a3b8;
218
+ border: 1px solid #334155;
219
+ margin-top: 12px;
220
+ }
221
+
222
+ .fg-btn-secondary:hover:not(:disabled) {
223
+ background: #1e293b;
224
+ }
225
+
226
+ .fg-btn svg {
227
+ width: 20px;
228
+ height: 20px;
229
+ flex-shrink: 0;
230
+ }
231
+
232
+ .fg-spinner {
233
+ width: 20px;
234
+ height: 20px;
235
+ border: 2px solid transparent;
236
+ border-top-color: currentColor;
237
+ border-radius: 50%;
238
+ animation: fg-spin 0.8s linear infinite;
239
+ }
240
+
241
+ @keyframes fg-spin {
242
+ to { transform: rotate(360deg); }
243
+ }
244
+
245
+ .fg-hint {
246
+ font-size: 12px;
247
+ color: #475569;
248
+ text-align: center;
249
+ margin-top: 16px;
250
+ font-family: system-ui, -apple-system, sans-serif;
251
+ }
252
+
253
+ .fg-info-box {
254
+ background: rgba(30, 41, 59, 0.5);
255
+ padding: 8px 12px;
256
+ border-radius: 8px;
257
+ margin-bottom: 12px;
258
+ }
259
+
260
+ .fg-info-box p {
261
+ font-size: 12px;
262
+ color: #64748b;
263
+ text-align: center;
264
+ margin: 0;
265
+ font-family: system-ui, -apple-system, sans-serif;
266
+ }
267
+
268
+ .fg-warning-box {
269
+ background: rgba(245, 158, 11, 0.1);
270
+ border: 1px solid rgba(245, 158, 11, 0.3);
271
+ border-radius: 12px;
272
+ padding: 16px;
273
+ margin-bottom: 24px;
274
+ display: flex;
275
+ align-items: flex-start;
276
+ gap: 12px;
277
+ }
278
+
279
+ .fg-warning-box svg {
280
+ width: 20px;
281
+ height: 20px;
282
+ color: #fbbf24;
283
+ flex-shrink: 0;
284
+ margin-top: 2px;
285
+ }
286
+
287
+ .fg-warning-box p {
288
+ font-size: 14px;
289
+ color: #fde68a;
290
+ margin: 0;
291
+ font-family: system-ui, -apple-system, sans-serif;
292
+ }
293
+
294
+ .fg-step-indicator {
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ gap: 12px;
299
+ margin-bottom: 24px;
300
+ }
301
+
302
+ .fg-step-dot {
303
+ width: 32px;
304
+ height: 32px;
305
+ border-radius: 50%;
306
+ display: flex;
307
+ align-items: center;
308
+ justify-content: center;
309
+ font-size: 14px;
310
+ font-weight: 700;
311
+ transition: all 0.3s;
312
+ font-family: system-ui, -apple-system, sans-serif;
313
+ }
314
+
315
+ .fg-step-dot.fg-pending {
316
+ background: #334155;
317
+ color: #64748b;
318
+ }
319
+
320
+ .fg-step-dot.fg-active {
321
+ background: var(--fg-accent, #6366f1);
322
+ color: white;
323
+ }
324
+
325
+ .fg-step-dot.fg-done {
326
+ background: #22c55e;
327
+ color: white;
328
+ }
329
+
330
+ .fg-step-line {
331
+ width: 24px;
332
+ height: 2px;
333
+ background: #334155;
334
+ transition: background 0.3s;
335
+ }
336
+
337
+ .fg-step-line.fg-done {
338
+ background: #22c55e;
339
+ }
340
+
341
+ .fg-footer {
342
+ margin-top: 24px;
343
+ padding-top: 16px;
344
+ border-top: 1px solid #1e293b;
345
+ text-align: center;
346
+ }
347
+
348
+ .fg-footer p {
349
+ font-size: 12px;
350
+ color: #475569;
351
+ margin: 0;
352
+ font-family: system-ui, -apple-system, sans-serif;
353
+ }
354
+
355
+ .fg-footer a {
356
+ color: #64748b;
357
+ text-decoration: none;
358
+ transition: color 0.2s;
359
+ }
360
+
361
+ .fg-footer a:hover {
362
+ color: #94a3b8;
363
+ }
364
+
365
+ .fg-btn-row {
366
+ display: flex;
367
+ gap: 8px;
368
+ margin-top: 12px;
369
+ }
370
+
371
+ .fg-btn-row .fg-btn {
372
+ flex: 1;
373
+ padding: 12px 16px;
374
+ font-size: 14px;
375
+ }
376
+ `;
377
+ var ICONS = {
378
+ x: '<svg viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
379
+ check: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
380
+ repost: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>',
381
+ warning: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>'
382
+ };
14
383
  var FollowGateClient = class {
15
384
  config = null;
16
385
  listeners = /* @__PURE__ */ new Map();
17
386
  currentUser = null;
18
- authToken = null;
19
- pendingUsername = null;
387
+ completedActions = [];
388
+ modalElement = null;
389
+ stylesInjected = false;
20
390
  /**
21
391
  * Initialize the SDK
22
- * @throws {FollowGateError} If configuration is invalid
23
392
  */
24
393
  init(config) {
25
394
  if (!config.appId || typeof config.appId !== "string") {
26
395
  throw new FollowGateError(
27
396
  "[FollowGate] Missing or invalid appId",
28
397
  "INVALID_APP_ID",
29
- "Get your App ID from https://followgate.app/dashboard. Make sure NEXT_PUBLIC_FOLLOWGATE_APP_ID is set in your environment."
398
+ "Get your App ID from https://followgate.app/dashboard"
30
399
  );
31
400
  }
32
401
  if (config.appId.trim() === "" || config.appId === "undefined") {
33
402
  throw new FollowGateError(
34
403
  "[FollowGate] appId is empty or undefined",
35
404
  "EMPTY_APP_ID",
36
- "Your appId appears to be empty. This often happens when environment variables are not properly configured. Check that NEXT_PUBLIC_FOLLOWGATE_APP_ID is set and rebuild your application."
405
+ "Check that NEXT_PUBLIC_FOLLOWGATE_APP_ID is set"
37
406
  );
38
407
  }
39
408
  if (!config.apiKey || typeof config.apiKey !== "string") {
40
409
  throw new FollowGateError(
41
410
  "[FollowGate] Missing or invalid apiKey",
42
411
  "INVALID_API_KEY",
43
- "Get your API Key from https://followgate.app/dashboard. Make sure NEXT_PUBLIC_FOLLOWGATE_API_KEY is set in your environment."
412
+ "Get your API Key from https://followgate.app/dashboard"
44
413
  );
45
414
  }
46
415
  if (config.apiKey.trim() === "" || config.apiKey === "undefined") {
47
416
  throw new FollowGateError(
48
417
  "[FollowGate] apiKey is empty or undefined",
49
418
  "EMPTY_API_KEY",
50
- "Your apiKey appears to be empty. This often happens when environment variables are not properly configured at BUILD TIME (not runtime). Set NEXT_PUBLIC_FOLLOWGATE_API_KEY and REBUILD your application."
419
+ "Set NEXT_PUBLIC_FOLLOWGATE_API_KEY and rebuild"
51
420
  );
52
421
  }
53
422
  if (!isValidApiKeyFormat(config.apiKey)) {
54
423
  throw new FollowGateError(
55
- `[FollowGate] Invalid API key format: "${config.apiKey.substring(0, 10)}..."`,
424
+ `[FollowGate] Invalid API key format`,
56
425
  "INVALID_API_KEY_FORMAT",
57
- 'API keys should start with "fg_live_" (production) or "fg_test_" (development). Get a valid key from https://followgate.app/dashboard'
426
+ 'API keys should start with "fg_live_" or "fg_test_"'
58
427
  );
59
428
  }
60
429
  this.config = {
61
430
  ...config,
62
- apiUrl: config.apiUrl || DEFAULT_API_URL
431
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
432
+ theme: config.theme || "dark",
433
+ accentColor: config.accentColor || "#6366f1"
63
434
  };
64
- this.handleAuthCallback();
65
435
  this.restoreSession();
66
436
  if (config.debug) {
67
437
  console.log("[FollowGate] Initialized with appId:", config.appId);
68
- console.log(
69
- "[FollowGate] API Key:",
70
- config.apiKey.substring(0, 12) + "..."
71
- );
72
438
  if (this.currentUser) {
73
439
  console.log("[FollowGate] Restored user:", this.currentUser.username);
74
440
  }
75
441
  }
76
442
  }
443
+ // ============================================
444
+ // Modal UI Methods
445
+ // ============================================
77
446
  /**
78
- * Authenticate user via Twitter OAuth (handled by FollowGate)
79
- * This identifies WHO is completing the social actions.
447
+ * Show the FollowGate modal
448
+ * If user is already unlocked, calls onComplete immediately
80
449
  */
81
- authenticate(options = {}) {
450
+ show() {
82
451
  if (!this.config) {
83
452
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
84
453
  }
85
- const redirectUri = options.redirectUri || window.location.href.split("?")[0];
86
- const authUrl = `${this.config.apiUrl}/api/v1/auth/twitter?app_id=${encodeURIComponent(this.config.appId)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
87
- if (this.config.debug) {
88
- console.log("[FollowGate] Starting auth flow:", authUrl);
89
- }
90
- if (options.popup) {
91
- const popup = window.open(
92
- authUrl,
93
- "followgate_auth",
94
- "width=600,height=700"
95
- );
96
- if (!popup) {
97
- this.emit("error", { message: "Popup blocked" });
454
+ if (this.isUnlocked()) {
455
+ if (this.config.debug) {
456
+ console.log("[FollowGate] Already unlocked, skipping modal");
98
457
  }
99
- } else {
100
- window.location.href = authUrl;
458
+ this.config.onComplete?.();
459
+ return;
101
460
  }
461
+ this.injectStyles();
462
+ this.createModal();
102
463
  }
103
464
  /**
104
- * Get current authenticated user
465
+ * Hide the modal
105
466
  */
106
- getUser() {
107
- return this.currentUser;
467
+ hide() {
468
+ if (this.modalElement) {
469
+ this.modalElement.classList.remove("fg-visible");
470
+ setTimeout(() => {
471
+ this.modalElement?.remove();
472
+ this.modalElement = null;
473
+ }, 300);
474
+ }
108
475
  }
109
- /**
110
- * Check if user is authenticated
111
- */
112
- isAuthenticated() {
113
- return this.currentUser !== null && this.authToken !== null;
476
+ injectStyles() {
477
+ if (this.stylesInjected) return;
478
+ if (typeof document === "undefined") return;
479
+ const style = document.createElement("style");
480
+ style.id = "followgate-styles";
481
+ style.textContent = MODAL_STYLES;
482
+ document.head.appendChild(style);
483
+ if (this.config?.accentColor) {
484
+ document.documentElement.style.setProperty("--fg-accent", this.config.accentColor);
485
+ document.documentElement.style.setProperty("--fg-accent-hover", this.config.accentColor);
486
+ }
487
+ this.stylesInjected = true;
114
488
  }
115
- /**
116
- * Logout - clear stored session
117
- */
118
- logout() {
119
- this.currentUser = null;
120
- this.authToken = null;
121
- this.pendingUsername = null;
122
- if (typeof localStorage !== "undefined") {
123
- localStorage.removeItem("followgate_token");
124
- localStorage.removeItem("followgate_user");
125
- localStorage.removeItem("followgate_pending_username");
489
+ createModal() {
490
+ if (typeof document === "undefined") return;
491
+ const backdrop = document.createElement("div");
492
+ backdrop.className = "fg-modal-backdrop";
493
+ backdrop.innerHTML = `
494
+ <div class="fg-modal">
495
+ <div id="fg-content"></div>
496
+ <div class="fg-footer">
497
+ <p>Powered by <a href="https://followgate.app" target="_blank" rel="noopener">FollowGate</a></p>
498
+ </div>
499
+ </div>
500
+ `;
501
+ document.body.appendChild(backdrop);
502
+ this.modalElement = backdrop;
503
+ requestAnimationFrame(() => {
504
+ backdrop.classList.add("fg-visible");
505
+ });
506
+ if (this.hasUsername()) {
507
+ this.renderFollowStep();
508
+ } else {
509
+ this.renderUsernameStep();
126
510
  }
127
- if (this.config?.debug) {
128
- console.log("[FollowGate] User logged out");
511
+ }
512
+ getContentElement() {
513
+ return document.getElementById("fg-content");
514
+ }
515
+ renderUsernameStep() {
516
+ const content = this.getContentElement();
517
+ if (!content) return;
518
+ content.innerHTML = `
519
+ <div class="fg-icon-box">
520
+ ${ICONS.x}
521
+ </div>
522
+ <h2 class="fg-title">Unlock Free Access</h2>
523
+ <p class="fg-subtitle">Enter your X username to get started</p>
524
+ <div class="fg-input-wrapper">
525
+ <span class="fg-input-prefix">@</span>
526
+ <input type="text" class="fg-input" id="fg-username-input" placeholder="your_username" autofocus>
527
+ </div>
528
+ <button class="fg-btn fg-btn-primary" id="fg-username-submit" disabled>
529
+ Continue
530
+ </button>
531
+ <p class="fg-hint">No login required \u2013 just your username</p>
532
+ `;
533
+ const input = document.getElementById("fg-username-input");
534
+ const btn = document.getElementById("fg-username-submit");
535
+ input?.addEventListener("input", () => {
536
+ btn.disabled = !input.value.trim();
537
+ });
538
+ input?.addEventListener("keydown", (e) => {
539
+ if (e.key === "Enter" && input.value.trim()) {
540
+ this.handleUsernameSubmit(input.value.trim());
541
+ }
542
+ });
543
+ btn?.addEventListener("click", () => {
544
+ if (input?.value.trim()) {
545
+ this.handleUsernameSubmit(input.value.trim());
546
+ }
547
+ });
548
+ setTimeout(() => input?.focus(), 100);
549
+ }
550
+ handleUsernameSubmit(username) {
551
+ const normalized = username.replace(/^@/, "");
552
+ this.setUsername(normalized);
553
+ this.renderFollowStep();
554
+ }
555
+ renderFollowStep() {
556
+ const content = this.getContentElement();
557
+ if (!content || !this.config?.twitter) return;
558
+ const handle = this.config.twitter.handle;
559
+ const hasRepost = !!this.config.twitter.tweetId;
560
+ content.innerHTML = `
561
+ ${hasRepost ? this.renderStepIndicator(1) : ""}
562
+ <div class="fg-icon-box">
563
+ ${ICONS.x}
564
+ </div>
565
+ ${this.currentUser ? `
566
+ <div class="fg-user-badge">
567
+ <div class="fg-user-badge-dot"></div>
568
+ <span class="fg-user-badge-text">@${this.currentUser.username}</span>
569
+ </div>
570
+ ` : ""}
571
+ <h2 class="fg-title">${hasRepost ? "Step 1: Follow" : "Follow to Continue"}</h2>
572
+ <p class="fg-subtitle" id="fg-follow-subtitle">Follow @${handle} on X</p>
573
+ <div id="fg-follow-actions">
574
+ <button class="fg-btn fg-btn-dark" id="fg-follow-btn">
575
+ ${ICONS.x}
576
+ Follow @${handle}
577
+ </button>
578
+ </div>
579
+ <p class="fg-hint">A new window will open. Return here after following.</p>
580
+ `;
581
+ document.getElementById("fg-follow-btn")?.addEventListener("click", () => {
582
+ this.handleFollowClick();
583
+ });
584
+ }
585
+ handleFollowClick() {
586
+ if (!this.config?.twitter) return;
587
+ const handle = this.config.twitter.handle;
588
+ this.openIntent({
589
+ platform: "twitter",
590
+ action: "follow",
591
+ target: handle
592
+ });
593
+ this.showFollowConfirmation();
594
+ }
595
+ showFollowConfirmation() {
596
+ if (!this.config?.twitter) return;
597
+ const handle = this.config.twitter.handle;
598
+ const subtitle = document.getElementById("fg-follow-subtitle");
599
+ const actions = document.getElementById("fg-follow-actions");
600
+ if (subtitle) {
601
+ subtitle.textContent = `Did you follow @${handle}?`;
602
+ }
603
+ if (actions) {
604
+ let seconds = WAIT_TIME_SECONDS;
605
+ actions.innerHTML = `
606
+ <div class="fg-info-box">
607
+ <p>You can close the X window after following</p>
608
+ </div>
609
+ <button class="fg-btn fg-btn-green" id="fg-follow-confirm" disabled>
610
+ <span class="fg-spinner"></span>
611
+ Waiting... (${seconds}s)
612
+ </button>
613
+ <button class="fg-btn fg-btn-secondary" id="fg-follow-retry">
614
+ ${ICONS.x}
615
+ Open again
616
+ </button>
617
+ `;
618
+ const confirmBtn = document.getElementById("fg-follow-confirm");
619
+ const retryBtn = document.getElementById("fg-follow-retry");
620
+ const interval = setInterval(() => {
621
+ seconds--;
622
+ if (seconds <= 0) {
623
+ clearInterval(interval);
624
+ if (confirmBtn) {
625
+ confirmBtn.disabled = false;
626
+ confirmBtn.innerHTML = `${ICONS.check} Yes, I followed`;
627
+ }
628
+ } else if (confirmBtn) {
629
+ confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
630
+ }
631
+ }, 1e3);
632
+ confirmBtn?.addEventListener("click", () => {
633
+ this.handleFollowConfirm();
634
+ });
635
+ retryBtn?.addEventListener("click", () => {
636
+ this.handleFollowClick();
637
+ });
129
638
  }
130
639
  }
131
- /**
132
- * Check if username input is needed (Twitter Free Tier limitation)
133
- */
134
- needsUsernameInput() {
135
- return this.pendingUsername !== null;
640
+ async handleFollowConfirm() {
641
+ if (!this.config?.twitter) return;
642
+ const handle = this.config.twitter.handle;
643
+ await this.complete({
644
+ platform: "twitter",
645
+ action: "follow",
646
+ target: handle
647
+ });
648
+ if (this.config.twitter.tweetId) {
649
+ this.renderRepostStep();
650
+ } else {
651
+ this.renderConfirmStep();
652
+ }
653
+ }
654
+ renderRepostStep() {
655
+ const content = this.getContentElement();
656
+ if (!content || !this.config?.twitter?.tweetId) return;
657
+ content.innerHTML = `
658
+ ${this.renderStepIndicator(2)}
659
+ <div class="fg-icon-box fg-success">
660
+ ${ICONS.repost}
661
+ </div>
662
+ ${this.currentUser ? `
663
+ <div class="fg-user-badge">
664
+ <div class="fg-user-badge-dot"></div>
665
+ <span class="fg-user-badge-text">@${this.currentUser.username}</span>
666
+ </div>
667
+ ` : ""}
668
+ <h2 class="fg-title">Step 2: Repost</h2>
669
+ <p class="fg-subtitle" id="fg-repost-subtitle">Repost the tweet to continue</p>
670
+ <div id="fg-repost-actions">
671
+ <button class="fg-btn fg-btn-green" id="fg-repost-btn">
672
+ ${ICONS.repost}
673
+ Repost tweet
674
+ </button>
675
+ </div>
676
+ <p class="fg-hint">A new window will open. Return here after reposting.</p>
677
+ `;
678
+ document.getElementById("fg-repost-btn")?.addEventListener("click", () => {
679
+ this.handleRepostClick();
680
+ });
681
+ }
682
+ handleRepostClick() {
683
+ if (!this.config?.twitter?.tweetId) return;
684
+ const tweetId = this.config.twitter.tweetId;
685
+ this.openIntent({
686
+ platform: "twitter",
687
+ action: "repost",
688
+ target: tweetId
689
+ });
690
+ this.showRepostConfirmation();
691
+ }
692
+ showRepostConfirmation() {
693
+ const subtitle = document.getElementById("fg-repost-subtitle");
694
+ const actions = document.getElementById("fg-repost-actions");
695
+ if (subtitle) {
696
+ subtitle.textContent = "Did you repost the tweet?";
697
+ }
698
+ if (actions) {
699
+ let seconds = WAIT_TIME_SECONDS;
700
+ actions.innerHTML = `
701
+ <div class="fg-info-box">
702
+ <p>You can close the X window after reposting</p>
703
+ </div>
704
+ <button class="fg-btn fg-btn-green" id="fg-repost-confirm" disabled>
705
+ <span class="fg-spinner"></span>
706
+ Waiting... (${seconds}s)
707
+ </button>
708
+ <button class="fg-btn fg-btn-secondary" id="fg-repost-retry">
709
+ ${ICONS.repost}
710
+ Open tweet again
711
+ </button>
712
+ `;
713
+ const confirmBtn = document.getElementById("fg-repost-confirm");
714
+ const retryBtn = document.getElementById("fg-repost-retry");
715
+ const interval = setInterval(() => {
716
+ seconds--;
717
+ if (seconds <= 0) {
718
+ clearInterval(interval);
719
+ if (confirmBtn) {
720
+ confirmBtn.disabled = false;
721
+ confirmBtn.innerHTML = `${ICONS.check} Yes, I reposted`;
722
+ }
723
+ } else if (confirmBtn) {
724
+ confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
725
+ }
726
+ }, 1e3);
727
+ confirmBtn?.addEventListener("click", () => {
728
+ this.handleRepostConfirm();
729
+ });
730
+ retryBtn?.addEventListener("click", () => {
731
+ this.handleRepostClick();
732
+ });
733
+ }
734
+ }
735
+ async handleRepostConfirm() {
736
+ if (!this.config?.twitter?.tweetId) return;
737
+ await this.complete({
738
+ platform: "twitter",
739
+ action: "repost",
740
+ target: this.config.twitter.tweetId
741
+ });
742
+ this.renderConfirmStep();
136
743
  }
744
+ renderConfirmStep() {
745
+ const content = this.getContentElement();
746
+ if (!content) return;
747
+ content.innerHTML = `
748
+ <h2 class="fg-title">Almost done!</h2>
749
+ <div class="fg-warning-box">
750
+ ${ICONS.warning}
751
+ <p><strong>Note:</strong> Access may be revoked if actions are not completed.</p>
752
+ </div>
753
+ <button class="fg-btn fg-btn-green" id="fg-finish-btn">
754
+ ${ICONS.check}
755
+ Got it
756
+ </button>
757
+ ${this.config?.twitter ? `
758
+ <div class="fg-btn-row">
759
+ <button class="fg-btn fg-btn-secondary" id="fg-redo-follow">
760
+ ${ICONS.x}
761
+ Open follow
762
+ </button>
763
+ ${this.config.twitter.tweetId ? `
764
+ <button class="fg-btn fg-btn-secondary" id="fg-redo-repost">
765
+ ${ICONS.repost}
766
+ Open repost
767
+ </button>
768
+ ` : ""}
769
+ </div>
770
+ ` : ""}
771
+ `;
772
+ document.getElementById("fg-finish-btn")?.addEventListener("click", () => {
773
+ this.handleFinish();
774
+ });
775
+ document.getElementById("fg-redo-follow")?.addEventListener("click", () => {
776
+ if (this.config?.twitter) {
777
+ this.openIntent({
778
+ platform: "twitter",
779
+ action: "follow",
780
+ target: this.config.twitter.handle
781
+ });
782
+ }
783
+ });
784
+ document.getElementById("fg-redo-repost")?.addEventListener("click", () => {
785
+ if (this.config?.twitter?.tweetId) {
786
+ this.openIntent({
787
+ platform: "twitter",
788
+ action: "repost",
789
+ target: this.config.twitter.tweetId
790
+ });
791
+ }
792
+ });
793
+ }
794
+ async handleFinish() {
795
+ await this.unlock();
796
+ this.hide();
797
+ this.config?.onComplete?.();
798
+ }
799
+ renderStepIndicator(currentStep) {
800
+ return `
801
+ <div class="fg-step-indicator">
802
+ <div class="fg-step-dot ${currentStep > 1 ? "fg-done" : currentStep === 1 ? "fg-active" : "fg-pending"}">
803
+ ${currentStep > 1 ? "\u2713" : "1"}
804
+ </div>
805
+ <div class="fg-step-line ${currentStep > 1 ? "fg-done" : ""}"></div>
806
+ <div class="fg-step-dot ${currentStep > 2 ? "fg-done" : currentStep === 2 ? "fg-active" : "fg-pending"}">
807
+ ${currentStep > 2 ? "\u2713" : "2"}
808
+ </div>
809
+ </div>
810
+ `;
811
+ }
812
+ // ============================================
813
+ // Core SDK Methods
814
+ // ============================================
137
815
  /**
138
- * Set username manually (when needsUsernameInput() returns true)
816
+ * Set the user's social username
139
817
  */
140
- setUsername(username) {
141
- if (!this.pendingUsername) {
142
- throw new Error(
143
- "[FollowGate] No pending username state. User is either not authenticated or username is already set."
144
- );
818
+ setUsername(username, platform = "twitter") {
819
+ if (!this.config) {
820
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
145
821
  }
146
822
  const normalizedUsername = username.startsWith("@") ? username.slice(1) : username;
147
823
  this.currentUser = {
148
- userId: "user_input",
149
824
  username: normalizedUsername,
150
- platform: "twitter"
825
+ platform
151
826
  };
152
- this.authToken = this.pendingUsername.token;
153
827
  if (typeof localStorage !== "undefined") {
154
- localStorage.setItem("followgate_token", this.authToken);
155
828
  localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
156
- localStorage.removeItem("followgate_pending_username");
157
829
  }
158
- this.pendingUsername = null;
159
- this.emit("authenticated", this.currentUser);
160
- if (this.config?.debug) {
161
- console.log("[FollowGate] Username set manually:", normalizedUsername);
830
+ if (this.config.debug) {
831
+ console.log("[FollowGate] Username set:", normalizedUsername);
162
832
  }
163
833
  }
164
834
  /**
165
- * Handle auth callback from URL params
835
+ * Get current user
166
836
  */
167
- handleAuthCallback() {
168
- if (typeof window === "undefined") return;
169
- const params = new URLSearchParams(window.location.search);
170
- const token = params.get("followgate_token");
171
- const username = params.get("followgate_user");
172
- const needsUsername = params.get("followgate_needs_username") === "true";
173
- if (token && needsUsername) {
174
- this.pendingUsername = {
175
- needsUsername: true,
176
- token
177
- };
178
- if (typeof localStorage !== "undefined") {
179
- localStorage.setItem(
180
- "followgate_pending_username",
181
- JSON.stringify(this.pendingUsername)
182
- );
183
- }
184
- const url = new URL(window.location.href);
185
- url.searchParams.delete("followgate_token");
186
- url.searchParams.delete("followgate_needs_username");
187
- window.history.replaceState({}, "", url.toString());
188
- if (this.config?.debug) {
189
- console.log("[FollowGate] OAuth successful, username input needed");
190
- }
191
- } else if (token && username) {
192
- this.authToken = token;
193
- this.currentUser = {
194
- userId: "",
195
- // Will be set from token verification
196
- username,
197
- platform: "twitter"
198
- };
199
- if (typeof localStorage !== "undefined") {
200
- localStorage.setItem("followgate_token", token);
201
- localStorage.setItem(
202
- "followgate_user",
203
- JSON.stringify(this.currentUser)
204
- );
205
- }
206
- const url = new URL(window.location.href);
207
- url.searchParams.delete("followgate_token");
208
- url.searchParams.delete("followgate_user");
209
- window.history.replaceState({}, "", url.toString());
210
- this.emit("authenticated", this.currentUser);
211
- if (this.config?.debug) {
212
- console.log("[FollowGate] User authenticated:", username);
213
- }
214
- }
837
+ getUser() {
838
+ return this.currentUser;
215
839
  }
216
840
  /**
217
- * Restore session from localStorage
841
+ * Check if username is set
218
842
  */
219
- restoreSession() {
220
- if (typeof localStorage === "undefined") return;
221
- const pendingJson = localStorage.getItem("followgate_pending_username");
222
- if (pendingJson) {
223
- try {
224
- this.pendingUsername = JSON.parse(pendingJson);
225
- if (this.config?.debug) {
226
- console.log("[FollowGate] Restored pending username state");
227
- }
228
- return;
229
- } catch {
230
- localStorage.removeItem("followgate_pending_username");
231
- }
232
- }
233
- const token = localStorage.getItem("followgate_token");
234
- const userJson = localStorage.getItem("followgate_user");
235
- if (token && userJson) {
236
- try {
237
- this.authToken = token;
238
- this.currentUser = JSON.parse(userJson);
239
- } catch {
240
- localStorage.removeItem("followgate_token");
241
- localStorage.removeItem("followgate_user");
242
- }
243
- }
843
+ hasUsername() {
844
+ return this.currentUser !== null;
244
845
  }
245
846
  /**
246
- * Open social action popup
847
+ * Clear stored session
247
848
  */
248
- async open(options) {
849
+ reset() {
850
+ this.currentUser = null;
851
+ this.completedActions = [];
852
+ if (typeof localStorage !== "undefined") {
853
+ localStorage.removeItem("followgate_user");
854
+ localStorage.removeItem("followgate_actions");
855
+ localStorage.removeItem("followgate_unlocked");
856
+ }
857
+ if (this.config?.debug) {
858
+ console.log("[FollowGate] Session reset");
859
+ }
860
+ }
861
+ // ============================================
862
+ // Intent URL Methods
863
+ // ============================================
864
+ getFollowUrl(platform, target) {
865
+ return this.buildIntentUrl({ platform, action: "follow", target });
866
+ }
867
+ getRepostUrl(platform, target) {
868
+ return this.buildIntentUrl({ platform, action: "repost", target });
869
+ }
870
+ getLikeUrl(platform, target) {
871
+ return this.buildIntentUrl({ platform, action: "like", target });
872
+ }
873
+ async openIntent(options) {
249
874
  if (!this.config) {
250
875
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
251
876
  }
252
877
  const url = this.buildIntentUrl(options);
253
878
  if (this.config.debug) {
254
- console.log("[FollowGate] Opening:", url);
879
+ console.log("[FollowGate] Opening intent:", url);
255
880
  }
256
- await this.trackEvent("gate_opened", options);
257
- const popup = window.open(url, "_blank", "width=600,height=700");
258
- if (!popup) {
259
- this.emit("error", { message: "Popup blocked" });
260
- return;
881
+ await this.trackEvent("intent_opened", { ...options });
882
+ window.open(url, "_blank", "width=600,height=700");
883
+ }
884
+ // ============================================
885
+ // Completion Methods
886
+ // ============================================
887
+ async complete(options) {
888
+ if (!this.config) {
889
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
890
+ }
891
+ if (!this.currentUser) {
892
+ throw new Error("[FollowGate] No username set. Call setUsername() first.");
893
+ }
894
+ const alreadyCompleted = this.completedActions.some(
895
+ (a) => a.platform === options.platform && a.action === options.action && a.target === options.target
896
+ );
897
+ if (!alreadyCompleted) {
898
+ this.completedActions.push(options);
899
+ this.saveCompletedActions();
261
900
  }
262
- await this.trackEvent("action_clicked", options);
901
+ await this.trackEvent("action_completed", {
902
+ ...options,
903
+ username: this.currentUser.username
904
+ });
263
905
  this.emit("complete", {
264
- platform: options.platform,
265
- action: options.action,
266
- target: options.target
906
+ ...options,
907
+ username: this.currentUser.username
267
908
  });
909
+ if (this.config.debug) {
910
+ console.log("[FollowGate] Action completed:", options);
911
+ }
268
912
  }
269
- /**
270
- * Verify follow status (for Pro/Business tiers with OAuth)
271
- */
272
- async verify(options) {
913
+ async unlock() {
273
914
  if (!this.config) {
274
915
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
275
916
  }
276
- try {
277
- const response = await fetch(`${this.config.apiUrl}/api/v1/verify`, {
278
- method: "POST",
279
- headers: {
280
- "Content-Type": "application/json",
281
- "X-API-Key": this.config.apiKey
282
- },
283
- body: JSON.stringify({
284
- platform: options.platform,
285
- action: options.action,
286
- target: options.target,
287
- externalUserId: options.userId
288
- })
289
- });
290
- const data = await response.json();
291
- return data.success && data.data?.verified === true;
292
- } catch (error) {
293
- if (this.config.debug) {
294
- console.error("[FollowGate] Verification error:", error);
295
- }
296
- return false;
917
+ if (typeof localStorage !== "undefined") {
918
+ localStorage.setItem("followgate_unlocked", "true");
297
919
  }
298
- }
299
- /**
300
- * Track analytics event
301
- */
302
- async trackEvent(event, options) {
303
- if (!this.config) return;
304
- try {
305
- await fetch(`${this.config.apiUrl}/api/v1/events`, {
306
- method: "POST",
307
- headers: {
308
- "Content-Type": "application/json",
309
- "X-API-Key": this.config.apiKey
310
- },
311
- body: JSON.stringify({
312
- event,
313
- platform: options.platform,
314
- action: options.action,
315
- target: options.target,
316
- externalUserId: options.userId
317
- })
318
- });
319
- } catch (error) {
320
- if (this.config.debug) {
321
- console.warn("[FollowGate] Failed to track event:", error);
322
- }
920
+ await this.trackEvent("gate_unlocked", {
921
+ username: this.currentUser?.username,
922
+ actions: this.completedActions
923
+ });
924
+ this.emit("unlocked", {
925
+ username: this.currentUser?.username,
926
+ actions: this.completedActions
927
+ });
928
+ if (this.config.debug) {
929
+ console.log("[FollowGate] Gate unlocked!");
323
930
  }
324
931
  }
325
- /**
326
- * Register event listener
327
- */
932
+ isUnlocked() {
933
+ if (typeof localStorage === "undefined") return false;
934
+ return localStorage.getItem("followgate_unlocked") === "true";
935
+ }
936
+ getUnlockStatus() {
937
+ return {
938
+ unlocked: this.isUnlocked(),
939
+ username: this.currentUser?.username,
940
+ completedActions: [...this.completedActions]
941
+ };
942
+ }
943
+ getCompletedActions() {
944
+ return [...this.completedActions];
945
+ }
946
+ // ============================================
947
+ // Event System
948
+ // ============================================
328
949
  on(event, callback) {
329
950
  if (!this.listeners.has(event)) {
330
951
  this.listeners.set(event, /* @__PURE__ */ new Set());
331
952
  }
332
953
  this.listeners.get(event).add(callback);
333
954
  }
334
- /**
335
- * Remove event listener
336
- */
337
955
  off(event, callback) {
338
956
  this.listeners.get(event)?.delete(callback);
339
957
  }
340
- /**
341
- * Build intent URL for platform
342
- */
958
+ // ============================================
959
+ // Private Methods
960
+ // ============================================
961
+ restoreSession() {
962
+ if (typeof localStorage === "undefined") return;
963
+ const userJson = localStorage.getItem("followgate_user");
964
+ if (userJson) {
965
+ try {
966
+ this.currentUser = JSON.parse(userJson);
967
+ } catch {
968
+ localStorage.removeItem("followgate_user");
969
+ }
970
+ }
971
+ const actionsJson = localStorage.getItem("followgate_actions");
972
+ if (actionsJson) {
973
+ try {
974
+ this.completedActions = JSON.parse(actionsJson);
975
+ } catch {
976
+ localStorage.removeItem("followgate_actions");
977
+ }
978
+ }
979
+ }
980
+ saveCompletedActions() {
981
+ if (typeof localStorage !== "undefined") {
982
+ localStorage.setItem(
983
+ "followgate_actions",
984
+ JSON.stringify(this.completedActions)
985
+ );
986
+ }
987
+ }
343
988
  buildIntentUrl(options) {
344
989
  const { platform, action, target } = options;
345
990
  switch (platform) {
@@ -398,6 +1043,27 @@ var FollowGateClient = class {
398
1043
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
399
1044
  }
400
1045
  }
1046
+ async trackEvent(event, data) {
1047
+ if (!this.config) return;
1048
+ try {
1049
+ await fetch(`${this.config.apiUrl}/api/v1/events`, {
1050
+ method: "POST",
1051
+ headers: {
1052
+ "Content-Type": "application/json",
1053
+ "X-API-Key": this.config.apiKey
1054
+ },
1055
+ body: JSON.stringify({
1056
+ event,
1057
+ appId: this.config.appId,
1058
+ ...data
1059
+ })
1060
+ });
1061
+ } catch (error) {
1062
+ if (this.config.debug) {
1063
+ console.warn("[FollowGate] Failed to track event:", error);
1064
+ }
1065
+ }
1066
+ }
401
1067
  emit(event, data) {
402
1068
  this.listeners.get(event)?.forEach((callback) => callback(data));
403
1069
  }