@followgate/js 0.14.0 → 0.15.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/README.md CHANGED
@@ -108,6 +108,41 @@ FollowGate.on('unlocked', (data) => {
108
108
  });
109
109
  ```
110
110
 
111
+ ## Username Input (2 Methods)
112
+
113
+ The SDK needs the user's social media username to work. There are exactly 2 ways to provide it:
114
+
115
+ **Method 1: User enters username manually (default)**
116
+ If no username is passed, the modal shows a Welcome step where the user types their username.
117
+
118
+ ```typescript
119
+ FollowGate.init({
120
+ appId: 'your-app-id',
121
+ apiKey: 'fg_live_xxx',
122
+ twitter: { handle: 'your_handle' },
123
+ });
124
+
125
+ FollowGate.show(); // → Modal starts with username input step
126
+ ```
127
+
128
+ **Method 2: App passes username via code (skips Welcome step)**
129
+ If your app already knows the user's username (e.g. from your own auth system), pass it directly. The Welcome step is skipped entirely.
130
+
131
+ ```typescript
132
+ FollowGate.init({
133
+ appId: 'your-app-id',
134
+ apiKey: 'fg_live_xxx',
135
+ twitter: {
136
+ handle: 'your_handle',
137
+ username: 'their_x_username', // ← skips Welcome step
138
+ },
139
+ });
140
+
141
+ FollowGate.show(); // → Modal starts directly with Follow step
142
+ ```
143
+
144
+ > **Note:** No Twitter API calls are made in either case. The SDK uses intent URLs only.
145
+
111
146
  ## Configuration Options
112
147
 
113
148
  ```typescript
package/dist/index.d.mts CHANGED
@@ -29,6 +29,7 @@ interface FollowGateConfig {
29
29
  onComplete?: () => void;
30
30
  theme?: 'dark' | 'light';
31
31
  accentColor?: string;
32
+ forceShow?: boolean;
32
33
  }
33
34
  /**
34
35
  * SDK Error class with helpful messages
@@ -93,6 +94,7 @@ declare class FollowGateClient {
93
94
  private completedActions;
94
95
  private modalElement;
95
96
  private stylesInjected;
97
+ private currentStep;
96
98
  /**
97
99
  * Initialize the SDK
98
100
  */
