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