@followgate/js 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,822 @@ 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(
511
+ "--fg-accent",
512
+ this.config.accentColor
513
+ );
514
+ document.documentElement.style.setProperty(
515
+ "--fg-accent-hover",
516
+ this.config.accentColor
517
+ );
518
+ }
519
+ this.stylesInjected = true;
520
+ }
521
+ createModal() {
522
+ if (typeof document === "undefined") return;
523
+ const backdrop = document.createElement("div");
524
+ backdrop.className = "fg-modal-backdrop";
525
+ backdrop.innerHTML = `
526
+ <div class="fg-modal">
527
+ <div id="fg-content"></div>
528
+ <div class="fg-footer">
529
+ <p>Powered by <a href="https://followgate.app" target="_blank" rel="noopener">FollowGate</a></p>
530
+ </div>
531
+ </div>
532
+ `;
533
+ document.body.appendChild(backdrop);
534
+ this.modalElement = backdrop;
535
+ requestAnimationFrame(() => {
536
+ backdrop.classList.add("fg-visible");
537
+ });
538
+ if (this.hasUsername()) {
539
+ this.renderFollowStep();
540
+ } else {
541
+ this.renderUsernameStep();
542
+ }
543
+ }
544
+ getContentElement() {
545
+ return document.getElementById("fg-content");
546
+ }
547
+ renderUsernameStep() {
548
+ const content = this.getContentElement();
549
+ if (!content) return;
550
+ content.innerHTML = `
551
+ <div class="fg-icon-box">
552
+ ${ICONS.x}
553
+ </div>
554
+ <h2 class="fg-title">Unlock Free Access</h2>
555
+ <p class="fg-subtitle">Enter your X username to get started</p>
556
+ <div class="fg-input-wrapper">
557
+ <span class="fg-input-prefix">@</span>
558
+ <input type="text" class="fg-input" id="fg-username-input" placeholder="your_username" autofocus>
559
+ </div>
560
+ <button class="fg-btn fg-btn-primary" id="fg-username-submit" disabled>
561
+ Continue
562
+ </button>
563
+ <p class="fg-hint">No login required \u2013 just your username</p>
564
+ `;
565
+ const input = document.getElementById(
566
+ "fg-username-input"
567
+ );
568
+ const btn = document.getElementById(
569
+ "fg-username-submit"
570
+ );
571
+ input?.addEventListener("input", () => {
572
+ btn.disabled = !input.value.trim();
573
+ });
574
+ input?.addEventListener("keydown", (e) => {
575
+ if (e.key === "Enter" && input.value.trim()) {
576
+ this.handleUsernameSubmit(input.value.trim());
577
+ }
578
+ });
579
+ btn?.addEventListener("click", () => {
580
+ if (input?.value.trim()) {
581
+ this.handleUsernameSubmit(input.value.trim());
582
+ }
583
+ });
584
+ setTimeout(() => input?.focus(), 100);
585
+ }
586
+ handleUsernameSubmit(username) {
587
+ const normalized = username.replace(/^@/, "");
588
+ this.setUsername(normalized);
589
+ this.renderFollowStep();
590
+ }
591
+ renderFollowStep() {
592
+ const content = this.getContentElement();
593
+ if (!content || !this.config?.twitter) return;
594
+ const handle = this.config.twitter.handle;
595
+ const hasRepost = !!this.config.twitter.tweetId;
596
+ content.innerHTML = `
597
+ ${hasRepost ? this.renderStepIndicator(1) : ""}
598
+ <div class="fg-icon-box">
599
+ ${ICONS.x}
600
+ </div>
601
+ ${this.currentUser ? `
602
+ <div class="fg-user-badge">
603
+ <div class="fg-user-badge-dot"></div>
604
+ <span class="fg-user-badge-text">@${this.currentUser.username}</span>
605
+ </div>
606
+ ` : ""}
607
+ <h2 class="fg-title">${hasRepost ? "Step 1: Follow" : "Follow to Continue"}</h2>
608
+ <p class="fg-subtitle" id="fg-follow-subtitle">Follow @${handle} on X</p>
609
+ <div id="fg-follow-actions">
610
+ <button class="fg-btn fg-btn-dark" id="fg-follow-btn">
611
+ ${ICONS.x}
612
+ Follow @${handle}
613
+ </button>
614
+ </div>
615
+ <p class="fg-hint">A new window will open. Return here after following.</p>
616
+ `;
617
+ document.getElementById("fg-follow-btn")?.addEventListener("click", () => {
618
+ this.handleFollowClick();
619
+ });
620
+ }
621
+ handleFollowClick() {
622
+ if (!this.config?.twitter) return;
623
+ const handle = this.config.twitter.handle;
624
+ this.openIntent({
625
+ platform: "twitter",
626
+ action: "follow",
627
+ target: handle
628
+ });
629
+ this.showFollowConfirmation();
630
+ }
631
+ showFollowConfirmation() {
632
+ if (!this.config?.twitter) return;
633
+ const handle = this.config.twitter.handle;
634
+ const subtitle = document.getElementById("fg-follow-subtitle");
635
+ const actions = document.getElementById("fg-follow-actions");
636
+ if (subtitle) {
637
+ subtitle.textContent = `Did you follow @${handle}?`;
638
+ }
639
+ if (actions) {
640
+ let seconds = WAIT_TIME_SECONDS;
641
+ actions.innerHTML = `
642
+ <div class="fg-info-box">
643
+ <p>You can close the X window after following</p>
644
+ </div>
645
+ <button class="fg-btn fg-btn-green" id="fg-follow-confirm" disabled>
646
+ <span class="fg-spinner"></span>
647
+ Waiting... (${seconds}s)
648
+ </button>
649
+ <button class="fg-btn fg-btn-secondary" id="fg-follow-retry">
650
+ ${ICONS.x}
651
+ Open again
652
+ </button>
653
+ `;
654
+ const confirmBtn = document.getElementById(
655
+ "fg-follow-confirm"
656
+ );
657
+ const retryBtn = document.getElementById("fg-follow-retry");
658
+ const interval = setInterval(() => {
659
+ seconds--;
660
+ if (seconds <= 0) {
661
+ clearInterval(interval);
662
+ if (confirmBtn) {
663
+ confirmBtn.disabled = false;
664
+ confirmBtn.innerHTML = `${ICONS.check} Yes, I followed`;
665
+ }
666
+ } else if (confirmBtn) {
667
+ confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
668
+ }
669
+ }, 1e3);
670
+ confirmBtn?.addEventListener("click", () => {
671
+ this.handleFollowConfirm();
672
+ });
673
+ retryBtn?.addEventListener("click", () => {
674
+ this.handleFollowClick();
675
+ });
676
+ }
677
+ }
678
+ async handleFollowConfirm() {
679
+ if (!this.config?.twitter) return;
680
+ const handle = this.config.twitter.handle;
681
+ await this.complete({
682
+ platform: "twitter",
683
+ action: "follow",
684
+ target: handle
685
+ });
686
+ if (this.config.twitter.tweetId) {
687
+ this.renderRepostStep();
688
+ } else {
689
+ this.renderConfirmStep();
690
+ }
691
+ }
692
+ renderRepostStep() {
693
+ const content = this.getContentElement();
694
+ if (!content || !this.config?.twitter?.tweetId) return;
695
+ content.innerHTML = `
696
+ ${this.renderStepIndicator(2)}
697
+ <div class="fg-icon-box fg-success">
698
+ ${ICONS.repost}
699
+ </div>
700
+ ${this.currentUser ? `
701
+ <div class="fg-user-badge">
702
+ <div class="fg-user-badge-dot"></div>
703
+ <span class="fg-user-badge-text">@${this.currentUser.username}</span>
704
+ </div>
705
+ ` : ""}
706
+ <h2 class="fg-title">Step 2: Repost</h2>
707
+ <p class="fg-subtitle" id="fg-repost-subtitle">Repost the tweet to continue</p>
708
+ <div id="fg-repost-actions">
709
+ <button class="fg-btn fg-btn-green" id="fg-repost-btn">
710
+ ${ICONS.repost}
711
+ Repost tweet
712
+ </button>
713
+ </div>
714
+ <p class="fg-hint">A new window will open. Return here after reposting.</p>
715
+ `;
716
+ document.getElementById("fg-repost-btn")?.addEventListener("click", () => {
717
+ this.handleRepostClick();
718
+ });
719
+ }
720
+ handleRepostClick() {
721
+ if (!this.config?.twitter?.tweetId) return;
722
+ const tweetId = this.config.twitter.tweetId;
723
+ this.openIntent({
724
+ platform: "twitter",
725
+ action: "repost",
726
+ target: tweetId
727
+ });
728
+ this.showRepostConfirmation();
729
+ }
730
+ showRepostConfirmation() {
731
+ const subtitle = document.getElementById("fg-repost-subtitle");
732
+ const actions = document.getElementById("fg-repost-actions");
733
+ if (subtitle) {
734
+ subtitle.textContent = "Did you repost the tweet?";
735
+ }
736
+ if (actions) {
737
+ let seconds = WAIT_TIME_SECONDS;
738
+ actions.innerHTML = `
739
+ <div class="fg-info-box">
740
+ <p>You can close the X window after reposting</p>
741
+ </div>
742
+ <button class="fg-btn fg-btn-green" id="fg-repost-confirm" disabled>
743
+ <span class="fg-spinner"></span>
744
+ Waiting... (${seconds}s)
745
+ </button>
746
+ <button class="fg-btn fg-btn-secondary" id="fg-repost-retry">
747
+ ${ICONS.repost}
748
+ Open tweet again
749
+ </button>
750
+ `;
751
+ const confirmBtn = document.getElementById(
752
+ "fg-repost-confirm"
753
+ );
754
+ const retryBtn = document.getElementById("fg-repost-retry");
755
+ const interval = setInterval(() => {
756
+ seconds--;
757
+ if (seconds <= 0) {
758
+ clearInterval(interval);
759
+ if (confirmBtn) {
760
+ confirmBtn.disabled = false;
761
+ confirmBtn.innerHTML = `${ICONS.check} Yes, I reposted`;
762
+ }
763
+ } else if (confirmBtn) {
764
+ confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
765
+ }
766
+ }, 1e3);
767
+ confirmBtn?.addEventListener("click", () => {
768
+ this.handleRepostConfirm();
769
+ });
770
+ retryBtn?.addEventListener("click", () => {
771
+ this.handleRepostClick();
772
+ });
773
+ }
774
+ }
775
+ async handleRepostConfirm() {
776
+ if (!this.config?.twitter?.tweetId) return;
777
+ await this.complete({
778
+ platform: "twitter",
779
+ action: "repost",
780
+ target: this.config.twitter.tweetId
781
+ });
782
+ this.renderConfirmStep();
783
+ }
784
+ renderConfirmStep() {
785
+ const content = this.getContentElement();
786
+ if (!content) return;
787
+ content.innerHTML = `
788
+ <h2 class="fg-title">Almost done!</h2>
789
+ <div class="fg-warning-box">
790
+ ${ICONS.warning}
791
+ <p><strong>Note:</strong> Access may be revoked if actions are not completed.</p>
792
+ </div>
793
+ <button class="fg-btn fg-btn-green" id="fg-finish-btn">
794
+ ${ICONS.check}
795
+ Got it
796
+ </button>
797
+ ${this.config?.twitter ? `
798
+ <div class="fg-btn-row">
799
+ <button class="fg-btn fg-btn-secondary" id="fg-redo-follow">
800
+ ${ICONS.x}
801
+ Open follow
802
+ </button>
803
+ ${this.config.twitter.tweetId ? `
804
+ <button class="fg-btn fg-btn-secondary" id="fg-redo-repost">
805
+ ${ICONS.repost}
806
+ Open repost
807
+ </button>
808
+ ` : ""}
809
+ </div>
810
+ ` : ""}
811
+ `;
812
+ document.getElementById("fg-finish-btn")?.addEventListener("click", () => {
813
+ this.handleFinish();
814
+ });
815
+ document.getElementById("fg-redo-follow")?.addEventListener("click", () => {
816
+ if (this.config?.twitter) {
817
+ this.openIntent({
818
+ platform: "twitter",
819
+ action: "follow",
820
+ target: this.config.twitter.handle
821
+ });
822
+ }
823
+ });
824
+ document.getElementById("fg-redo-repost")?.addEventListener("click", () => {
825
+ if (this.config?.twitter?.tweetId) {
826
+ this.openIntent({
827
+ platform: "twitter",
828
+ action: "repost",
829
+ target: this.config.twitter.tweetId
830
+ });
831
+ }
832
+ });
833
+ }
834
+ async handleFinish() {
835
+ await this.unlock();
836
+ this.hide();
837
+ this.config?.onComplete?.();
838
+ }
839
+ renderStepIndicator(currentStep) {
840
+ return `
841
+ <div class="fg-step-indicator">
842
+ <div class="fg-step-dot ${currentStep > 1 ? "fg-done" : currentStep === 1 ? "fg-active" : "fg-pending"}">
843
+ ${currentStep > 1 ? "\u2713" : "1"}
844
+ </div>
845
+ <div class="fg-step-line ${currentStep > 1 ? "fg-done" : ""}"></div>
846
+ <div class="fg-step-dot ${currentStep > 2 ? "fg-done" : currentStep === 2 ? "fg-active" : "fg-pending"}">
847
+ ${currentStep > 2 ? "\u2713" : "2"}
848
+ </div>
849
+ </div>
850
+ `;
851
+ }
852
+ // ============================================
853
+ // Core SDK Methods
854
+ // ============================================
101
855
  /**
102
856
  * Set the user's social username
103
- * This is the main entry point - no OAuth needed!
104
857
  */