@@ -103,7 +105,7 @@ declare class FollowGateClient {
103
105
  private fetchServerConfig;
104
106
  /**
105
107
  * Show the FollowGate modal
106
- * If user is already unlocked, calls onComplete immediately
108
+ * If user is already unlocked, calls onComplete immediately (unless forceShow is true)
107
109
  */
108
110
  show(): Promise<void>;
109
111
  /**
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ interface FollowGateConfig {
29
29
  onComplete?: () => void;
30
30
  theme?: 'dark' | 'light';
31
31
  accentColor?: string;
32
+ forceShow?: boolean;
32
33
  }
33
34
  /**
34
35
  * SDK Error class with helpful messages
@@ -93,6 +94,7 @@ declare class FollowGateClient {
93
94
  private completedActions;
94
95
  private modalElement;
95
96
  private stylesInjected;
97
+ private currentStep;
96
98
  /**
97
99
  * Initialize the SDK
98
100
  */
@@ -103,7 +105,7 @@ declare class FollowGateClient {
103
105
  private fetchServerConfig;
104
106
  /**
105
107
  * Show the FollowGate modal
106
- * If user is already unlocked, calls onComplete immediately
108
+ * If user is already unlocked, calls onComplete immediately (unless forceShow is true)
107
109
  */
108
110
  show(): Promise<void>;
109
111
  /**
package/dist/index.js CHANGED
@@ -503,6 +503,7 @@ var FollowGateClient = class {
503
503
  completedActions = [];
504
504
  modalElement = null;
505
505
  stylesInjected = false;
506
+ currentStep = "welcome";
506
507
  /**
507
508
  * Initialize the SDK
508
509
  */
@@ -598,22 +599,29 @@ var FollowGateClient = class {
598
599
  }
599
600
  /**
600
601
  * Show the FollowGate modal
601
- * If user is already unlocked, calls onComplete immediately
602
+ * If user is already unlocked, calls onComplete immediately (unless forceShow is true)
602
603
  */
603
604
  async show() {
604
605
  if (!this.config) {
605
606
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
606
607
  }
607
608
  if (this.isUnlocked()) {
608
- if (this.config.debug) {
609
- console.log(
609
+ if (this.config.forceShow) {
610
+ this.clearUnlockStatus();
611
+ if (this.config.debug) {
612
+ console.log(
613
+ "[FollowGate] forceShow enabled - cleared unlock status, showing modal"
614
+ );
615
+ }
616
+ } else {
617
+ console.warn(
610
618
  "[FollowGate] Modal skipped - user already unlocked.",
611
- "Call FollowGate.reset() to clear unlock status.",
619
+ "Use forceShow: true in init() to always show modal, or call FollowGate.reset() to clear.",
612
620
  this.config.userId ? `(userId: ${this.config.userId})` : "(no userId set)"
613
621
  );
622
+ this.config.onComplete?.();
623
+ return;
614
624
  }
615
- this.config.onComplete?.();
616
- return;
617
625
  }
618
626
  await this.fetchServerConfig();
619
627
  if (this.config.twitter?.username && !this.hasUsername()) {
@@ -625,6 +633,7 @@ var FollowGateClient = class {
625
633
  );
626
634
  }
627
635
  }
636
+ this.trackEvent("modal_opened");
628
637
  this.injectStyles();
629
638
  this.createModal();
630
639
  }
@@ -681,6 +690,7 @@ var FollowGateClient = class {
681
690
  document.body.appendChild(backdrop);
682
691
  this.modalElement = backdrop;
683
692
  document.getElementById("fg-close-btn")?.addEventListener("click", () => {
693
+ this.trackEvent("modal_closed", { step: this.currentStep });
684
694
  this.hide(true);
685
695
  });
686
696
  requestAnimationFrame(() => {
@@ -773,14 +783,16 @@ var FollowGateClient = class {
773
783
  renderUsernameStep() {
774
784
  const content = this.getContentElement();
775
785
  if (!content) return;
786
+ this.currentStep = "welcome";
787
+ this.trackEvent("step_viewed", { step: "welcome" });
776
788
  const handle = this.getTargetHandle();
777
789
  const hasRepost = this.shouldShowRepostStep();
778
790
  const allowSkip = this.serverConfig?.allowSkip ?? false;
779
791
  const welcomeTitle = this.serverConfig?.welcomeTitle || "Unlock Free Access";
780
792
  const welcomeMessage = this.serverConfig?.welcomeMessage || "Enter your X username to get started";
781
- let explanationText = handle ? `Complete a quick follow${hasRepost ? " & repost" : ""} on X for @${handle} to unlock access.` : `Complete a quick social action to unlock access.`;
793
+ let explanationText = handle ? `Follow @${handle} on X${hasRepost ? " and repost a post" : ""} to unlock.` : `Complete a quick action on X to unlock.`;
782
794
  if (allowSkip) {
783
- explanationText += ` You can skip if you prefer, but it helps support the developer.`;
795
+ explanationText += ` You can skip single steps, but it really helps the developer.`;
784
796
  }
785
797
  content.innerHTML = `
786
798
  <div class="fg-icon-box">
@@ -824,11 +836,14 @@ var FollowGateClient = class {
824
836
  handleUsernameSubmit(username) {
825
837
  const normalized = username.replace(/^@/, "");
826
838
  this.setUsername(normalized);
839
+ this.trackEvent("username_submitted", { username: normalized });
827
840
  this.renderFollowStep();
828
841
  }
829
842
  renderFollowStep() {
830
843
  const content = this.getContentElement();
831
844
  if (!content) return;
845
+ this.currentStep = "follow";
846
+ this.trackEvent("step_viewed", { step: "follow" });
832
847
  const handle = this.getTargetHandle();
833
848
  if (!handle) {
834
849
  console.error(
@@ -871,6 +886,7 @@ var FollowGateClient = class {
871
886
  });
872
887
  }
873
888
  handleSkipFollow() {
889
+ this.trackEvent("step_skipped", { step: "follow" });
874
890
  if (this.shouldShowRepostStep()) {
875
891
  this.renderRepostStep();
876
892
  } else {
@@ -952,6 +968,8 @@ var FollowGateClient = class {
952
968
  const content = this.getContentElement();
953
969
  const postId = this.getTargetPostUrl();
954
970
  if (!content || !postId) return;
971
+ this.currentStep = "repost";
972
+ this.trackEvent("step_viewed", { step: "repost" });
955
973
  content.innerHTML = `
956
974
  ${this.renderStepIndicator(2)}
957
975
  <div class="fg-icon-box fg-success">
@@ -978,6 +996,7 @@ var FollowGateClient = class {
978
996
  this.handleRepostClick();
979
997
  });
980
998
  document.getElementById("fg-skip-repost")?.addEventListener("click", () => {
999
+ this.trackEvent("step_skipped", { step: "repost" });
981
1000
  this.renderConfirmStep();
982
1001
  });
983
1002
  }
@@ -1049,6 +1068,8 @@ var FollowGateClient = class {
1049
1068
  renderConfirmStep() {
1050
1069
  const content = this.getContentElement();
1051
1070
  if (!content) return;
1071
+ this.currentStep = "confirm";
1072
+ this.trackEvent("step_viewed", { step: "confirm" });
1052
1073
  const username = this.currentUser?.username;
1053
1074
  const targetHandle = this.getTargetHandle();
1054
1075
  const postId = this.getTargetPostUrl();
@@ -1065,7 +1086,10 @@ var FollowGateClient = class {
1065
1086
  verifyText = "Verifying repost";
1066
1087
  }
1067
1088
  content.innerHTML = `
1068
- <h2 class="fg-title">${this.escapeHtml(successTitle)}</h2>
1089
+ <div style="display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 4px;">
1090
+ <span style="color: #4ade80; width: 28px; height: 28px;">${ICONS.check}</span>
1091
+ <h2 class="fg-title" style="margin: 0;">${this.escapeHtml(successTitle)}</h2>
1092
+ </div>
1069
1093
  ${successMessage ? `<p class="fg-subtitle" style="margin-bottom: 16px;">${this.escapeHtml(successMessage)}</p>` : ""}
1070
1094
  <div class="fg-verify-box">
1071
1095
  <div class="fg-verify-box-left">
@@ -1080,15 +1104,13 @@ var FollowGateClient = class {
1080
1104
  ` : ""}
1081
1105
  </div>
1082
1106
  <p class="fg-verify-hint">Verification may take some time</p>
1083
- <div class="fg-warning-box">
1084
- ${ICONS.warning}
1085
- <p><strong>Note:</strong> Access may be revoked if actions are not completed.</p>
1086
- </div>
1107
+ <p style="color: rgba(255,255,255,0.4); font-size: 11px; margin: 8px 0 16px; text-align: center;">Access may be revoked if actions are not completed.</p>
1087
1108
  <button class="fg-btn fg-btn-green" id="fg-finish-btn">
1088
1109
  ${ICONS.check}
1089
- Got it
1110
+ Got it \u2014 Continue
1090
1111
  </button>
1091
1112
  ${hasFollow || hasRepost ? `
1113
+ <p style="color: rgba(255,255,255,0.4); font-size: 11px; margin: 12px 0 6px; text-align: center;">Missed something? Open again:</p>
1092
1114
  <div class="fg-btn-row">
1093
1115
  ${hasFollow ? `
1094
1116
  <button class="fg-btn fg-btn-secondary" id="fg-redo-follow">
@@ -1543,8 +1565,37 @@ var FollowGateClient = class {
1543
1565
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
1544
1566
  }
1545
1567
  }
1546
- async trackEvent(event, data) {
1568
+ async trackEvent(event, data = {}) {
1547
1569
  if (!this.config) return;
1570
+ const payload = {
1571
+ event
1572
+ };
1573
+ if (data.platform) {
1574
+ payload.platform = data.platform.toUpperCase();
1575
+ }
1576
+ if (data.action) {
1577
+ payload.action = data.action.toUpperCase();
1578
+ }
1579
+ if (data.target) {
1580
+ payload.target = data.target;
1581
+ }
1582
+ if (data.username || this.currentUser?.username) {
1583
+ payload.username = data.username || this.currentUser?.username;
1584
+ }
1585
+ if (data.externalUserId) {
1586
+ payload.externalUserId = data.externalUserId;
1587
+ }
1588
+ const {
1589
+ platform: _p,
1590
+ action: _a,
1591
+ target: _t,
1592
+ username: _u,
1593
+ externalUserId: _e,
1594
+ ...rest
1595
+ } = data;
1596
+ if (Object.keys(rest).length > 0) {
1597
+ payload.metadata = rest;
1598
+ }
1548
1599
  try {
1549
1600
  await fetch(`${this.config.apiUrl}/api/v1/events`, {
1550
1601
  method: "POST",
@@ -1552,11 +1603,7 @@ var FollowGateClient = class {
1552
1603
  "Content-Type": "application/json",
1553
1604
  "X-API-Key": this.config.apiKey
1554
1605
  },
1555
- body: JSON.stringify({
1556
- event,
1557
- appId: this.config.appId,
1558
- ...data
1559
- })
1606
+ body: JSON.stringify(payload)
1560
1607
  });
1561
1608
  } catch (error) {
1562
1609
  if (this.config.debug) {
package/dist/index.mjs CHANGED
@@ -477,6 +477,7 @@ var FollowGateClient = class {
477
477
  completedActions = [];
478
478
  modalElement = null;
479
479
  stylesInjected = false;
480
+ currentStep = "welcome";
480
481
  /**
481
482
  * Initialize the SDK
482
483
  */
@@ -572,22 +573,29 @@ var FollowGateClient = class {
572
573
  }
573
574
  /**
574
575
  * Show the FollowGate modal
575
- * If user is already unlocked, calls onComplete immediately
576
+ * If user is already unlocked, calls onComplete immediately (unless forceShow is true)
576
577
  */
577
578
  async show() {
578
579
  if (!this.config) {
579
580
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
580
581
  }
581
582
  if (this.isUnlocked()) {
582
- if (this.config.debug) {
583
- console.log(
583
+ if (this.config.forceShow) {
584
+ this.clearUnlockStatus();
585
+ if (this.config.debug) {
586
+ console.log(
587
+ "[FollowGate] forceShow enabled - cleared unlock status, showing modal"
588
+ );
589
+ }
590
+ } else {
591
+ console.warn(
584
592
  "[FollowGate] Modal skipped - user already unlocked.",
585
- "Call FollowGate.reset() to clear unlock status.",
593
+ "Use forceShow: true in init() to always show modal, or call FollowGate.reset() to clear.",
586
594
  this.config.userId ? `(userId: ${this.config.userId})` : "(no userId set)"
587
595
  );
596
+ this.config.onComplete?.();
597
+ return;
588
598
  }
589
- this.config.onComplete?.();
590
- return;
591
599
  }
592
600
  await this.fetchServerConfig();
593
601
  if (this.config.twitter?.username && !this.hasUsername()) {
@@ -599,6 +607,7 @@ var FollowGateClient = class {
599
607
  );
600
608
  }
601
609
  }
610
+ this.trackEvent("modal_opened");
602
611
  this.injectStyles();
603
612
  this.createModal();
604
613
  }
@@ -655,6 +664,7 @@ var FollowGateClient = class {
655
664
  document.body.appendChild(backdrop);
656
665
  this.modalElement = backdrop;
657
666
  document.getElementById("fg-close-btn")?.addEventListener("click", () => {
667
+ this.trackEvent("modal_closed", { step: this.currentStep });
658
668
  this.hide(true);
659
669
  });
660
670
  requestAnimationFrame(() => {
@@ -747,14 +757,16 @@ var FollowGateClient = class {
747
757
  renderUsernameStep() {
748
758
  const content = this.getContentElement();
749
759
  if (!content) return;
760
+ this.currentStep = "welcome";
761
+ this.trackEvent("step_viewed", { step: "welcome" });
750
762
  const handle = this.getTargetHandle();
751
763
  const hasRepost = this.shouldShowRepostStep();
752
764
  const allowSkip = this.serverConfig?.allowSkip ?? false;
753
765
  const welcomeTitle = this.serverConfig?.welcomeTitle || "Unlock Free Access";
754
766
  const welcomeMessage = this.serverConfig?.welcomeMessage || "Enter your X username to get started";
755
- let explanationText = handle ? `Complete a quick follow${hasRepost ? " & repost" : ""} on X for @${handle} to unlock access.` : `Complete a quick social action to unlock access.`;
767
+ let explanationText = handle ? `Follow @${handle} on X${hasRepost ? " and repost a post" : ""} to unlock.` : `Complete a quick action on X to unlock.`;
756
768
  if (allowSkip) {
757
- explanationText += ` You can skip if you prefer, but it helps support the developer.`;
769
+ explanationText += ` You can skip single steps, but it really helps the developer.`;
758
770
  }
759
771
  content.innerHTML = `
760
772
  <div class="fg-icon-box">
@@ -798,11 +810,14 @@ var FollowGateClient = class {
798
810
  handleUsernameSubmit(username) {
799
811
  const normalized = username.replace(/^@/, "");
800
812
  this.setUsername(normalized);
813
+ this.trackEvent("username_submitted", { username: normalized });
801
814
  this.renderFollowStep();
802
815
  }
803
816
  renderFollowStep() {
804
817
  const content = this.getContentElement();
805
818
  if (!content) return;
819
+ this.currentStep = "follow";
820
+ this.trackEvent("step_viewed", { step: "follow" });
806
821
  const handle = this.getTargetHandle();
807
822
  if (!handle) {
808
823
  console.error(
@@ -845,6 +860,7 @@ var FollowGateClient = class {
845
860
  });
846
861
  }
847
862
  handleSkipFollow() {
863
+ this.trackEvent("step_skipped", { step: "follow" });
848
864
  if (this.shouldShowRepostStep()) {
849
865
  this.renderRepostStep();
850
866
  } else {
@@ -926,6 +942,8 @@ var FollowGateClient = class {
926
942
  const content = this.getContentElement();
927
943
  const postId = this.getTargetPostUrl();
928
944
  if (!content || !postId) return;
945
+ this.currentStep = "repost";
946
+ this.trackEvent("step_viewed", { step: "repost" });
929
947
  content.innerHTML = `
930
948
  ${this.renderStepIndicator(2)}
931
949
  <div class="fg-icon-box fg-success">
@@ -952,6 +970,7 @@ var FollowGateClient = class {
952
970
  this.handleRepostClick();
953
971
  });
954
972
  document.getElementById("fg-skip-repost")?.addEventListener("click", () => {
973
+ this.trackEvent("step_skipped", { step: "repost" });
955
974
  this.renderConfirmStep();
956
975
  });
957
976
  }
@@ -1023,6 +1042,8 @@ var FollowGateClient = class {
1023
1042
  renderConfirmStep() {
1024
1043
  const content = this.getContentElement();
1025
1044
  if (!content) return;
1045
+ this.currentStep = "confirm";
1046
+ this.trackEvent("step_viewed", { step: "confirm" });
1026
1047
  const username = this.currentUser?.username;
1027
1048
  const targetHandle = this.getTargetHandle();
1028
1049
  const postId = this.getTargetPostUrl();
@@ -1039,7 +1060,10 @@ var FollowGateClient = class {
1039
1060
  verifyText = "Verifying repost";
1040
1061
  }
1041
1062
  content.innerHTML = `
1042
- <h2 class="fg-title">${this.escapeHtml(successTitle)}</h2>
1063
+ <div style="display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 4px;">
1064
+ <span style="color: #4ade80; width: 28px; height: 28px;">${ICONS.check}</span>
1065
+ <h2 class="fg-title" style="margin: 0;">${this.escapeHtml(successTitle)}</h2>
1066
+ </div>
1043
1067
  ${successMessage ? `<p class="fg-subtitle" style="margin-bottom: 16px;">${this.escapeHtml(successMessage)}</p>` : ""}
1044
1068
  <div class="fg-verify-box">
1045
1069
  <div class="fg-verify-box-left">
@@ -1054,15 +1078,13 @@ var FollowGateClient = class {
1054
1078
  ` : ""}
1055
1079
  </div>
1056
1080
  <p class="fg-verify-hint">Verification may take some time</p>
1057
- <div class="fg-warning-box">
1058
- ${ICONS.warning}
1059
- <p><strong>Note:</strong> Access may be revoked if actions are not completed.</p>
1060
- </div>
1081
+ <p style="color: rgba(255,255,255,0.4); font-size: 11px; margin: 8px 0 16px; text-align: center;">Access may be revoked if actions are not completed.</p>
1061
1082
  <button class="fg-btn fg-btn-green" id="fg-finish-btn">
1062
1083
  ${ICONS.check}
1063
- Got it
1084
+ Got it \u2014 Continue
1064
1085
  </button>
1065
1086
  ${hasFollow || hasRepost ? `
1087
+ <p style="color: rgba(255,255,255,0.4); font-size: 11px; margin: 12px 0 6px; text-align: center;">Missed something? Open again:</p>
1066
1088
  <div class="fg-btn-row">
1067
1089
  ${hasFollow ? `
1068
1090
  <button class="fg-btn fg-btn-secondary" id="fg-redo-follow">
@@ -1517,8 +1539,37 @@ var FollowGateClient = class {
1517
1539
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
1518
1540
  }
1519
1541
  }
1520
- async trackEvent(event, data) {
1542
+ async trackEvent(event, data = {}) {
1521
1543
  if (!this.config) return;
1544
+ const payload = {
1545
+ event
1546
+ };
1547
+ if (data.platform) {
1548
+ payload.platform = data.platform.toUpperCase();
1549
+ }
1550
+ if (data.action) {
1551
+ payload.action = data.action.toUpperCase();
1552
+ }
1553
+ if (data.target) {
1554
+ payload.target = data.target;
1555
+ }
1556
+ if (data.username || this.currentUser?.username) {
1557
+ payload.username = data.username || this.currentUser?.username;
1558
+ }
1559
+ if (data.externalUserId) {
1560
+ payload.externalUserId = data.externalUserId;
1561
+ }
1562
+ const {
1563
+ platform: _p,
1564
+ action: _a,
1565
+ target: _t,
1566
+ username: _u,
1567
+ externalUserId: _e,
1568
+ ...rest
1569
+ } = data;
1570
+ if (Object.keys(rest).length > 0) {
1571
+ payload.metadata = rest;
1572
+ }
1522
1573
  try {
1523
1574
  await fetch(`${this.config.apiUrl}/api/v1/events`, {
1524
1575
  method: "POST",
@@ -1526,11 +1577,7 @@ var FollowGateClient = class {
1526
1577
  "Content-Type": "application/json",
1527
1578
  "X-API-Key": this.config.apiKey
1528
1579
  },
1529
- body: JSON.stringify({
1530
- event,
1531
- appId: this.config.appId,
1532
- ...data
1533
- })
1580
+ body: JSON.stringify(payload)
1534
1581
  });
1535
1582
  } catch (error) {
1536
1583
  if (this.config.debug) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@followgate/js",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "FollowGate SDK - Grow your audience with every download. Require social actions (follow, repost) before users can access your app.",
5
5
  "author": "FollowGate <hello@followgate.app>",
6
6
  "homepage": "https://followgate.app",