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