105
858
  setUsername(username, platform = "twitter") {
106
859
  if (!this.config) {
@@ -112,7 +865,7 @@ var FollowGateClient = class {
112
865
  platform
113
866
  };
114
867
  if (typeof localStorage !== "undefined") {
115
- localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
868
+ localStorage.setItem(this.getStorageKey("followgate_user"), JSON.stringify(this.currentUser));
116
869
  }
117
870
  if (this.config.debug) {
118
871
  console.log("[FollowGate] Username set:", normalizedUsername);
@@ -137,9 +890,9 @@ var FollowGateClient = class {
137
890
  this.currentUser = null;
138
891
  this.completedActions = [];
139
892
  if (typeof localStorage !== "undefined") {
140
- localStorage.removeItem("followgate_user");
141
- localStorage.removeItem("followgate_actions");
142
- localStorage.removeItem("followgate_unlocked");
893
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
894
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
895
+ localStorage.removeItem(this.getStorageKey("followgate_unlocked"));
143
896
  }
144
897
  if (this.config?.debug) {
145
898
  console.log("[FollowGate] Session reset");
@@ -148,27 +901,15 @@ var FollowGateClient = class {
148
901
  // ============================================
149
902
  // Intent URL Methods
150
903
  // ============================================
151
- /**
152
- * Get follow intent URL for a platform
153
- */
154
904
  getFollowUrl(platform, target) {
155
905
  return this.buildIntentUrl({ platform, action: "follow", target });
156
906
  }
157
- /**
158
- * Get repost/retweet intent URL for a platform
159
- */
160
907
  getRepostUrl(platform, target) {
161
908
  return this.buildIntentUrl({ platform, action: "repost", target });
162
909
  }
163
- /**
164
- * Get like intent URL for a platform
165
- */
166
910
  getLikeUrl(platform, target) {
167
911
  return this.buildIntentUrl({ platform, action: "like", target });
168
912
  }
169
- /**
170
- * Open intent URL in new window
171
- */
172
913
  async openIntent(options) {
173
914
  if (!this.config) {
174
915
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
@@ -183,10 +924,6 @@ var FollowGateClient = class {
183
924
  // ============================================
184
925
  // Completion Methods
185
926
  // ============================================
186
- /**
187
- * Mark an action as completed (trust-first)
188
- * Call this when user confirms they did the action
189
- */
190
927
  async complete(options) {
191
928
  if (!this.config) {
192
929
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
@@ -215,16 +952,12 @@ var FollowGateClient = class {
215
952
  console.log("[FollowGate] Action completed:", options);
216
953
  }
217
954
  }
218
- /**
219
- * Mark the gate as unlocked
220
- * Call this when all required actions are done
221
- */
222
955
  async unlock() {
223
956
  if (!this.config) {
224
957
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
225
958
  }
226
959
  if (typeof localStorage !== "undefined") {
227
- localStorage.setItem("followgate_unlocked", "true");
960
+ localStorage.setItem(this.getStorageKey("followgate_unlocked"), "true");
228
961
  }
229
962
  await this.trackEvent("gate_unlocked", {
230
963
  username: this.currentUser?.username,
@@ -238,16 +971,10 @@ var FollowGateClient = class {
238
971
  console.log("[FollowGate] Gate unlocked!");
239
972
  }
240
973
  }
241
- /**
242
- * Check if gate is unlocked
243
- */
244
974
  isUnlocked() {
245
975
  if (typeof localStorage === "undefined") return false;
246
- return localStorage.getItem("followgate_unlocked") === "true";
976
+ return localStorage.getItem(this.getStorageKey("followgate_unlocked")) === "true";
247
977
  }
248
- /**
249
- * Get unlock status with details
250
- */
251
978
  getUnlockStatus() {
252
979
  return {
253
980
  unlocked: this.isUnlocked(),
@@ -255,27 +982,18 @@ var FollowGateClient = class {
255
982
  completedActions: [...this.completedActions]
256
983
  };
257
984
  }
258
- /**
259
- * Get completed actions
260
- */
261
985
  getCompletedActions() {
262
986
  return [...this.completedActions];
263
987
  }
264
988
  // ============================================
265
989
  // Event System
266
990
  // ============================================
267
- /**
268
- * Register event listener
269
- */
270
991
  on(event, callback) {
271
992
  if (!this.listeners.has(event)) {
272
993
  this.listeners.set(event, /* @__PURE__ */ new Set());
273
994
  }
274
995
  this.listeners.get(event).add(callback);
275
996
  }
276
- /**
277
- * Remove event listener
278
- */
279
997
  off(event, callback) {
280
998
  this.listeners.get(event)?.delete(callback);
281
999
  }
@@ -283,41 +1001,42 @@ var FollowGateClient = class {
283
1001
  // Private Methods
284
1002
  // ============================================
285
1003
  /**
286
- * Restore session from localStorage
1004
+ * Get storage key with optional userId suffix
1005
+ * This ensures unlock status is per-user, not per-browser
287
1006
  */
1007
+ getStorageKey(base) {
1008
+ if (this.config?.userId) {
1009
+ return `${base}_${this.config.userId}`;
1010
+ }
1011
+ return base;
1012
+ }
288
1013
  restoreSession() {
289
1014
  if (typeof localStorage === "undefined") return;
290
- const userJson = localStorage.getItem("followgate_user");
1015
+ const userJson = localStorage.getItem(this.getStorageKey("followgate_user"));
291
1016
  if (userJson) {
292
1017
  try {
293
1018
  this.currentUser = JSON.parse(userJson);
294
1019
  } catch {
295
- localStorage.removeItem("followgate_user");
1020
+ localStorage.removeItem(this.getStorageKey("followgate_user"));
296
1021
  }
297
1022
  }
298
- const actionsJson = localStorage.getItem("followgate_actions");
1023
+ const actionsJson = localStorage.getItem(this.getStorageKey("followgate_actions"));
299
1024
  if (actionsJson) {
300
1025
  try {
301
1026
  this.completedActions = JSON.parse(actionsJson);
302
1027
  } catch {
303
- localStorage.removeItem("followgate_actions");
1028
+ localStorage.removeItem(this.getStorageKey("followgate_actions"));
304
1029
  }
305
1030
  }
306
1031
  }
307
- /**
308
- * Save completed actions to localStorage
309
- */
310
1032
  saveCompletedActions() {
311
1033
  if (typeof localStorage !== "undefined") {
312
1034
  localStorage.setItem(
313
- "followgate_actions",
1035
+ this.getStorageKey("followgate_actions"),
314
1036
  JSON.stringify(this.completedActions)
315
1037
  );
316
1038
  }
317
1039
  }
318
- /**
319
- * Build intent URL for platform
320
- */
321
1040
  buildIntentUrl(options) {
322
1041
  const { platform, action, target } = options;
323
1042
  switch (platform) {
@@ -376,9 +1095,6 @@ var FollowGateClient = class {
376
1095
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
377
1096
  }
378
1097
  }
379
- /**
380
- * Track analytics event
381
- */
382
1098
  async trackEvent(event, data) {
383
1099
  if (!this.config) return;
384
1100
  try {