@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.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,808 @@ 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("--fg-accent", this.config.accentColor);
485
+ document.documentElement.style.setProperty("--fg-accent-hover", this.config.accentColor);
486
+ }
487
+ this.stylesInjected = true;
488
+ }
489
+ createModal() {
490
+ if (typeof document === "undefined") return;
491
+ const backdrop = document.createElement("div");
492
+ backdrop.className = "fg-modal-backdrop";
493
+ backdrop.innerHTML = `
494
+ <div class="fg-modal">
495
+ <div id="fg-content"></div>
496
+ <div class="fg-footer">
497
+ <p>Powered by <a href="https://followgate.app" target="_blank" rel="noopener">FollowGate</a></p>
498
+ </div>
499
+ </div>
500
+ `;
501
+ document.body.appendChild(backdrop);
502
+ this.modalElement = backdrop;
503
+ requestAnimationFrame(() => {
504
+ backdrop.classList.add("fg-visible");
505
+ });
506
+ if (this.hasUsername()) {
507
+ this.renderFollowStep();
508
+ } else {
509
+ this.renderUsernameStep();
510
+ }
511
+ }
512
+ getContentElement() {
513
+ return document.getElementById("fg-content");
514
+ }
515
+ renderUsernameStep() {
516
+ const content = this.getContentElement();
517
+ if (!content) return;
518
+ content.innerHTML = `
519
+ <div class="fg-icon-box">
520
+ ${ICONS.x}
521
+ </div>
522
+ <h2 class="fg-title">Unlock Free Access</h2>
523
+ <p class="fg-subtitle">Enter your X username to get started</p>
524
+ <div class="fg-input-wrapper">
525
+ <span class="fg-input-prefix">@</span>
526
+ <input type="text" class="fg-input" id="fg-username-input" placeholder="your_username" autofocus>
527
+ </div>
528
+ <button class="fg-btn fg-btn-primary" id="fg-username-submit" disabled>
529
+ Continue
530
+ </button>
531
+ <p class="fg-hint">No login required \u2013 just your username</p>
532
+ `;
533
+ const input = document.getElementById("fg-username-input");
534
+ const btn = document.getElementById("fg-username-submit");
535
+ input?.addEventListener("input", () => {
536
+ btn.disabled = !input.value.trim();
537
+ });
538
+ input?.addEventListener("keydown", (e) => {
539
+ if (e.key === "Enter" && input.value.trim()) {
540
+ this.handleUsernameSubmit(input.value.trim());
541
+ }
542
+ });
543
+ btn?.addEventListener("click", () => {
544
+ if (input?.value.trim()) {
545
+ this.handleUsernameSubmit(input.value.trim());
546
+ }
547
+ });
548
+ setTimeout(() => input?.focus(), 100);
549
+ }
550
+ handleUsernameSubmit(username) {
551
+ const normalized = username.replace(/^@/, "");
552
+ this.setUsername(normalized);
553
+ this.renderFollowStep();
554
+ }
555
+ renderFollowStep() {
556
+ const content = this.getContentElement();
557
+ if (!content || !this.config?.twitter) return;
558
+ const handle = this.config.twitter.handle;
559
+ const hasRepost = !!this.config.twitter.tweetId;
560
+ content.innerHTML = `
561
+ ${hasRepost ? this.renderStepIndicator(1) : ""}
562
+ <div class="fg-icon-box">
563
+ ${ICONS.x}
564
+ </div>
565
+ ${this.currentUser ? `
566
+ <div class="fg-user-badge">
567
+ <div class="fg-user-badge-dot"></div>
568
+ <span class="fg-user-badge-text">@${this.currentUser.username}</span>
569
+ </div>
570
+ ` : ""}
571
+ <h2 class="fg-title">${hasRepost ? "Step 1: Follow" : "Follow to Continue"}</h2>
572
+ <p class="fg-subtitle" id="fg-follow-subtitle">Follow @${handle} on X</p>
573
+ <div id="fg-follow-actions">
574
+ <button class="fg-btn fg-btn-dark" id="fg-follow-btn">
575
+ ${ICONS.x}
576
+ Follow @${handle}
577
+ </button>
578
+ </div>
579
+ <p class="fg-hint">A new window will open. Return here after following.</p>
580
+ `;
581
+ document.getElementById("fg-follow-btn")?.addEventListener("click", () => {
582
+ this.handleFollowClick();
583
+ });
584
+ }
585
+ handleFollowClick() {
586
+ if (!this.config?.twitter) return;
587
+ const handle = this.config.twitter.handle;
588
+ this.openIntent({
589
+ platform: "twitter",
590
+ action: "follow",
591
+ target: handle
592
+ });
593
+ this.showFollowConfirmation();
594
+ }
595
+ showFollowConfirmation() {
596
+ if (!this.config?.twitter) return;
597
+ const handle = this.config.twitter.handle;
598
+ const subtitle = document.getElementById("fg-follow-subtitle");
599
+ const actions = document.getElementById("fg-follow-actions");
600
+ if (subtitle) {
601
+ subtitle.textContent = `Did you follow @${handle}?`;
602
+ }
603
+ if (actions) {
604
+ let seconds = WAIT_TIME_SECONDS;
605
+ actions.innerHTML = `
606
+ <div class="fg-info-box">
607
+ <p>You can close the X window after following</p>
608
+ </div>
609
+ <button class="fg-btn fg-btn-green" id="fg-follow-confirm" disabled>
610
+ <span class="fg-spinner"></span>
611
+ Waiting... (${seconds}s)
612
+ </button>
613
+ <button class="fg-btn fg-btn-secondary" id="fg-follow-retry">
614
+ ${ICONS.x}
615
+ Open again
616
+ </button>
617
+ `;
618
+ const confirmBtn = document.getElementById("fg-follow-confirm");
619
+ const retryBtn = document.getElementById("fg-follow-retry");
620
+ const interval = setInterval(() => {
621
+ seconds--;
622
+ if (seconds <= 0) {
623
+ clearInterval(interval);
624
+ if (confirmBtn) {
625
+ confirmBtn.disabled = false;
626
+ confirmBtn.innerHTML = `${ICONS.check} Yes, I followed`;
627
+ }
628
+ } else if (confirmBtn) {
629
+ confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
630
+ }
631
+ }, 1e3);
632
+ confirmBtn?.addEventListener("click", () => {
633
+ this.handleFollowConfirm();
634
+ });
635
+ retryBtn?.addEventListener("click", () => {
636
+ this.handleFollowClick();
637
+ });
638
+ }
639
+ }
640
+ async handleFollowConfirm() {
641
+ if (!this.config?.twitter) return;
642
+ const handle = this.config.twitter.handle;
643
+ await this.complete({
644
+ platform: "twitter",
645
+ action: "follow",
646
+ target: handle
647
+ });
648
+ if (this.config.twitter.tweetId) {
649
+ this.renderRepostStep();
650
+ } else {
651
+ this.renderConfirmStep();
652
+ }
653
+ }
654
+ renderRepostStep() {
655
+ const content = this.getContentElement();
656
+ if (!content || !this.config?.twitter?.tweetId) return;
657
+ content.innerHTML = `
658
+ ${this.renderStepIndicator(2)}
659
+ <div class="fg-icon-box fg-success">
660
+ ${ICONS.repost}
661
+ </div>
662
+ ${this.currentUser ? `
663
+ <div class="fg-user-badge">
664
+ <div class="fg-user-badge-dot"></div>
665
+ <span class="fg-user-badge-text">@${this.currentUser.username}</span>
666
+ </div>
667
+ ` : ""}
668
+ <h2 class="fg-title">Step 2: Repost</h2>
669
+ <p class="fg-subtitle" id="fg-repost-subtitle">Repost the tweet to continue</p>
670
+ <div id="fg-repost-actions">
671
+ <button class="fg-btn fg-btn-green" id="fg-repost-btn">
672
+ ${ICONS.repost}
673
+ Repost tweet
674
+ </button>
675
+ </div>
676
+ <p class="fg-hint">A new window will open. Return here after reposting.</p>
677
+ `;
678
+ document.getElementById("fg-repost-btn")?.addEventListener("click", () => {
679
+ this.handleRepostClick();
680
+ });
681
+ }
682
+ handleRepostClick() {
683
+ if (!this.config?.twitter?.tweetId) return;
684
+ const tweetId = this.config.twitter.tweetId;
685
+ this.openIntent({
686
+ platform: "twitter",
687
+ action: "repost",
688
+ target: tweetId
689
+ });
690
+ this.showRepostConfirmation();
691
+ }
692
+ showRepostConfirmation() {
693
+ const subtitle = document.getElementById("fg-repost-subtitle");
694
+ const actions = document.getElementById("fg-repost-actions");
695
+ if (subtitle) {
696
+ subtitle.textContent = "Did you repost the tweet?";
697
+ }
698
+ if (actions) {
699
+ let seconds = WAIT_TIME_SECONDS;
700
+ actions.innerHTML = `
701
+ <div class="fg-info-box">
702
+ <p>You can close the X window after reposting</p>
703
+ </div>
704
+ <button class="fg-btn fg-btn-green" id="fg-repost-confirm" disabled>
705
+ <span class="fg-spinner"></span>
706
+ Waiting... (${seconds}s)
707
+ </button>
708
+ <button class="fg-btn fg-btn-secondary" id="fg-repost-retry">
709
+ ${ICONS.repost}
710
+ Open tweet again
711
+ </button>
712
+ `;
713
+ const confirmBtn = document.getElementById("fg-repost-confirm");
714
+ const retryBtn = document.getElementById("fg-repost-retry");
715
+ const interval = setInterval(() => {
716
+ seconds--;
717
+ if (seconds <= 0) {
718
+ clearInterval(interval);
719
+ if (confirmBtn) {
720
+ confirmBtn.disabled = false;
721
+ confirmBtn.innerHTML = `${ICONS.check} Yes, I reposted`;
722
+ }
723
+ } else if (confirmBtn) {
724
+ confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
725
+ }
726
+ }, 1e3);
727
+ confirmBtn?.addEventListener("click", () => {
728
+ this.handleRepostConfirm();
729
+ });
730
+ retryBtn?.addEventListener("click", () => {
731
+ this.handleRepostClick();
732
+ });
733
+ }
734
+ }
735
+ async handleRepostConfirm() {
736
+ if (!this.config?.twitter?.tweetId) return;
737
+ await this.complete({
738
+ platform: "twitter",
739
+ action: "repost",
740
+ target: this.config.twitter.tweetId
741
+ });
742
+ this.renderConfirmStep();
743
+ }
744
+ renderConfirmStep() {
745
+ const content = this.getContentElement();
746
+ if (!content) return;
747
+ content.innerHTML = `
748
+ <h2 class="fg-title">Almost done!</h2>
749
+ <div class="fg-warning-box">
750
+ ${ICONS.warning}
751
+ <p><strong>Note:</strong> Access may be revoked if actions are not completed.</p>
752
+ </div>
753
+ <button class="fg-btn fg-btn-green" id="fg-finish-btn">
754
+ ${ICONS.check}
755
+ Got it
756
+ </button>
757
+ ${this.config?.twitter ? `
758
+ <div class="fg-btn-row">
759
+ <button class="fg-btn fg-btn-secondary" id="fg-redo-follow">
760
+ ${ICONS.x}
761
+ Open follow
762
+ </button>
763
+ ${this.config.twitter.tweetId ? `
764
+ <button class="fg-btn fg-btn-secondary" id="fg-redo-repost">
765
+ ${ICONS.repost}
766
+ Open repost
767
+ </button>
768
+ ` : ""}
769
+ </div>
770
+ ` : ""}
771
+ `;
772
+ document.getElementById("fg-finish-btn")?.addEventListener("click", () => {
773
+ this.handleFinish();
774
+ });
775
+ document.getElementById("fg-redo-follow")?.addEventListener("click", () => {
776
+ if (this.config?.twitter) {
777
+ this.openIntent({
778
+ platform: "twitter",
779
+ action: "follow",
780
+ target: this.config.twitter.handle
781
+ });
782
+ }
783
+ });
784
+ document.getElementById("fg-redo-repost")?.addEventListener("click", () => {
785
+ if (this.config?.twitter?.tweetId) {
786
+ this.openIntent({
787
+ platform: "twitter",
788
+ action: "repost",
789
+ target: this.config.twitter.tweetId
790
+ });
791
+ }
792
+ });
793
+ }
794
+ async handleFinish() {
795
+ await this.unlock();
796
+ this.hide();
797
+ this.config?.onComplete?.();
798
+ }
799
+ renderStepIndicator(currentStep) {
800
+ return `
801
+ <div class="fg-step-indicator">
802
+ <div class="fg-step-dot ${currentStep > 1 ? "fg-done" : currentStep === 1 ? "fg-active" : "fg-pending"}">
803
+ ${currentStep > 1 ? "\u2713" : "1"}
804
+ </div>
805
+ <div class="fg-step-line ${currentStep > 1 ? "fg-done" : ""}"></div>
806
+ <div class="fg-step-dot ${currentStep > 2 ? "fg-done" : currentStep === 2 ? "fg-active" : "fg-pending"}">
807
+ ${currentStep > 2 ? "\u2713" : "2"}
808
+ </div>
809
+ </div>
810
+ `;
811
+ }
812
+ // ============================================
813
+ // Core SDK Methods
814
+ // ============================================
75
815
  /**
76
816
  * Set the user's social username
77
- * This is the main entry point - no OAuth needed!
78
817
  */
