@followgate/js 0.4.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.d.mts CHANGED
@@ -24,109 +24,112 @@ declare class FollowGateError extends Error {
24
24
  constructor(message: string, code: string, hint?: string | undefined);
25
25
  }
26
26
  /**
27
- * Open action options
27
+ * Complete action options
28
28
  */
29
- interface OpenOptions {
29
+ interface CompleteOptions {
30
30
  platform: Platform;
31
31
  action: SocialAction;
32
32
  target: string;
33
- userId?: string;
34
33
  }
35
- /**
36
- * LinkedIn target type
37
- */
38
- type LinkedInTargetType = 'company' | 'profile';
39
34
  /**
40
35
  * Event types
41
36
  */
42
- type EventType = 'complete' | 'error' | 'cancel' | 'authenticated';
37
+ type EventType = 'complete' | 'error' | 'unlocked';
43
38
  /**
44
39
  * Event callback
45
40
  */
46
41
  type EventCallback = (data: unknown) => void;
47
42
  /**
48
- * Authenticated user info
43
+ * User info (stored locally)
49
44
  */
50
- interface AuthenticatedUser {
51
- userId: string;
45
+ interface UserInfo {
52
46
  username: string;
53
47
  platform: Platform;
54
48
  }
55
49
  /**
56
- * Auth state when username input is needed
57
- */
58
- interface PendingUsernameState {
59
- needsUsername: true;
60
- token: string;
61
- }
62
- /**
63
- * Authentication options
50
+ * Unlock status
64
51
  */
65
- interface AuthOptions {
66
- /** Where to redirect after auth (defaults to current page) */
67
- redirectUri?: string;
68
- /** Open in popup instead of redirect */
69
- popup?: boolean;
52
+ interface UnlockStatus {
53
+ unlocked: boolean;
54
+ username?: string;
55
+ completedActions?: CompleteOptions[];
70
56
  }
71
57
  /**
72
58
  * FollowGate SDK Client
59
+ *
60
+ * Simple username-based flow:
61
+ * 1. User enters username
62
+ * 2. User clicks intent URLs to follow/repost
63
+ * 3. User confirms they did it
64
+ * 4. App is unlocked
65
+ *
66
+ * No OAuth required!
73
67
  */
74
68
  declare class FollowGateClient {
75
69
  private config;
76
70
  private listeners;
77
71
  private currentUser;
78
- private authToken;
79
- private pendingUsername;
72
+ private completedActions;
80
73
  /**
81
74
  * Initialize the SDK
82
75
  * @throws {FollowGateError} If configuration is invalid
83
76
  */
84
77
  init(config: FollowGateConfig): void;
85
78
  /**
86
- * Authenticate user via Twitter OAuth (handled by FollowGate)
87
- * This identifies WHO is completing the social actions.
79
+ * Set the user's social username
80
+ * This is the main entry point - no OAuth needed!
88
81
  */
89
- authenticate(options?: AuthOptions): void;
82
+ setUsername(username: string, platform?: Platform): void;
90
83
  /**
91
- * Get current authenticated user
84
+ * Get current user
92
85
  */
93
- getUser(): AuthenticatedUser | null;
86
+ getUser(): UserInfo | null;
94
87
  /**
95
- * Check if user is authenticated
88
+ * Check if username is set
96
89
  */
97
- isAuthenticated(): boolean;
90
+ hasUsername(): boolean;
98
91
  /**
99
- * Logout - clear stored session
92
+ * Clear stored session
100
93
  */
101
- logout(): void;
94
+ reset(): void;
102
95
  /**
103
- * Check if username input is needed (Twitter Free Tier limitation)
96
+ * Get follow intent URL for a platform
104
97
  */
105
- needsUsernameInput(): boolean;
98
+ getFollowUrl(platform: Platform, target: string): string;
106
99
  /**
107
- * Set username manually (when needsUsernameInput() returns true)
100
+ * Get repost/retweet intent URL for a platform
108
101
  */
109
- setUsername(username: string): void;
102
+ getRepostUrl(platform: Platform, target: string): string;
110
103
  /**
111
- * Handle auth callback from URL params
104
+ * Get like intent URL for a platform
112
105
  */
113
- private handleAuthCallback;
106
+ getLikeUrl(platform: Platform, target: string): string;
114
107
  /**
115
- * Restore session from localStorage
108
+ * Open intent URL in new window
116
109
  */
117
- private restoreSession;
110
+ openIntent(options: CompleteOptions): Promise<void>;
118
111
  /**
119
- * Open social action popup
112
+ * Mark an action as completed (trust-first)
113
+ * Call this when user confirms they did the action
120
114
  */
121
- open(options: OpenOptions): Promise<void>;
115
+ complete(options: CompleteOptions): Promise<void>;
122
116
  /**
123
- * Verify follow status (for Pro/Business tiers with OAuth)
117
+ * Mark the gate as unlocked
118
+ * Call this when all required actions are done
124
119
  */
125
- verify(options: OpenOptions): Promise<boolean>;
120
+ unlock(): Promise<void>;
126
121
  /**
127
- * Track analytics event
122
+ * Check if gate is unlocked
128
123
  */
129
- private trackEvent;
124
+ isUnlocked(): boolean;
125
+ /**
126
+ * Get unlock status with details
127
+ */
128
+ getUnlockStatus(): UnlockStatus;
129
+ /**
130
+ * Get completed actions
131
+ */
132
+ getCompletedActions(): CompleteOptions[];
130
133
  /**
131
134
  * Register event listener
132
135
  */
@@ -135,6 +138,14 @@ declare class FollowGateClient {
135
138
  * Remove event listener
136
139
  */
137
140
  off(event: EventType, callback: EventCallback): void;
141
+ /**
142
+ * Restore session from localStorage
143
+ */
144
+ private restoreSession;
145
+ /**
146
+ * Save completed actions to localStorage
147
+ */
148
+ private saveCompletedActions;
138
149
  /**
139
150
  * Build intent URL for platform
140
151
  */
@@ -142,8 +153,12 @@ declare class FollowGateClient {
142
153
  private buildTwitterUrl;
143
154
  private buildBlueskyUrl;
144
155
  private buildLinkedInUrl;
156
+ /**
157
+ * Track analytics event
158
+ */
159
+ private trackEvent;
145
160
  private emit;
146
161
  }
147
162
  declare const FollowGate: FollowGateClient;
148
163
 
149
- export { type AuthOptions, type AuthenticatedUser, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type LinkedInTargetType, type OpenOptions, type PendingUsernameState, type Platform, type SocialAction };
164
+ export { type CompleteOptions, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type Platform, type SocialAction, type UnlockStatus, type UserInfo };
package/dist/index.d.ts CHANGED
@@ -24,109 +24,112 @@ declare class FollowGateError extends Error {
24
24
  constructor(message: string, code: string, hint?: string | undefined);
25
25
  }
26
26
  /**
27
- * Open action options
27
+ * Complete action options
28
28
  */
29
- interface OpenOptions {
29
+ interface CompleteOptions {
30
30
  platform: Platform;
31
31
  action: SocialAction;
32
32
  target: string;
33
- userId?: string;
34
33
  }
35
- /**
36
- * LinkedIn target type
37
- */
38
- type LinkedInTargetType = 'company' | 'profile';
39
34
  /**
40
35
  * Event types
41
36
  */
42
- type EventType = 'complete' | 'error' | 'cancel' | 'authenticated';
37
+ type EventType = 'complete' | 'error' | 'unlocked';
43
38
  /**
44
39
  * Event callback
45
40
  */
46
41
  type EventCallback = (data: unknown) => void;
47
42
  /**
48
- * Authenticated user info
43
+ * User info (stored locally)
49
44
  */
50
- interface AuthenticatedUser {
51
- userId: string;
45
+ interface UserInfo {
52
46
  username: string;
53
47
  platform: Platform;
54
48
  }
55
49
  /**
56
- * Auth state when username input is needed
57
- */
58
- interface PendingUsernameState {
59
- needsUsername: true;
60
- token: string;
61
- }
62
- /**
63
- * Authentication options
50
+ * Unlock status
64
51
  */
65
- interface AuthOptions {
66
- /** Where to redirect after auth (defaults to current page) */
67
- redirectUri?: string;
68
- /** Open in popup instead of redirect */
69
- popup?: boolean;
52
+ interface UnlockStatus {
53
+ unlocked: boolean;
54
+ username?: string;
55
+ completedActions?: CompleteOptions[];
70
56
  }
71
57
  /**
72
58
  * FollowGate SDK Client
59
+ *
60
+ * Simple username-based flow:
61
+ * 1. User enters username
62
+ * 2. User clicks intent URLs to follow/repost
63
+ * 3. User confirms they did it
64
+ * 4. App is unlocked
65
+ *
66
+ * No OAuth required!
73
67
  */
74
68
  declare class FollowGateClient {
75
69
  private config;
76
70
  private listeners;
77
71
  private currentUser;
78
- private authToken;
79
- private pendingUsername;
72
+ private completedActions;
80
73
  /**
81
74
  * Initialize the SDK
82
75
  * @throws {FollowGateError} If configuration is invalid
83
76
  */
84
77
  init(config: FollowGateConfig): void;
85
78
  /**
86
- * Authenticate user via Twitter OAuth (handled by FollowGate)
87
- * This identifies WHO is completing the social actions.
79
+ * Set the user's social username
80
+ * This is the main entry point - no OAuth needed!
88
81
  */
89
- authenticate(options?: AuthOptions): void;
82
+ setUsername(username: string, platform?: Platform): void;
90
83
  /**
91
- * Get current authenticated user
84
+ * Get current user
92
85
  */
93
- getUser(): AuthenticatedUser | null;
86
+ getUser(): UserInfo | null;
94
87
  /**
95
- * Check if user is authenticated
88
+ * Check if username is set
96
89
  */
97
- isAuthenticated(): boolean;
90
+ hasUsername(): boolean;
98
91
  /**
99
- * Logout - clear stored session
92
+ * Clear stored session
100
93
  */
101
- logout(): void;
94
+ reset(): void;
102
95
  /**
103
- * Check if username input is needed (Twitter Free Tier limitation)
96
+ * Get follow intent URL for a platform
104
97
  */
105
- needsUsernameInput(): boolean;
98
+ getFollowUrl(platform: Platform, target: string): string;
106
99
  /**
107
- * Set username manually (when needsUsernameInput() returns true)
100
+ * Get repost/retweet intent URL for a platform
108
101
  */
109
- setUsername(username: string): void;
102
+ getRepostUrl(platform: Platform, target: string): string;
110
103
  /**
111
- * Handle auth callback from URL params
104
+ * Get like intent URL for a platform
112
105
  */
113
- private handleAuthCallback;
106
+ getLikeUrl(platform: Platform, target: string): string;
114
107
  /**
115
- * Restore session from localStorage
108
+ * Open intent URL in new window
116
109
  */
117
- private restoreSession;
110
+ openIntent(options: CompleteOptions): Promise<void>;
118
111
  /**
119
- * Open social action popup
112
+ * Mark an action as completed (trust-first)
113
+ * Call this when user confirms they did the action
120
114
  */
121
- open(options: OpenOptions): Promise<void>;
115
+ complete(options: CompleteOptions): Promise<void>;
122
116
  /**
123
- * Verify follow status (for Pro/Business tiers with OAuth)
117
+ * Mark the gate as unlocked
118
+ * Call this when all required actions are done
124
119
  */
125
- verify(options: OpenOptions): Promise<boolean>;
120
+ unlock(): Promise<void>;
126
121
  /**
127
- * Track analytics event
122
+ * Check if gate is unlocked
128
123
  */
129
- private trackEvent;
124
+ isUnlocked(): boolean;
125
+ /**
126
+ * Get unlock status with details
127
+ */
128
+ getUnlockStatus(): UnlockStatus;
129
+ /**
130
+ * Get completed actions
131
+ */
132
+ getCompletedActions(): CompleteOptions[];
130
133
  /**
131
134
  * Register event listener
132
135
  */
@@ -135,6 +138,14 @@ declare class FollowGateClient {
135
138
  * Remove event listener
136
139
  */
137
140
  off(event: EventType, callback: EventCallback): void;
141
+ /**
142
+ * Restore session from localStorage
143
+ */
144
+ private restoreSession;
145
+ /**
146
+ * Save completed actions to localStorage
147
+ */
148
+ private saveCompletedActions;
138
149
  /**
139
150
  * Build intent URL for platform
140
151
  */
@@ -142,8 +153,12 @@ declare class FollowGateClient {
142
153
  private buildTwitterUrl;
143
154
  private buildBlueskyUrl;
144
155
  private buildLinkedInUrl;
156
+ /**
157
+ * Track analytics event
158
+ */
159
+ private trackEvent;
145
160
  private emit;
146
161
  }
147
162
  declare const FollowGate: FollowGateClient;
148
163
 
149
- export { type AuthOptions, type AuthenticatedUser, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type LinkedInTargetType, type OpenOptions, type PendingUsernameState, type Platform, type SocialAction };
164
+ export { type CompleteOptions, type EventCallback, type EventType, FollowGate, FollowGateClient, type FollowGateConfig, FollowGateError, type Platform, type SocialAction, type UnlockStatus, type UserInfo };
package/dist/index.js CHANGED
@@ -41,8 +41,7 @@ var FollowGateClient = class {
41
41
  config = null;
42
42
  listeners = /* @__PURE__ */ new Map();
43
43
  currentUser = null;
44
- authToken = null;
45
- pendingUsername = null;
44
+ completedActions = [];
46
45
  /**
47
46
  * Initialize the SDK
48
47
  * @throws {FollowGateError} If configuration is invalid
@@ -87,7 +86,6 @@ var FollowGateClient = class {
87
86
  ...config,
88
87
  apiUrl: config.apiUrl || DEFAULT_API_URL
89
88
  };
90
- this.handleAuthCallback();
91
89
  this.restoreSession();
92
90
  if (config.debug) {
93
91
  console.log("[FollowGate] Initialized with appId:", config.appId);
@@ -101,253 +99,171 @@ var FollowGateClient = class {
101
99
  }
102
100
  }
103
101
  /**
104
- * Authenticate user via Twitter OAuth (handled by FollowGate)
105
- * 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!
106
104
  */
107
- authenticate(options = {}) {
105
+ setUsername(username, platform = "twitter") {
108
106
  if (!this.config) {
109
107
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
110
108
  }
111
- const redirectUri = options.redirectUri || window.location.href.split("?")[0];
112
- const authUrl = `${this.config.apiUrl}/api/v1/auth/twitter?app_id=${encodeURIComponent(this.config.appId)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
113
- if (this.config.debug) {
114
- 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));
115
116
  }
116
- if (options.popup) {
117
- const popup = window.open(
118
- authUrl,
119
- "followgate_auth",
120
- "width=600,height=700"
121
- );
122
- if (!popup) {
123
- this.emit("error", { message: "Popup blocked" });
124
- }
125
- } else {
126
- window.location.href = authUrl;
117
+ if (this.config.debug) {
118
+ console.log("[FollowGate] Username set:", normalizedUsername);
127
119
  }
128
120
  }
129
121
  /**
130
- * Get current authenticated user
122
+ * Get current user
131
123
  */
132
124
  getUser() {
133
125
  return this.currentUser;
134
126
  }
135
127
  /**
136
- * Check if user is authenticated
128
+ * Check if username is set
137
129
  */
138
- isAuthenticated() {
139
- return this.currentUser !== null && this.authToken !== null;
130
+ hasUsername() {
131
+ return this.currentUser !== null;
140
132
  }
141
133
  /**
142
- * Logout - clear stored session
134
+ * Clear stored session
143
135
  */
144
- logout() {
136
+ reset() {
145
137
  this.currentUser = null;
146
- this.authToken = null;
147
- this.pendingUsername = null;
138
+ this.completedActions = [];
148
139
  if (typeof localStorage !== "undefined") {
149
- localStorage.removeItem("followgate_token");
150
140
  localStorage.removeItem("followgate_user");
151
- localStorage.removeItem("followgate_pending_username");
141
+ localStorage.removeItem("followgate_actions");
142
+ localStorage.removeItem("followgate_unlocked");
152
143
  }
153
144
  if (this.config?.debug) {
154
- console.log("[FollowGate] User logged out");
145
+ console.log("[FollowGate] Session reset");
155
146
  }
156
147
  }
148
+ // ============================================
149
+ // Intent URL Methods
150
+ // ============================================
157
151
  /**
158
- * Check if username input is needed (Twitter Free Tier limitation)
152
+ * Get follow intent URL for a platform
159
153
  */
160
- needsUsernameInput() {
161
- return this.pendingUsername !== null;
154
+ getFollowUrl(platform, target) {
155
+ return this.buildIntentUrl({ platform, action: "follow", target });
162
156
  }
163
157
  /**
164
- * Set username manually (when needsUsernameInput() returns true)
158
+ * Get repost/retweet intent URL for a platform
165
159
  */
166
- setUsername(username) {
167
- if (!this.pendingUsername) {
168
- throw new Error(
169
- "[FollowGate] No pending username state. User is either not authenticated or username is already set."
170
- );
171
- }
172
- const normalizedUsername = username.startsWith("@") ? username.slice(1) : username;
173
- this.currentUser = {
174
- userId: "user_input",
175
- username: normalizedUsername,
176
- platform: "twitter"
177
- };
178
- this.authToken = this.pendingUsername.token;
179
- if (typeof localStorage !== "undefined") {
180
- localStorage.setItem("followgate_token", this.authToken);
181
- localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
182
- localStorage.removeItem("followgate_pending_username");
183
- }
184
- this.pendingUsername = null;
185
- this.emit("authenticated", this.currentUser);
186
- if (this.config?.debug) {
187
- console.log("[FollowGate] Username set manually:", normalizedUsername);
188
- }
160
+ getRepostUrl(platform, target) {
161
+ return this.buildIntentUrl({ platform, action: "repost", target });
189
162
  }
190
163
  /**
191
- * Handle auth callback from URL params
164
+ * Get like intent URL for a platform
192
165
  */
193
- handleAuthCallback() {
194
- if (typeof window === "undefined") return;
195
- const params = new URLSearchParams(window.location.search);
196
- const token = params.get("followgate_token");
197
- const username = params.get("followgate_user");
198
- const needsUsername = params.get("followgate_needs_username") === "true";
199
- if (token && needsUsername) {
200
- this.pendingUsername = {
201
- needsUsername: true,
202
- token
203
- };
204
- if (typeof localStorage !== "undefined") {
205
- localStorage.setItem(
206
- "followgate_pending_username",
207
- JSON.stringify(this.pendingUsername)
208
- );
209
- }
210
- const url = new URL(window.location.href);
211
- url.searchParams.delete("followgate_token");
212
- url.searchParams.delete("followgate_needs_username");
213
- window.history.replaceState({}, "", url.toString());
214
- if (this.config?.debug) {
215
- console.log("[FollowGate] OAuth successful, username input needed");
216
- }
217
- } else if (token && username) {
218
- this.authToken = token;
219
- this.currentUser = {
220
- userId: "",
221
- // Will be set from token verification
222
- username,
223
- platform: "twitter"
224
- };
225
- if (typeof localStorage !== "undefined") {
226
- localStorage.setItem("followgate_token", token);
227
- localStorage.setItem(
228
- "followgate_user",
229
- JSON.stringify(this.currentUser)
230
- );
231
- }
232
- const url = new URL(window.location.href);
233
- url.searchParams.delete("followgate_token");
234
- url.searchParams.delete("followgate_user");
235
- window.history.replaceState({}, "", url.toString());
236
- this.emit("authenticated", this.currentUser);
237
- if (this.config?.debug) {
238
- console.log("[FollowGate] User authenticated:", username);
239
- }
240
- }
166
+ getLikeUrl(platform, target) {
167
+ return this.buildIntentUrl({ platform, action: "like", target });
241
168
  }
242
169
  /**
243
- * Restore session from localStorage
170
+ * Open intent URL in new window
244
171
  */
245
- restoreSession() {
246
- if (typeof localStorage === "undefined") return;
247
- const pendingJson = localStorage.getItem("followgate_pending_username");
248
- if (pendingJson) {
249
- try {
250
- this.pendingUsername = JSON.parse(pendingJson);
251
- if (this.config?.debug) {
252
- console.log("[FollowGate] Restored pending username state");
253
- }
254
- return;
255
- } catch {
256
- localStorage.removeItem("followgate_pending_username");
257
- }
172
+ async openIntent(options) {
173
+ if (!this.config) {
174
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
258
175
  }
259
- const token = localStorage.getItem("followgate_token");
260
- const userJson = localStorage.getItem("followgate_user");
261
- if (token && userJson) {
262
- try {
263
- this.authToken = token;
264
- this.currentUser = JSON.parse(userJson);
265
- } catch {
266
- localStorage.removeItem("followgate_token");
267
- localStorage.removeItem("followgate_user");
268
- }
176
+ const url = this.buildIntentUrl(options);
177
+ if (this.config.debug) {
178
+ console.log("[FollowGate] Opening intent:", url);
269
179
  }
180
+ await this.trackEvent("intent_opened", { ...options });
181
+ window.open(url, "_blank", "width=600,height=700");
270
182
  }
183
+ // ============================================
184
+ // Completion Methods
185
+ // ============================================
271
186
  /**
272
- * Open social action popup
187
+ * Mark an action as completed (trust-first)
188
+ * Call this when user confirms they did the action
273
189
  */
274
- async open(options) {
190
+ async complete(options) {
275
191
  if (!this.config) {
276
192
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
277
193
  }
278
- const url = this.buildIntentUrl(options);
279
- if (this.config.debug) {
280
- console.log("[FollowGate] Opening:", url);
194
+ if (!this.currentUser) {
195
+ throw new Error(
196
+ "[FollowGate] No username set. Call setUsername() first."
197
+ );
281
198
  }
282
- await this.trackEvent("gate_opened", options);
283
- const popup = window.open(url, "_blank", "width=600,height=700");
284
- if (!popup) {
285
- this.emit("error", { message: "Popup blocked" });
286
- 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();
287
205
  }
288
- await this.trackEvent("action_clicked", options);
206
+ await this.trackEvent("action_completed", {
207
+ ...options,
208
+ username: this.currentUser.username
209
+ });
289
210
  this.emit("complete", {
290
- platform: options.platform,
291
- action: options.action,
292
- target: options.target
211
+ ...options,
212
+ username: this.currentUser.username
293
213
  });
214
+ if (this.config.debug) {
215
+ console.log("[FollowGate] Action completed:", options);
216
+ }
294
217
  }
295
218
  /**
296
- * Verify follow status (for Pro/Business tiers with OAuth)
219
+ * Mark the gate as unlocked
220
+ * Call this when all required actions are done
297
221
  */
298
- async verify(options) {
222
+ async unlock() {
299
223
  if (!this.config) {
300
224
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
301
225
  }
302
- try {
303
- const response = await fetch(`${this.config.apiUrl}/api/v1/verify`, {
304
- method: "POST",
305
- headers: {
306
- "Content-Type": "application/json",
307
- "X-API-Key": this.config.apiKey
308
- },
309
- body: JSON.stringify({
310
- platform: options.platform,
311
- action: options.action,
312
- target: options.target,
313
- externalUserId: options.userId
314
- })
315
- });
316
- const data = await response.json();
317
- return data.success && data.data?.verified === true;
318
- } catch (error) {
319
- if (this.config.debug) {
320
- console.error("[FollowGate] Verification error:", error);
321
- }
322
- 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!");
323
239
  }
324
240
  }
325
241
  /**
326
- * Track analytics event
242
+ * Check if gate is unlocked
327
243
  */
328
- async trackEvent(event, options) {
329
- if (!this.config) return;
330
- try {
331
- await fetch(`${this.config.apiUrl}/api/v1/events`, {
332
- method: "POST",
333
- headers: {
334
- "Content-Type": "application/json",
335
- "X-API-Key": this.config.apiKey
336
- },
337
- body: JSON.stringify({
338
- event,
339
- platform: options.platform,
340
- action: options.action,
341
- target: options.target,
342
- externalUserId: options.userId
343
- })
344
- });
345
- } catch (error) {
346
- if (this.config.debug) {
347
- console.warn("[FollowGate] Failed to track event:", error);
348
- }
349
- }
244
+ isUnlocked() {
245
+ if (typeof localStorage === "undefined") return false;
246
+ return localStorage.getItem("followgate_unlocked") === "true";
350
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
+ // ============================================
351
267
  /**
352
268
  * Register event listener
353
269
  */
@@ -363,6 +279,42 @@ var FollowGateClient = class {
363
279
  off(event, callback) {
364
280
  this.listeners.get(event)?.delete(callback);
365
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
+ }
366
318
  /**
367
319
  * Build intent URL for platform
368
320
  */
@@ -424,6 +376,30 @@ var FollowGateClient = class {
424
376
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
425
377
  }
426
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
+ }
427
403
  emit(event, data) {
428
404
  this.listeners.get(event)?.forEach((callback) => callback(data));
429
405
  }
package/dist/index.mjs CHANGED
@@ -15,8 +15,7 @@ var FollowGateClient = class {
15
15
  config = null;
16
16
  listeners = /* @__PURE__ */ new Map();
17
17
  currentUser = null;
18
- authToken = null;
19
- pendingUsername = null;
18
+ completedActions = [];
20
19
  /**
21
20
  * Initialize the SDK
22
21
  * @throws {FollowGateError} If configuration is invalid
@@ -61,7 +60,6 @@ var FollowGateClient = class {
61
60
  ...config,
62
61
  apiUrl: config.apiUrl || DEFAULT_API_URL
63
62
  };
64
- this.handleAuthCallback();
65
63
  this.restoreSession();
66
64
  if (config.debug) {
67
65
  console.log("[FollowGate] Initialized with appId:", config.appId);
@@ -75,253 +73,171 @@ var FollowGateClient = class {
75
73
  }
76
74
  }
77
75
  /**
78
- * Authenticate user via Twitter OAuth (handled by FollowGate)
79
- * 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!
80
78
  */
81
- authenticate(options = {}) {
79
+ setUsername(username, platform = "twitter") {
82
80
  if (!this.config) {
83
81
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
84
82
  }
85
- const redirectUri = options.redirectUri || window.location.href.split("?")[0];
86
- const authUrl = `${this.config.apiUrl}/api/v1/auth/twitter?app_id=${encodeURIComponent(this.config.appId)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
87
- if (this.config.debug) {
88
- 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));
89
90
  }
90
- if (options.popup) {
91
- const popup = window.open(
92
- authUrl,
93
- "followgate_auth",
94
- "width=600,height=700"
95
- );
96
- if (!popup) {
97
- this.emit("error", { message: "Popup blocked" });
98
- }
99
- } else {
100
- window.location.href = authUrl;
91
+ if (this.config.debug) {
92
+ console.log("[FollowGate] Username set:", normalizedUsername);
101
93
  }
102
94
  }
103
95
  /**
104
- * Get current authenticated user
96
+ * Get current user
105
97
  */
106
98
  getUser() {
107
99
  return this.currentUser;
108
100
  }
109
101
  /**
110
- * Check if user is authenticated
102
+ * Check if username is set
111
103
  */
112
- isAuthenticated() {
113
- return this.currentUser !== null && this.authToken !== null;
104
+ hasUsername() {
105
+ return this.currentUser !== null;
114
106
  }
115
107
  /**
116
- * Logout - clear stored session
108
+ * Clear stored session
117
109
  */
118
- logout() {
110
+ reset() {
119
111
  this.currentUser = null;
120
- this.authToken = null;
121
- this.pendingUsername = null;
112
+ this.completedActions = [];
122
113
  if (typeof localStorage !== "undefined") {
123
- localStorage.removeItem("followgate_token");
124
114
  localStorage.removeItem("followgate_user");
125
- localStorage.removeItem("followgate_pending_username");
115
+ localStorage.removeItem("followgate_actions");
116
+ localStorage.removeItem("followgate_unlocked");
126
117
  }
127
118
  if (this.config?.debug) {
128
- console.log("[FollowGate] User logged out");
119
+ console.log("[FollowGate] Session reset");
129
120
  }
130
121
  }
122
+ // ============================================
123
+ // Intent URL Methods
124
+ // ============================================
131
125
  /**
132
- * Check if username input is needed (Twitter Free Tier limitation)
126
+ * Get follow intent URL for a platform
133
127
  */
134
- needsUsernameInput() {
135
- return this.pendingUsername !== null;
128
+ getFollowUrl(platform, target) {
129
+ return this.buildIntentUrl({ platform, action: "follow", target });
136
130
  }
137
131
  /**
138
- * Set username manually (when needsUsernameInput() returns true)
132
+ * Get repost/retweet intent URL for a platform
139
133
  */
140
- setUsername(username) {
141
- if (!this.pendingUsername) {
142
- throw new Error(
143
- "[FollowGate] No pending username state. User is either not authenticated or username is already set."
144
- );
145
- }
146
- const normalizedUsername = username.startsWith("@") ? username.slice(1) : username;
147
- this.currentUser = {
148
- userId: "user_input",
149
- username: normalizedUsername,
150
- platform: "twitter"
151
- };
152
- this.authToken = this.pendingUsername.token;
153
- if (typeof localStorage !== "undefined") {
154
- localStorage.setItem("followgate_token", this.authToken);
155
- localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
156
- localStorage.removeItem("followgate_pending_username");
157
- }
158
- this.pendingUsername = null;
159
- this.emit("authenticated", this.currentUser);
160
- if (this.config?.debug) {
161
- console.log("[FollowGate] Username set manually:", normalizedUsername);
162
- }
134
+ getRepostUrl(platform, target) {
135
+ return this.buildIntentUrl({ platform, action: "repost", target });
163
136
  }
164
137
  /**
165
- * Handle auth callback from URL params
138
+ * Get like intent URL for a platform
166
139
  */
167
- handleAuthCallback() {
168
- if (typeof window === "undefined") return;
169
- const params = new URLSearchParams(window.location.search);
170
- const token = params.get("followgate_token");
171
- const username = params.get("followgate_user");
172
- const needsUsername = params.get("followgate_needs_username") === "true";
173
- if (token && needsUsername) {
174
- this.pendingUsername = {
175
- needsUsername: true,
176
- token
177
- };
178
- if (typeof localStorage !== "undefined") {
179
- localStorage.setItem(
180
- "followgate_pending_username",
181
- JSON.stringify(this.pendingUsername)
182
- );
183
- }
184
- const url = new URL(window.location.href);
185
- url.searchParams.delete("followgate_token");
186
- url.searchParams.delete("followgate_needs_username");
187
- window.history.replaceState({}, "", url.toString());
188
- if (this.config?.debug) {
189
- console.log("[FollowGate] OAuth successful, username input needed");
190
- }
191
- } else if (token && username) {
192
- this.authToken = token;
193
- this.currentUser = {
194
- userId: "",
195
- // Will be set from token verification
196
- username,
197
- platform: "twitter"
198
- };
199
- if (typeof localStorage !== "undefined") {
200
- localStorage.setItem("followgate_token", token);
201
- localStorage.setItem(
202
- "followgate_user",
203
- JSON.stringify(this.currentUser)
204
- );
205
- }
206
- const url = new URL(window.location.href);
207
- url.searchParams.delete("followgate_token");
208
- url.searchParams.delete("followgate_user");
209
- window.history.replaceState({}, "", url.toString());
210
- this.emit("authenticated", this.currentUser);
211
- if (this.config?.debug) {
212
- console.log("[FollowGate] User authenticated:", username);
213
- }
214
- }
140
+ getLikeUrl(platform, target) {
141
+ return this.buildIntentUrl({ platform, action: "like", target });
215
142
  }
216
143
  /**
217
- * Restore session from localStorage
144
+ * Open intent URL in new window
218
145
  */
219
- restoreSession() {
220
- if (typeof localStorage === "undefined") return;
221
- const pendingJson = localStorage.getItem("followgate_pending_username");
222
- if (pendingJson) {
223
- try {
224
- this.pendingUsername = JSON.parse(pendingJson);
225
- if (this.config?.debug) {
226
- console.log("[FollowGate] Restored pending username state");
227
- }
228
- return;
229
- } catch {
230
- localStorage.removeItem("followgate_pending_username");
231
- }
146
+ async openIntent(options) {
147
+ if (!this.config) {
148
+ throw new Error("[FollowGate] SDK not initialized. Call init() first.");
232
149
  }
233
- const token = localStorage.getItem("followgate_token");
234
- const userJson = localStorage.getItem("followgate_user");
235
- if (token && userJson) {
236
- try {
237
- this.authToken = token;
238
- this.currentUser = JSON.parse(userJson);
239
- } catch {
240
- localStorage.removeItem("followgate_token");
241
- localStorage.removeItem("followgate_user");
242
- }
150
+ const url = this.buildIntentUrl(options);
151
+ if (this.config.debug) {
152
+ console.log("[FollowGate] Opening intent:", url);
243
153
  }
154
+ await this.trackEvent("intent_opened", { ...options });
155
+ window.open(url, "_blank", "width=600,height=700");
244
156
  }
157
+ // ============================================
158
+ // Completion Methods
159
+ // ============================================
245
160
  /**
246
- * Open social action popup
161
+ * Mark an action as completed (trust-first)
162
+ * Call this when user confirms they did the action
247
163
  */
248
- async open(options) {
164
+ async complete(options) {
249
165
  if (!this.config) {
250
166
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
251
167
  }
252
- const url = this.buildIntentUrl(options);
253
- if (this.config.debug) {
254
- console.log("[FollowGate] Opening:", url);
168
+ if (!this.currentUser) {
169
+ throw new Error(
170
+ "[FollowGate] No username set. Call setUsername() first."
171
+ );
255
172
  }
256
- await this.trackEvent("gate_opened", options);
257
- const popup = window.open(url, "_blank", "width=600,height=700");
258
- if (!popup) {
259
- this.emit("error", { message: "Popup blocked" });
260
- 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();
261
179
  }
262
- await this.trackEvent("action_clicked", options);
180
+ await this.trackEvent("action_completed", {
181
+ ...options,
182
+ username: this.currentUser.username
183
+ });
263
184
  this.emit("complete", {
264
- platform: options.platform,
265
- action: options.action,
266
- target: options.target
185
+ ...options,
186
+ username: this.currentUser.username
267
187
  });
188
+ if (this.config.debug) {
189
+ console.log("[FollowGate] Action completed:", options);
190
+ }
268
191
  }
269
192
  /**
270
- * Verify follow status (for Pro/Business tiers with OAuth)
193
+ * Mark the gate as unlocked
194
+ * Call this when all required actions are done
271
195
  */
272
- async verify(options) {
196
+ async unlock() {
273
197
  if (!this.config) {
274
198
  throw new Error("[FollowGate] SDK not initialized. Call init() first.");
275
199
  }
276
- try {
277
- const response = await fetch(`${this.config.apiUrl}/api/v1/verify`, {
278
- method: "POST",
279
- headers: {
280
- "Content-Type": "application/json",
281
- "X-API-Key": this.config.apiKey
282
- },
283
- body: JSON.stringify({
284
- platform: options.platform,
285
- action: options.action,
286
- target: options.target,
287
- externalUserId: options.userId
288
- })
289
- });
290
- const data = await response.json();
291
- return data.success && data.data?.verified === true;
292
- } catch (error) {
293
- if (this.config.debug) {
294
- console.error("[FollowGate] Verification error:", error);
295
- }
296
- 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!");
297
213
  }
298
214
  }
299
215
  /**
300
- * Track analytics event
216
+ * Check if gate is unlocked
301
217
  */
302
- async trackEvent(event, options) {
303
- if (!this.config) return;
304
- try {
305
- await fetch(`${this.config.apiUrl}/api/v1/events`, {
306
- method: "POST",
307
- headers: {
308
- "Content-Type": "application/json",
309
- "X-API-Key": this.config.apiKey
310
- },
311
- body: JSON.stringify({
312
- event,
313
- platform: options.platform,
314
- action: options.action,
315
- target: options.target,
316
- externalUserId: options.userId
317
- })
318
- });
319
- } catch (error) {
320
- if (this.config.debug) {
321
- console.warn("[FollowGate] Failed to track event:", error);
322
- }
323
- }
218
+ isUnlocked() {
219
+ if (typeof localStorage === "undefined") return false;
220
+ return localStorage.getItem("followgate_unlocked") === "true";
324
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
+ // ============================================
325
241
  /**
326
242
  * Register event listener
327
243
  */
@@ -337,6 +253,42 @@ var FollowGateClient = class {
337
253
  off(event, callback) {
338
254
  this.listeners.get(event)?.delete(callback);
339
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
+ }
340
292
  /**
341
293
  * Build intent URL for platform
342
294
  */
@@ -398,6 +350,30 @@ var FollowGateClient = class {
398
350
  throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
399
351
  }
400
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
+ }
401
377
  emit(event, data) {
402
378
  this.listeners.get(event)?.forEach((callback) => callback(data));
403
379
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@followgate/js",
3
- "version": "0.4.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",