@followgate/js 0.5.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,70 +38,808 @@ 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
413
  completedActions = [];
414
+ modalElement = null;
415
+ stylesInjected = false;
45
416
  /**
46
417
  * Initialize the SDK
47
- * @throws {FollowGateError} If configuration is invalid
48
418
  */
49
419
  init(config) {
50
420
  if (!config.appId || typeof config.appId !== "string") {
51
421
  throw new FollowGateError(
52
422
  "[FollowGate] Missing or invalid appId",
53
423
  "INVALID_APP_ID",
54
- "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"
55
425
  );
56
426
  }
57
427
  if (config.appId.trim() === "" || config.appId === "undefined") {
58
428
  throw new FollowGateError(
59
429
  "[FollowGate] appId is empty or undefined",
60
430
  "EMPTY_APP_ID",
61
- "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"
62
432
  );
63
433
  }
64
434
  if (!config.apiKey || typeof config.apiKey !== "string") {
65
435
  throw new FollowGateError(
66
436
  "[FollowGate] Missing or invalid apiKey",
67
437
  "INVALID_API_KEY",
68
- "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"
69
439
  );
70
440
  }
71
441
  if (config.apiKey.trim() === "" || config.apiKey === "undefined") {
72
442
  throw new FollowGateError(
73
443
  "[FollowGate] apiKey is empty or undefined",
74
444
  "EMPTY_API_KEY",
75
- "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"
76
446
  );
77
447
  }
78
448
  if (!isValidApiKeyFormat(config.apiKey)) {
79
449
  throw new FollowGateError(
80
- `[FollowGate] Invalid API key format: "${config.apiKey.substring(0, 10)}..."`,
450
+ `[FollowGate] Invalid API key format`,
81
451
  "INVALID_API_KEY_FORMAT",
82
- '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_"'
83
453
  );
84
454
  }
85
455
  this.config = {
86
456
  ...config,
87
- apiUrl: config.apiUrl || DEFAULT_API_URL
457
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
458
+ theme: config.theme || "dark",
459
+ accentColor: config.accentColor || "#6366f1"
88
460
  };
89
461
  this.restoreSession();
90
462
  if (config.debug) {
91
463
  console.log("[FollowGate] Initialized with appId:", config.appId);
92
- console.log(
93
- "[FollowGate] API Key:",
94
- config.apiKey.substring(0, 12) + "..."
95
- );
96
464
  if (this.currentUser) {
97
465
  console.log("[FollowGate] Restored user:", this.currentUser.username);
98
466
  }
99
467
  }
100
468
  }
469
+ // ============================================
470
+ // Modal UI Methods
471
+ // ============================================
472
+ /**
473
+ * Show the FollowGate modal
474
+ * If user is already unlocked, calls onComplete immediately
475
+ */
476
+ show() {
477
+ if (!this.config) {
478
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
479
+ }
480
+ if (this.isUnlocked()) {
481
+ if (this.config.debug) {
482
+ console.log("[FollowGate] Already unlocked, skipping modal");
483
+ }
484
+ this.config.onComplete?.();
485
+ return;
486
+ }
487
+ this.injectStyles();
488
+ this.createModal();
489
+ }
490
+ /**
491
+ * Hide the modal
492
+ */
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
+ }
501
+ }
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;
514
+ }
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();
536
+ }
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
+ });
664
+ }
665
+ }
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
+ });
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
+ // ============================================
101
841
  /**
102
842
  * Set the user's social username
103
- * This is the main entry point - no OAuth needed!
104
843
  */
