@followgate/js 0.3.0 → 0.5.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,276 +1,243 @@
1
1
  // src/index.ts
2
2
  var DEFAULT_API_URL = "https://api.followgate.app";
3
+ var FollowGateError = class extends Error {
4
+ constructor(message, code, hint) {
5
+ super(message);
6
+ this.code = code;
7
+ this.hint = hint;
8
+ this.name = "FollowGateError";
9
+ }
10
+ };
11
+ function isValidApiKeyFormat(apiKey) {
12
+ return /^fg_(live|test)_[a-zA-Z0-9_-]+$/.test(apiKey);
13
+ }
3
14
  var FollowGateClient = class {
4
15
  config = null;
5
16
  listeners = /* @__PURE__ */ new Map();
6
17
  currentUser = null;
7
- authToken = null;
8
- pendingUsername = null;
18
+ completedActions = [];
9
19
  /**
10
20
  * Initialize the SDK
21
+ * @throws {FollowGateError} If configuration is invalid
11
22
  */
12
23
  init(config) {
24
+ if (!config.appId || typeof config.appId !== "string") {
25
+ throw new FollowGateError(
26
+ "[FollowGate] Missing or invalid appId",
27
+ "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."
29
+ );
30
+ }
31
+ if (config.appId.trim() === "" || config.appId === "undefined") {
32
+ throw new FollowGateError(
33
+ "[FollowGate] appId is empty or undefined",
34
+ "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."
36
+ );
37
+ }
38
+ if (!config.apiKey || typeof config.apiKey !== "string") {
39
+ throw new FollowGateError(
40
+ "[FollowGate] Missing or invalid apiKey",
41
+ "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."
43
+ );
44
+ }
45
+ if (config.apiKey.trim() === "" || config.apiKey === "undefined") {
46
+ throw new FollowGateError(
47
+ "[FollowGate] apiKey is empty or undefined",
48
+ "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."
50
+ );
51
+ }
52
+ if (!isValidApiKeyFormat(config.apiKey)) {
53
+ throw new FollowGateError(
54
+ `[FollowGate] Invalid API key format: "${config.apiKey.substring(0, 10)}..."`,
55
+ "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'
57
+ );
58
+ }
13
59
  this.config = {
14
60
  ...config,
15
61
  apiUrl: config.apiUrl || DEFAULT_API_URL
16
62
  };
17
- this.handleAuthCallback();
18
63
  this.restoreSession();
19
64
  if (config.debug) {
20
65
  console.log("[FollowGate] Initialized with appId:", config.appId);
66
+ console.log(
67
+ "[FollowGate] API Key:",
68
+ config.apiKey.substring(0, 12) + "..."
69
+ );
21
70
  if (this.currentUser) {
22
71
  console.log("[FollowGate] Restored user:", this.currentUser.username);
23
72
  }
24
73
  }
25
74
  }
26
75
  /**
27
- * Authenticate user via Twitter OAuth (handled by FollowGate)
28
- * This identifies WHO is completing the social actions.
76
+ * Set the user's social username
77
+ * This is the main entry point - no OAuth needed!
29
78
  */
30
- authenticate(options = {}) {
79
+ setUsername(username, platform = "twitter") {
31
80
  if (!this.config) {
32
81
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
33
82
  }
34
- const redirectUri = options.redirectUri || window.location.href.split("?")[0];
35
- const authUrl = `${this.config.apiUrl}/api/v1/auth/twitter?app_id=${encodeURIComponent(this.config.appId)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
36
- if (this.config.debug) {
37
- console.log("[FollowGate] Starting auth flow:", authUrl);
83
+ const normalizedUsername = username.startsWith("@") ? username.slice(1) : username;
84
+ this.currentUser = {
85
+ username: normalizedUsername,
86
+ platform
87
+ };
88
+ if (typeof localStorage !== "undefined") {
89
+ localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
38
90
  }
39
- if (options.popup) {
40
- const popup = window.open(
41
- authUrl,
42
- "followgate_auth",
43
- "width=600,height=700"
44
- );
45
- if (!popup) {
46
- this.emit("error", { message: "Popup blocked" });
47
- }
48
- } else {
49
- window.location.href = authUrl;
91
+ if (this.config.debug) {
92
+ console.log("[FollowGate] Username set:", normalizedUsername);
50
93
  }
51
94
  }
52
95
  /**
53
- * Get current authenticated user
96
+ * Get current user
54
97
  */
55
98
  getUser() {
56
99
  return this.currentUser;
57
100
  }
58
101
  /**
59
- * Check if user is authenticated
102
+ * Check if username is set
60
103
  */
61
- isAuthenticated() {
62
- return this.currentUser !== null && this.authToken !== null;
104
+ hasUsername() {
105
+ return this.currentUser !== null;
63
106
  }
64
107
  /**
65
- * Logout - clear stored session
108
+ * Clear stored session
66
109
  */
67
- logout() {
110
+ reset() {
68
111
  this.currentUser = null;
69
- this.authToken = null;
70
- this.pendingUsername = null;
112
+ this.completedActions = [];
71
113
  if (typeof localStorage !== "undefined") {
72
- localStorage.removeItem("followgate_token");
73
114
  localStorage.removeItem("followgate_user");
74
- localStorage.removeItem("followgate_pending_username");
115
+ localStorage.removeItem("followgate_actions");
116
+ localStorage.removeItem("followgate_unlocked");
75
117
  }
76
118
  if (this.config?.debug) {
77
- console.log("[FollowGate] User logged out");
119
+ console.log("[FollowGate] Session reset");
78
120
  }
79
121
  }
122
+ // ============================================
123
+ // Intent URL Methods
124
+ // ============================================
80
125
  /**
81
- * Check if username input is needed (Twitter Free Tier limitation)
126
+ * Get follow intent URL for a platform
82
127
  */
83
- needsUsernameInput() {
84
- return this.pendingUsername !== null;
128
+ getFollowUrl(platform, target) {
129
+ return this.buildIntentUrl({ platform, action: "follow", target });
85
130
  }
86
131
  /**
87
- * Set username manually (when needsUsernameInput() returns true)
132
+ * Get repost/retweet intent URL for a platform
88
133
  */
89
- setUsername(username) {
90
- if (!this.pendingUsername) {
91
- throw new Error(
92
- "[FollowGate] No pending username state. User is either not authenticated or username is already set."
93
- );
94
- }
95
- const normalizedUsername = username.startsWith("@") ? username.slice(1) : username;
96
- this.currentUser = {
97
- userId: "user_input",
98
- username: normalizedUsername,
99
- platform: "twitter"
100
- };
101
- this.authToken = this.pendingUsername.token;
102
- if (typeof localStorage !== "undefined") {
103
- localStorage.setItem("followgate_token", this.authToken);
104
- localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
105
- localStorage.removeItem("followgate_pending_username");
106
- }
107
- this.pendingUsername = null;
108
- this.emit("authenticated", this.currentUser);
109
- if (this.config?.debug) {
110
- console.log("[FollowGate] Username set manually:", normalizedUsername);
111
- }
134
+ getRepostUrl(platform, target) {
135
+ return this.buildIntentUrl({ platform, action: "repost", target });
112
136
  }
113
137
  /**
114
- * Handle auth callback from URL params
138
+ * Get like intent URL for a platform
115
139
  */
116
- handleAuthCallback() {
117
- if (typeof window === "undefined") return;
118
- const params = new URLSearchParams(window.location.search);
119
- const token = params.get("followgate_token");
120
- const username = params.get("followgate_user");
121
- const needsUsername = params.get("followgate_needs_username") === "true";
122
- if (token && needsUsername) {
123
- this.pendingUsername = {
124
- needsUsername: true,
125
- token
126
- };
127
- if (typeof localStorage !== "undefined") {
128
- localStorage.setItem(
129
- "followgate_pending_username",
130
- JSON.stringify(this.pendingUsername)
131
- );
132
- }
133
- const url = new URL(window.location.href);
134
- url.searchParams.delete("followgate_token");
135
- url.searchParams.delete("followgate_needs_username");
136
- window.history.replaceState({}, "", url.toString());
137
- if (this.config?.debug) {
138
- console.log("[FollowGate] OAuth successful, username input needed");
139
- }
140
- } else if (token && username) {
141
- this.authToken = token;
142
- this.currentUser = {
143
- userId: "",
144
- // Will be set from token verification
145
- username,
146
- platform: "twitter"
147
- };
148
- if (typeof localStorage !== "undefined") {
149
- localStorage.setItem("followgate_token", token);
150
- localStorage.setItem(
151
- "followgate_user",
152
- JSON.stringify(this.currentUser)
153
- );
154
- }
155
- const url = new URL(window.location.href);
156
- url.searchParams.delete("followgate_token");
157
- url.searchParams.delete("followgate_user");
158
- window.history.replaceState({}, "", url.toString());
159
- this.emit("authenticated", this.currentUser);
160
- if (this.config?.debug) {
161
- console.log("[FollowGate] User authenticated:", username);
162
- }
163
- }
140
+ getLikeUrl(platform, target) {
141
+ return this.buildIntentUrl({ platform, action: "like", target });
164
142
  }
165
143
  /**
166
- * Restore session from localStorage
144
+ * Open intent URL in new window
167
145
  */
168
- restoreSession() {
169
- if (typeof localStorage === "undefined") return;
170
- const pendingJson = localStorage.getItem("followgate_pending_username");
171
- if (pendingJson) {
172
- try {
173
- this.pendingUsername = JSON.parse(pendingJson);
174
- if (this.config?.debug) {
175
- console.log("[FollowGate] Restored pending username state");
176
- }
177
- return;
178
- } catch {
179
- localStorage.removeItem("followgate_pending_username");
180
- }
146
+ async openIntent(options) {
147
+ if (!this.config) {
148
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
181
149
  }
182
- const token = localStorage.getItem("followgate_token");
183
- const userJson = localStorage.getItem("followgate_user");
184
- if (token && userJson) {
185
- try {
186
- this.authToken = token;
187
- this.currentUser = JSON.parse(userJson);
188
- } catch {
189
- localStorage.removeItem("followgate_token");
190
- localStorage.removeItem("followgate_user");
191
- }
150
+ const url = this.buildIntentUrl(options);
151
+ if (this.config.debug) {
152
+ console.log("[FollowGate] Opening intent:", url);
192
153
  }
154
+ await this.trackEvent("intent_opened", { ...options });
155
+ window.open(url, "_blank", "width=600,height=700");
193
156
  }
157
+ // ============================================
158
+ // Completion Methods
159
+ // ============================================
194
160
  /**
195
- * Open social action popup
161
+ * Mark an action as completed (trust-first)
162
+ * Call this when user confirms they did the action
196
163
  */
197
- async open(options) {
164
+ async complete(options) {
198
165
  if (!this.config) {
199
166
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
200
167
  }
201
- const url = this.buildIntentUrl(options);
202
- if (this.config.debug) {
203
- console.log("[FollowGate] Opening:", url);
168
+ if (!this.currentUser) {
169
+ throw new Error(
170
+ "[FollowGate] No username set. Call setUsername() first."
171
+ );
204
172
  }
205
- await this.trackEvent("gate_opened", options);
206
- const popup = window.open(url, "_blank", "width=600,height=700");
207
- if (!popup) {
208
- this.emit("error", { message: "Popup blocked" });
209
- return;
173
+ const alreadyCompleted = this.completedActions.some(
174
+ (a) => a.platform === options.platform && a.action === options.action && a.target === options.target
175
+ );
176
+ if (!alreadyCompleted) {
177
+ this.completedActions.push(options);
178
+ this.saveCompletedActions();
210
179
  }
211
- await this.trackEvent("action_clicked", options);
180
+ await this.trackEvent("action_completed", {
181
+ ...options,
182
+ username: this.currentUser.username
183
+ });
212
184
  this.emit("complete", {
213
- platform: options.platform,
214
- action: options.action,
215
- target: options.target
185
+ ...options,
186
+ username: this.currentUser.username
216
187
  });
188
+ if (this.config.debug) {
189
+ console.log("[FollowGate] Action completed:", options);
190
+ }
217
191
  }
218
192
  /**
219
- * Verify follow status (for Pro/Business tiers with OAuth)
193
+ * Mark the gate as unlocked
194
+ * Call this when all required actions are done
220
195
  */
221
- async verify(options) {
196
+ async unlock() {
222
197
  if (!this.config) {
223
198
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
224
199
  }
225
- try {
226
- const response = await fetch(`${this.config.apiUrl}/api/v1/verify`, {
227
- method: "POST",
228
- headers: {
229
- "Content-Type": "application/json",
230
- "X-API-Key": this.config.apiKey
231
- },
232
- body: JSON.stringify({
233
- platform: options.platform,
234
- action: options.action,
235
- target: options.target,
236
- externalUserId: options.userId
237
- })
238
- });
239
- const data = await response.json();
240
- return data.success && data.data?.verified === true;
241
- } catch (error) {
242
- if (this.config.debug) {
243
- console.error("[FollowGate] Verification error:", error);
244
- }
245
- return false;
200
+ if (typeof localStorage !== "undefined") {
201
+ localStorage.setItem("followgate_unlocked", "true");
202
+ }
203
+ await this.trackEvent("gate_unlocked", {
204
+ username: this.currentUser?.username,
205
+ actions: this.completedActions
206
+ });
207
+ this.emit("unlocked", {
208
+ username: this.currentUser?.username,
209
+ actions: this.completedActions
210
+ });
211
+ if (this.config.debug) {
212
+ console.log("[FollowGate] Gate unlocked!");
246
213
  }
247
214
  }
248
215
  /**
249
- * Track analytics event
216
+ * Check if gate is unlocked
250
217
  */
251
- async trackEvent(event, options) {
252
- if (!this.config) return;
253
- try {
254
- await fetch(`${this.config.apiUrl}/api/v1/events`, {
255
- method: "POST",
256
- headers: {
257
- "Content-Type": "application/json",
258
- "X-API-Key": this.config.apiKey
259
- },
260
- body: JSON.stringify({
261
- event,
262
- platform: options.platform,
263
- action: options.action,
264
- target: options.target,
265
- externalUserId: options.userId
266
- })
267
- });
268
- } catch (error) {
269
- if (this.config.debug) {
270
- console.warn("[FollowGate] Failed to track event:", error);
271
- }
272
- }
218
+ isUnlocked() {
219
+ if (typeof localStorage === "undefined") return false;
220
+ return localStorage.getItem("followgate_unlocked") === "true";
273
221
  }
222
+ /**
223
+ * Get unlock status with details
224
+ */
225
+ getUnlockStatus() {
226
+ return {
227
+ unlocked: this.isUnlocked(),
228
+ username: this.currentUser?.username,
229
+ completedActions: [...this.completedActions]
230
+ };
231
+ }
232
+ /**
233
+ * Get completed actions
234
+ */
235
+ getCompletedActions() {
236
+ return [...this.completedActions];
237
+ }
238
+ // ============================================
239
+ // Event System
240
+ // ============================================
274
241
  /**
275
242
  * Register event listener
276
243
  */
@@ -286,6 +253,42 @@ var FollowGateClient = class {
286
253
  off(event, callback) {
287
254
  this.listeners.get(event)?.delete(callback);
288
255
  }
256
+ // ============================================
257
+ // Private Methods
258
+ // ============================================
259
+ /**
260
+ * Restore session from localStorage
261
+ */
262
+ restoreSession() {
263
+ if (typeof localStorage === "undefined") return;
264
+ const userJson = localStorage.getItem("followgate_user");
265
+ if (userJson) {
266
+ try {
267
+ this.currentUser = JSON.parse(userJson);
268
+ } catch {
269
+ localStorage.removeItem("followgate_user");
270
+ }
271
+ }
272
+ const actionsJson = localStorage.getItem("followgate_actions");
273
+ if (actionsJson) {
274
+ try {
275
+ this.completedActions = JSON.parse(actionsJson);
276
+ } catch {
277
+ localStorage.removeItem("followgate_actions");
278
+ }
279
+ }
280
+ }
281
+ /**
282
+ * Save completed actions to localStorage
283
+ */
284
+ saveCompletedActions() {
285
+ if (typeof localStorage !== "undefined") {
286
+ localStorage.setItem(
287
+ "followgate_actions",
288
+ JSON.stringify(this.completedActions)
289
+ );
290
+ }
291
+ }
289
292
  /**
290
293
  * Build intent URL for platform
291
294
  */
@@ -347,6 +350,30 @@ var FollowGateClient = class {
347
350
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
348
351
  }
349
352
  }
353
+ /**
354
+ * Track analytics event
355
+ */
356
+ async trackEvent(event, data) {
357
+ if (!this.config) return;
358
+ try {
359
+ await fetch(`${this.config.apiUrl}/api/v1/events`, {
360
+ method: "POST",
361
+ headers: {
362
+ "Content-Type": "application/json",
363
+ "X-API-Key": this.config.apiKey
364
+ },
365
+ body: JSON.stringify({
366
+ event,
367
+ appId: this.config.appId,
368
+ ...data
369
+ })
370
+ });
371
+ } catch (error) {
372
+ if (this.config.debug) {
373
+ console.warn("[FollowGate] Failed to track event:", error);
374
+ }
375
+ }
376
+ }
350
377
  emit(event, data) {
351
378
  this.listeners.get(event)?.forEach((callback) => callback(data));
352
379
  }
@@ -354,5 +381,6 @@ var FollowGateClient = class {
354
381
  var FollowGate = new FollowGateClient();
355
382
  export {
356
383
  FollowGate,
357
- FollowGateClient
384
+ FollowGateClient,
385
+ FollowGateError
358
386
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@followgate/js",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",