79
818
  setUsername(username, platform = "twitter") {
80
819
  if (!this.config) {
@@ -122,27 +861,15 @@ var FollowGateClient = class {
122
861
  // ============================================
123
862
  // Intent URL Methods
124
863
  // ============================================
125
- /**
126
- * Get follow intent URL for a platform
127
- */
128
864
  getFollowUrl(platform, target) {
129
865
  return this.buildIntentUrl({ platform, action: "follow", target });
130
866
  }
131
- /**
132
- * Get repost/retweet intent URL for a platform
133
- */
134
867
  getRepostUrl(platform, target) {
135
868
  return this.buildIntentUrl({ platform, action: "repost", target });
136
869
  }
137
- /**
138
- * Get like intent URL for a platform
139
- */
140
870
  getLikeUrl(platform, target) {
141
871
  return this.buildIntentUrl({ platform, action: "like", target });
142
872
  }
143
- /**
144
- * Open intent URL in new window
145
- */
146
873
  async openIntent(options) {
147
874
  if (!this.config) {
148
875
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
@@ -157,18 +884,12 @@ var FollowGateClient = class {
157
884
  // ============================================
158
885
  // Completion Methods
159
886
  // ============================================
160
- /**
161
- * Mark an action as completed (trust-first)
162
- * Call this when user confirms they did the action
163
- */
164
887
  async complete(options) {
165
888
  if (!this.config) {
166
889
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
167
890
  }
168
891
  if (!this.currentUser) {
169
- throw new Error(
170
- "[FollowGate] No username set. Call setUsername() first."
171
- );
892
+ throw new Error("[FollowGate] No username set. Call setUsername() first.");
172
893
  }
173
894
  const alreadyCompleted = this.completedActions.some(
174
895
  (a) => a.platform === options.platform && a.action === options.action && a.target === options.target
@@ -189,10 +910,6 @@ var FollowGateClient = class {
189
910
  console.log("[FollowGate] Action completed:", options);
190
911
  }
191
912
  }
192
- /**
193
- * Mark the gate as unlocked
194
- * Call this when all required actions are done
195
- */
196
913
  async unlock() {
197
914
  if (!this.config) {
198
915
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
@@ -212,16 +929,10 @@ var FollowGateClient = class {
212
929
  console.log("[FollowGate] Gate unlocked!");
213
930
  }
214
931
  }
215
- /**
216
- * Check if gate is unlocked
217
- */
218
932
  isUnlocked() {
219
933
  if (typeof localStorage === "undefined") return false;
220
934
  return localStorage.getItem("followgate_unlocked") === "true";
221
935
  }
222
- /**
223
- * Get unlock status with details
224
- */
225
936
  getUnlockStatus() {
226
937
  return {
227
938
  unlocked: this.isUnlocked(),
@@ -229,36 +940,24 @@ var FollowGateClient = class {
229
940
  completedActions: [...this.completedActions]
230
941
  };
231
942
  }
232
- /**
233
- * Get completed actions
234
- */
235
943
  getCompletedActions() {
236
944
  return [...this.completedActions];
237
945
  }
238
946
  // ============================================
239
947
  // Event System
240
948
  // ============================================
241
- /**
242
- * Register event listener
243
- */
244
949
  on(event, callback) {
245
950
  if (!this.listeners.has(event)) {
246
951
  this.listeners.set(event, /* @__PURE__ */ new Set());
247
952
  }
248
953
  this.listeners.get(event).add(callback);
249
954
  }
250
- /**
251
- * Remove event listener
252
- */
253
955
  off(event, callback) {
254
956
  this.listeners.get(event)?.delete(callback);
255
957
  }
256
958
  // ============================================
257
959
  // Private Methods
258
960
  // ============================================
259
- /**
260
- * Restore session from localStorage
261
- */
262
961
  restoreSession() {
263
962
  if (typeof localStorage === "undefined") return;
264
963
  const userJson = localStorage.getItem("followgate_user");
@@ -278,9 +977,6 @@ var FollowGateClient = class {
278
977
  }
279
978
  }
280
979
  }
281
- /**
282
- * Save completed actions to localStorage
283
- */
284
980
  saveCompletedActions() {
285
981
  if (typeof localStorage !== "undefined") {
286
982
  localStorage.setItem(
@@ -289,9 +985,6 @@ var FollowGateClient = class {
289
985
  );
290
986
  }
291
987
  }
292
- /**
293
- * Build intent URL for platform
294
- */
295
988
  buildIntentUrl(options) {
296
989
  const { platform, action, target } = options;
297
990
  switch (platform) {
@@ -350,9 +1043,6 @@ var FollowGateClient = class {
350
1043
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
351
1044
  }
352
1045
  }
353
- /**
354
- * Track analytics event
355
- */
356
1046
  async trackEvent(event, data) {
357
1047
  if (!this.config) return;
358
1048
  try {