105
844
  setUsername(username, platform = "twitter") {
106
845
  if (!this.config) {
@@ -148,27 +887,15 @@ var FollowGateClient = class {
148
887
  // ============================================
149
888
  // Intent URL Methods
150
889
  // ============================================
151
- /**
152
- * Get follow intent URL for a platform
153
- */
154
890
  getFollowUrl(platform, target) {
155
891
  return this.buildIntentUrl({ platform, action: "follow", target });
156
892
  }
157
- /**
158
- * Get repost/retweet intent URL for a platform
159
- */
160
893
  getRepostUrl(platform, target) {
161
894
  return this.buildIntentUrl({ platform, action: "repost", target });
162
895
  }
163
- /**
164
- * Get like intent URL for a platform
165
- */
166
896
  getLikeUrl(platform, target) {
167
897
  return this.buildIntentUrl({ platform, action: "like", target });
168
898
  }
169
- /**
170
- * Open intent URL in new window
171
- */
172
899
  async openIntent(options) {
173
900
  if (!this.config) {
174
901
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
@@ -183,18 +910,12 @@ var FollowGateClient = class {
183
910
  // ============================================
184
911
  // Completion Methods
185
912
  // ============================================
186
- /**
187
- * Mark an action as completed (trust-first)
188
- * Call this when user confirms they did the action
189
- */
190
913
  async complete(options) {
191
914
  if (!this.config) {
192
915
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
193
916
  }
194
917
  if (!this.currentUser) {
195
- throw new Error(
196
- "[FollowGate] No username set. Call setUsername() first."
197
- );
918
+ throw new Error("[FollowGate] No username set. Call setUsername() first.");
198
919
  }
199
920
  const alreadyCompleted = this.completedActions.some(
200
921
  (a) => a.platform === options.platform && a.action === options.action && a.target === options.target
@@ -215,10 +936,6 @@ var FollowGateClient = class {
215
936
  console.log("[FollowGate] Action completed:", options);
216
937
  }
217
938
  }
218
- /**
219
- * Mark the gate as unlocked
220
- * Call this when all required actions are done
221
- */
222
939
  async unlock() {
223
940
  if (!this.config) {
224
941
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
@@ -238,16 +955,10 @@ var FollowGateClient = class {
238
955
  console.log("[FollowGate] Gate unlocked!");
239
956
  }
240
957
  }
241
- /**
242
- * Check if gate is unlocked
243
- */
244
958
  isUnlocked() {
245
959
  if (typeof localStorage === "undefined") return false;
246
960
  return localStorage.getItem("followgate_unlocked") === "true";
247
961
  }
248
- /**
249
- * Get unlock status with details
250
- */
251
962
  getUnlockStatus() {
252
963
  return {
253
964
  unlocked: this.isUnlocked(),
@@ -255,36 +966,24 @@ var FollowGateClient = class {
255
966
  completedActions: [...this.completedActions]
256
967
  };
257
968
  }
258
- /**
259
- * Get completed actions
260
- */
261
969
  getCompletedActions() {
262
970
  return [...this.completedActions];
263
971
  }
264
972
  // ============================================
265
973
  // Event System
266
974
  // ============================================
267
- /**
268
- * Register event listener
269
- */
270
975
  on(event, callback) {
271
976
  if (!this.listeners.has(event)) {
272
977
  this.listeners.set(event, /* @__PURE__ */ new Set());
273
978
  }
274
979
  this.listeners.get(event).add(callback);
275
980
  }
276
- /**
277
- * Remove event listener
278
- */
279
981
  off(event, callback) {
280
982
  this.listeners.get(event)?.delete(callback);
281
983
  }
282
984
  // ============================================
283
985
  // Private Methods
284
986
  // ============================================
285
- /**
286
- * Restore session from localStorage
287
- */
288
987
  restoreSession() {
289
988
  if (typeof localStorage === "undefined") return;
290
989
  const userJson = localStorage.getItem("followgate_user");
@@ -304,9 +1003,6 @@ var FollowGateClient = class {
304
1003
  }
305
1004
  }
306
1005
  }
307
- /**
308
- * Save completed actions to localStorage
309
- */
310
1006
  saveCompletedActions() {
311
1007
  if (typeof localStorage !== "undefined") {
312
1008
  localStorage.setItem(
@@ -315,9 +1011,6 @@ var FollowGateClient = class {
315
1011
  );
316
1012
  }
317
1013
  }
318
- /**
319
- * Build intent URL for platform
320
- */
321
1014
  buildIntentUrl(options) {
322
1015
  const { platform, action, target } = options;
323
1016
  switch (platform) {
@@ -376,9 +1069,6 @@ var FollowGateClient = class {
376
1069
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
377
1070
  }
378
1071
  }
379
- /**
380
- * Track analytics event
381
- */
382
1072
  async trackEvent(event, data) {
383
1073
  if (!this.config) return;
384
1074
  try {