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