@followgate/js 0.5.0 → 0.7.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 +274 -86
- package/dist/index.d.mts +43 -56
- package/dist/index.d.ts +43 -56
- package/dist/index.js +785 -69
- package/dist/index.mjs +785 -69
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
var DEFAULT_API_URL = "https://api.followgate.app";
|
|
29
|
+
var WAIT_TIME_SECONDS = 3;
|
|
29
30
|
var FollowGateError = class extends Error {
|
|
30
31
|
constructor(message, code, hint) {
|
|
31
32
|
super(message);
|
|
@@ -37,70 +38,822 @@ var FollowGateError = class extends Error {
|
|
|
37
38
|
function isValidApiKeyFormat(apiKey) {
|
|
38
39
|
return /^fg_(live|test)_[a-zA-Z0-9_-]+$/.test(apiKey);
|
|
39
40
|
}
|
|
41
|
+
var MODAL_STYLES = `
|
|
42
|
+
.fg-modal-backdrop {
|
|
43
|
+
position: fixed;
|
|
44
|
+
inset: 0;
|
|
45
|
+
z-index: 99999;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
background: rgba(0, 0, 0, 0.8);
|
|
50
|
+
backdrop-filter: blur(4px);
|
|
51
|
+
opacity: 0;
|
|
52
|
+
transition: opacity 0.3s ease;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.fg-modal-backdrop.fg-visible {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.fg-modal {
|
|
60
|
+
position: relative;
|
|
61
|
+
width: 100%;
|
|
62
|
+
max-width: 420px;
|
|
63
|
+
margin: 16px;
|
|
64
|
+
background: #0f172a;
|
|
65
|
+
border-radius: 16px;
|
|
66
|
+
border: 1px solid #334155;
|
|
67
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
68
|
+
padding: 32px;
|
|
69
|
+
transform: scale(0.95);
|
|
70
|
+
transition: transform 0.3s ease;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.fg-modal-backdrop.fg-visible .fg-modal {
|
|
74
|
+
transform: scale(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.fg-icon-box {
|
|
78
|
+
width: 64px;
|
|
79
|
+
height: 64px;
|
|
80
|
+
margin: 0 auto 24px;
|
|
81
|
+
background: #1e293b;
|
|
82
|
+
border-radius: 16px;
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
border: 1px solid #334155;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.fg-icon-box svg {
|
|
90
|
+
width: 32px;
|
|
91
|
+
height: 32px;
|
|
92
|
+
fill: currentColor;
|
|
93
|
+
color: white;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.fg-icon-box.fg-success {
|
|
97
|
+
background: rgba(34, 197, 94, 0.1);
|
|
98
|
+
border-color: rgba(34, 197, 94, 0.3);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.fg-icon-box.fg-success svg {
|
|
102
|
+
color: #22c55e;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.fg-title {
|
|
106
|
+
font-size: 24px;
|
|
107
|
+
font-weight: 700;
|
|
108
|
+
color: white;
|
|
109
|
+
text-align: center;
|
|
110
|
+
margin: 0 0 8px;
|
|
111
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.fg-subtitle {
|
|
115
|
+
font-size: 14px;
|
|
116
|
+
color: #94a3b8;
|
|
117
|
+
text-align: center;
|
|
118
|
+
margin: 0 0 24px;
|
|
119
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.fg-user-badge {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
justify-content: center;
|
|
126
|
+
gap: 8px;
|
|
127
|
+
padding: 6px 12px;
|
|
128
|
+
background: #1e293b;
|
|
129
|
+
border-radius: 9999px;
|
|
130
|
+
width: fit-content;
|
|
131
|
+
margin: 0 auto 16px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.fg-user-badge-dot {
|
|
135
|
+
width: 8px;
|
|
136
|
+
height: 8px;
|
|
137
|
+
background: #22c55e;
|
|
138
|
+
border-radius: 50%;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.fg-user-badge-text {
|
|
142
|
+
font-size: 14px;
|
|
143
|
+
color: #cbd5e1;
|
|
144
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.fg-input-wrapper {
|
|
148
|
+
position: relative;
|
|
149
|
+
margin-bottom: 16px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.fg-input-prefix {
|
|
153
|
+
position: absolute;
|
|
154
|
+
left: 16px;
|
|
155
|
+
top: 50%;
|
|
156
|
+
transform: translateY(-50%);
|
|
157
|
+
color: #64748b;
|
|
158
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.fg-input {
|
|
162
|
+
width: 100%;
|
|
163
|
+
padding: 16px 16px 16px 36px;
|
|
164
|
+
background: #1e293b;
|
|
165
|
+
border: 1px solid #334155;
|
|
166
|
+
border-radius: 12px;
|
|
167
|
+
color: white;
|
|
168
|
+
font-size: 16px;
|
|
169
|
+
outline: none;
|
|
170
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
171
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
172
|
+
box-sizing: border-box;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.fg-input:focus {
|
|
176
|
+
border-color: var(--fg-accent, #6366f1);
|
|
177
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.fg-input::placeholder {
|
|
181
|
+
color: #475569;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.fg-btn {
|
|
185
|
+
width: 100%;
|
|
186
|
+
padding: 16px 24px;
|
|
187
|
+
border-radius: 12px;
|
|
188
|
+
font-size: 16px;
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
transition: all 0.2s;
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
justify-content: center;
|
|
195
|
+
gap: 12px;
|
|
196
|
+
border: none;
|
|
197
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
198
|
+
box-sizing: border-box;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.fg-btn:disabled {
|
|
202
|
+
opacity: 0.5;
|
|
203
|
+
cursor: not-allowed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.fg-btn-primary {
|
|
207
|
+
background: var(--fg-accent, #6366f1);
|
|
208
|
+
color: white;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.fg-btn-primary:hover:not(:disabled) {
|
|
212
|
+
background: var(--fg-accent-hover, #4f46e5);
|
|
213
|
+
transform: scale(1.02);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.fg-btn-primary:active:not(:disabled) {
|
|
217
|
+
transform: scale(0.98);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.fg-btn-dark {
|
|
221
|
+
background: #000;
|
|
222
|
+
color: white;
|
|
223
|
+
border: 1px solid #334155;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.fg-btn-dark:hover:not(:disabled) {
|
|
227
|
+
background: #111;
|
|
228
|
+
transform: scale(1.02);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.fg-btn-green {
|
|
232
|
+
background: #16a34a;
|
|
233
|
+
color: white;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.fg-btn-green:hover:not(:disabled) {
|
|
237
|
+
background: #15803d;
|
|
238
|
+
transform: scale(1.02);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.fg-btn-secondary {
|
|
242
|
+
background: transparent;
|
|
243
|
+
color: #94a3b8;
|
|
244
|
+
border: 1px solid #334155;
|
|
245
|
+
margin-top: 12px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.fg-btn-secondary:hover:not(:disabled) {
|
|
249
|
+
background: #1e293b;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.fg-btn svg {
|
|
253
|
+
width: 20px;
|
|
254
|
+
height: 20px;
|
|
255
|
+
flex-shrink: 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.fg-spinner {
|
|
259
|
+
width: 20px;
|
|
260
|
+
height: 20px;
|
|
261
|
+
border: 2px solid transparent;
|
|
262
|
+
border-top-color: currentColor;
|
|
263
|
+
border-radius: 50%;
|
|
264
|
+
animation: fg-spin 0.8s linear infinite;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@keyframes fg-spin {
|
|
268
|
+
to { transform: rotate(360deg); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.fg-hint {
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
color: #475569;
|
|
274
|
+
text-align: center;
|
|
275
|
+
margin-top: 16px;
|
|
276
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.fg-info-box {
|
|
280
|
+
background: rgba(30, 41, 59, 0.5);
|
|
281
|
+
padding: 8px 12px;
|
|
282
|
+
border-radius: 8px;
|
|
283
|
+
margin-bottom: 12px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.fg-info-box p {
|
|
287
|
+
font-size: 12px;
|
|
288
|
+
color: #64748b;
|
|
289
|
+
text-align: center;
|
|
290
|
+
margin: 0;
|
|
291
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.fg-warning-box {
|
|
295
|
+
background: rgba(245, 158, 11, 0.1);
|
|
296
|
+
border: 1px solid rgba(245, 158, 11, 0.3);
|
|
297
|
+
border-radius: 12px;
|
|
298
|
+
padding: 16px;
|
|
299
|
+
margin-bottom: 24px;
|
|
300
|
+
display: flex;
|
|
301
|
+
align-items: flex-start;
|
|
302
|
+
gap: 12px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.fg-warning-box svg {
|
|
306
|
+
width: 20px;
|
|
307
|
+
height: 20px;
|
|
308
|
+
color: #fbbf24;
|
|
309
|
+
flex-shrink: 0;
|
|
310
|
+
margin-top: 2px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.fg-warning-box p {
|
|
314
|
+
font-size: 14px;
|
|
315
|
+
color: #fde68a;
|
|
316
|
+
margin: 0;
|
|
317
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.fg-step-indicator {
|
|
321
|
+
display: flex;
|
|
322
|
+
align-items: center;
|
|
323
|
+
justify-content: center;
|
|
324
|
+
gap: 12px;
|
|
325
|
+
margin-bottom: 24px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.fg-step-dot {
|
|
329
|
+
width: 32px;
|
|
330
|
+
height: 32px;
|
|
331
|
+
border-radius: 50%;
|
|
332
|
+
display: flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
justify-content: center;
|
|
335
|
+
font-size: 14px;
|
|
336
|
+
font-weight: 700;
|
|
337
|
+
transition: all 0.3s;
|
|
338
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.fg-step-dot.fg-pending {
|
|
342
|
+
background: #334155;
|
|
343
|
+
color: #64748b;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.fg-step-dot.fg-active {
|
|
347
|
+
background: var(--fg-accent, #6366f1);
|
|
348
|
+
color: white;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.fg-step-dot.fg-done {
|
|
352
|
+
background: #22c55e;
|
|
353
|
+
color: white;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.fg-step-line {
|
|
357
|
+
width: 24px;
|
|
358
|
+
height: 2px;
|
|
359
|
+
background: #334155;
|
|
360
|
+
transition: background 0.3s;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.fg-step-line.fg-done {
|
|
364
|
+
background: #22c55e;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.fg-footer {
|
|
368
|
+
margin-top: 24px;
|
|
369
|
+
padding-top: 16px;
|
|
370
|
+
border-top: 1px solid #1e293b;
|
|
371
|
+
text-align: center;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.fg-footer p {
|
|
375
|
+
font-size: 12px;
|
|
376
|
+
color: #475569;
|
|
377
|
+
margin: 0;
|
|
378
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.fg-footer a {
|
|
382
|
+
color: #64748b;
|
|
383
|
+
text-decoration: none;
|
|
384
|
+
transition: color 0.2s;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.fg-footer a:hover {
|
|
388
|
+
color: #94a3b8;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.fg-btn-row {
|
|
392
|
+
display: flex;
|
|
393
|
+
gap: 8px;
|
|
394
|
+
margin-top: 12px;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.fg-btn-row .fg-btn {
|
|
398
|
+
flex: 1;
|
|
399
|
+
padding: 12px 16px;
|
|
400
|
+
font-size: 14px;
|
|
401
|
+
}
|
|
402
|
+
`;
|
|
403
|
+
var ICONS = {
|
|
404
|
+
x: '<svg viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
|
|
405
|
+
check: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
|
|
406
|
+
repost: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>',
|
|
407
|
+
warning: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>'
|
|
408
|
+
};
|
|
40
409
|
var FollowGateClient = class {
|
|
41
410
|
config = null;
|
|
42
411
|
listeners = /* @__PURE__ */ new Map();
|
|
43
412
|
currentUser = null;
|
|
44
413
|
completedActions = [];
|
|
414
|
+
modalElement = null;
|
|
415
|
+
stylesInjected = false;
|
|
45
416
|
/**
|
|
46
417
|
* Initialize the SDK
|
|
47
|
-
* @throws {FollowGateError} If configuration is invalid
|
|
48
418
|
*/
|
|
49
419
|
init(config) {
|
|
50
420
|
if (!config.appId || typeof config.appId !== "string") {
|
|
51
421
|
throw new FollowGateError(
|
|
52
422
|
"[FollowGate] Missing or invalid appId",
|
|
53
423
|
"INVALID_APP_ID",
|
|
54
|
-
"Get your App ID from https://followgate.app/dashboard
|
|
424
|
+
"Get your App ID from https://followgate.app/dashboard"
|
|
55
425
|
);
|
|
56
426
|
}
|
|
57
427
|
if (config.appId.trim() === "" || config.appId === "undefined") {
|
|
58
428
|
throw new FollowGateError(
|
|
59
429
|
"[FollowGate] appId is empty or undefined",
|
|
60
430
|
"EMPTY_APP_ID",
|
|
61
|
-
"
|
|
431
|
+
"Check that NEXT_PUBLIC_FOLLOWGATE_APP_ID is set"
|
|
62
432
|
);
|
|
63
433
|
}
|
|
64
434
|
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
65
435
|
throw new FollowGateError(
|
|
66
436
|
"[FollowGate] Missing or invalid apiKey",
|
|
67
437
|
"INVALID_API_KEY",
|
|
68
|
-
"Get your API Key from https://followgate.app/dashboard
|
|
438
|
+
"Get your API Key from https://followgate.app/dashboard"
|
|
69
439
|
);
|
|
70
440
|
}
|
|
71
441
|
if (config.apiKey.trim() === "" || config.apiKey === "undefined") {
|
|
72
442
|
throw new FollowGateError(
|
|
73
443
|
"[FollowGate] apiKey is empty or undefined",
|
|
74
444
|
"EMPTY_API_KEY",
|
|
75
|
-
"
|
|
445
|
+
"Set NEXT_PUBLIC_FOLLOWGATE_API_KEY and rebuild"
|
|
76
446
|
);
|
|
77
447
|
}
|
|
78
448
|
if (!isValidApiKeyFormat(config.apiKey)) {
|
|
79
449
|
throw new FollowGateError(
|
|
80
|
-
`[FollowGate] Invalid API key format
|
|
450
|
+
`[FollowGate] Invalid API key format`,
|
|
81
451
|
"INVALID_API_KEY_FORMAT",
|
|
82
|
-
'API keys should start with "fg_live_"
|
|
452
|
+
'API keys should start with "fg_live_" or "fg_test_"'
|
|
83
453
|
);
|
|
84
454
|
}
|
|
85
455
|
this.config = {
|
|
86
456
|
...config,
|
|
87
|
-
apiUrl: config.apiUrl || DEFAULT_API_URL
|
|
457
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
458
|
+
theme: config.theme || "dark",
|
|
459
|
+
accentColor: config.accentColor || "#6366f1"
|
|
88
460
|
};
|
|
89
461
|
this.restoreSession();
|
|
90
462
|
if (config.debug) {
|
|
91
463
|
console.log("[FollowGate] Initialized with appId:", config.appId);
|
|
92
|
-
console.log(
|
|
93
|
-
"[FollowGate] API Key:",
|
|
94
|
-
config.apiKey.substring(0, 12) + "..."
|
|
95
|
-
);
|
|
96
464
|
if (this.currentUser) {
|
|
97
465
|
console.log("[FollowGate] Restored user:", this.currentUser.username);
|
|
98
466
|
}
|
|
99
467
|
}
|
|
100
468
|
}
|
|
469
|
+
// ============================================
|
|
470
|
+
// Modal UI Methods
|
|
471
|
+
// ============================================
|
|
472
|
+
/**
|
|
473
|
+
* Show the FollowGate modal
|
|
474
|
+
* If user is already unlocked, calls onComplete immediately
|
|
475
|
+
*/
|
|
476
|
+
show() {
|
|
477
|
+
if (!this.config) {
|
|
478
|
+
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
479
|
+
}
|
|
480
|
+
if (this.isUnlocked()) {
|
|
481
|
+
if (this.config.debug) {
|
|
482
|
+
console.log("[FollowGate] Already unlocked, skipping modal");
|
|
483
|
+
}
|
|
484
|
+
this.config.onComplete?.();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
this.injectStyles();
|
|
488
|
+
this.createModal();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Hide the modal
|
|
492
|
+
*/
|
|
493
|
+
hide() {
|
|
494
|
+
if (this.modalElement) {
|
|
495
|
+
this.modalElement.classList.remove("fg-visible");
|
|
496
|
+
setTimeout(() => {
|
|
497
|
+
this.modalElement?.remove();
|
|
498
|
+
this.modalElement = null;
|
|
499
|
+
}, 300);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
injectStyles() {
|
|
503
|
+
if (this.stylesInjected) return;
|
|
504
|
+
if (typeof document === "undefined") return;
|
|
505
|
+
const style = document.createElement("style");
|
|
506
|
+
style.id = "followgate-styles";
|
|
507
|
+
style.textContent = MODAL_STYLES;
|
|
508
|
+
document.head.appendChild(style);
|
|
509
|
+
if (this.config?.accentColor) {
|
|
510
|
+
document.documentElement.style.setProperty(
|
|
511
|
+
"--fg-accent",
|
|
512
|
+
this.config.accentColor
|
|
513
|
+
);
|
|
514
|
+
document.documentElement.style.setProperty(
|
|
515
|
+
"--fg-accent-hover",
|
|
516
|
+
this.config.accentColor
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
this.stylesInjected = true;
|
|
520
|
+
}
|
|
521
|
+
createModal() {
|
|
522
|
+
if (typeof document === "undefined") return;
|
|
523
|
+
const backdrop = document.createElement("div");
|
|
524
|
+
backdrop.className = "fg-modal-backdrop";
|
|
525
|
+
backdrop.innerHTML = `
|
|
526
|
+
<div class="fg-modal">
|
|
527
|
+
<div id="fg-content"></div>
|
|
528
|
+
<div class="fg-footer">
|
|
529
|
+
<p>Powered by <a href="https://followgate.app" target="_blank" rel="noopener">FollowGate</a></p>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
`;
|
|
533
|
+
document.body.appendChild(backdrop);
|
|
534
|
+
this.modalElement = backdrop;
|
|
535
|
+
requestAnimationFrame(() => {
|
|
536
|
+
backdrop.classList.add("fg-visible");
|
|
537
|
+
});
|
|
538
|
+
if (this.hasUsername()) {
|
|
539
|
+
this.renderFollowStep();
|
|
540
|
+
} else {
|
|
541
|
+
this.renderUsernameStep();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
getContentElement() {
|
|
545
|
+
return document.getElementById("fg-content");
|
|
546
|
+
}
|
|
547
|
+
renderUsernameStep() {
|
|
548
|
+
const content = this.getContentElement();
|
|
549
|
+
if (!content) return;
|
|
550
|
+
content.innerHTML = `
|
|
551
|
+
<div class="fg-icon-box">
|
|
552
|
+
${ICONS.x}
|
|
553
|
+
</div>
|
|
554
|
+
<h2 class="fg-title">Unlock Free Access</h2>
|
|
555
|
+
<p class="fg-subtitle">Enter your X username to get started</p>
|
|
556
|
+
<div class="fg-input-wrapper">
|
|
557
|
+
<span class="fg-input-prefix">@</span>
|
|
558
|
+
<input type="text" class="fg-input" id="fg-username-input" placeholder="your_username" autofocus>
|
|
559
|
+
</div>
|
|
560
|
+
<button class="fg-btn fg-btn-primary" id="fg-username-submit" disabled>
|
|
561
|
+
Continue
|
|
562
|
+
</button>
|
|
563
|
+
<p class="fg-hint">No login required \u2013 just your username</p>
|
|
564
|
+
`;
|
|
565
|
+
const input = document.getElementById(
|
|
566
|
+
"fg-username-input"
|
|
567
|
+
);
|
|
568
|
+
const btn = document.getElementById(
|
|
569
|
+
"fg-username-submit"
|
|
570
|
+
);
|
|
571
|
+
input?.addEventListener("input", () => {
|
|
572
|
+
btn.disabled = !input.value.trim();
|
|
573
|
+
});
|
|
574
|
+
input?.addEventListener("keydown", (e) => {
|
|
575
|
+
if (e.key === "Enter" && input.value.trim()) {
|
|
576
|
+
this.handleUsernameSubmit(input.value.trim());
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
btn?.addEventListener("click", () => {
|
|
580
|
+
if (input?.value.trim()) {
|
|
581
|
+
this.handleUsernameSubmit(input.value.trim());
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
setTimeout(() => input?.focus(), 100);
|
|
585
|
+
}
|
|
586
|
+
handleUsernameSubmit(username) {
|
|
587
|
+
const normalized = username.replace(/^@/, "");
|
|
588
|
+
this.setUsername(normalized);
|
|
589
|
+
this.renderFollowStep();
|
|
590
|
+
}
|
|
591
|
+
renderFollowStep() {
|
|
592
|
+
const content = this.getContentElement();
|
|
593
|
+
if (!content || !this.config?.twitter) return;
|
|
594
|
+
const handle = this.config.twitter.handle;
|
|
595
|
+
const hasRepost = !!this.config.twitter.tweetId;
|
|
596
|
+
content.innerHTML = `
|
|
597
|
+
${hasRepost ? this.renderStepIndicator(1) : ""}
|
|
598
|
+
<div class="fg-icon-box">
|
|
599
|
+
${ICONS.x}
|
|
600
|
+
</div>
|
|
601
|
+
${this.currentUser ? `
|
|
602
|
+
<div class="fg-user-badge">
|
|
603
|
+
<div class="fg-user-badge-dot"></div>
|
|
604
|
+
<span class="fg-user-badge-text">@${this.currentUser.username}</span>
|
|
605
|
+
</div>
|
|
606
|
+
` : ""}
|
|
607
|
+
<h2 class="fg-title">${hasRepost ? "Step 1: Follow" : "Follow to Continue"}</h2>
|
|
608
|
+
<p class="fg-subtitle" id="fg-follow-subtitle">Follow @${handle} on X</p>
|
|
609
|
+
<div id="fg-follow-actions">
|
|
610
|
+
<button class="fg-btn fg-btn-dark" id="fg-follow-btn">
|
|
611
|
+
${ICONS.x}
|
|
612
|
+
Follow @${handle}
|
|
613
|
+
</button>
|
|
614
|
+
</div>
|
|
615
|
+
<p class="fg-hint">A new window will open. Return here after following.</p>
|
|
616
|
+
`;
|
|
617
|
+
document.getElementById("fg-follow-btn")?.addEventListener("click", () => {
|
|
618
|
+
this.handleFollowClick();
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
handleFollowClick() {
|
|
622
|
+
if (!this.config?.twitter) return;
|
|
623
|
+
const handle = this.config.twitter.handle;
|
|
624
|
+
this.openIntent({
|
|
625
|
+
platform: "twitter",
|
|
626
|
+
action: "follow",
|
|
627
|
+
target: handle
|
|
628
|
+
});
|
|
629
|
+
this.showFollowConfirmation();
|
|
630
|
+
}
|
|
631
|
+
showFollowConfirmation() {
|
|
632
|
+
if (!this.config?.twitter) return;
|
|
633
|
+
const handle = this.config.twitter.handle;
|
|
634
|
+
const subtitle = document.getElementById("fg-follow-subtitle");
|
|
635
|
+
const actions = document.getElementById("fg-follow-actions");
|
|
636
|
+
if (subtitle) {
|
|
637
|
+
subtitle.textContent = `Did you follow @${handle}?`;
|
|
638
|
+
}
|
|
639
|
+
if (actions) {
|
|
640
|
+
let seconds = WAIT_TIME_SECONDS;
|
|
641
|
+
actions.innerHTML = `
|
|
642
|
+
<div class="fg-info-box">
|
|
643
|
+
<p>You can close the X window after following</p>
|
|
644
|
+
</div>
|
|
645
|
+
<button class="fg-btn fg-btn-green" id="fg-follow-confirm" disabled>
|
|
646
|
+
<span class="fg-spinner"></span>
|
|
647
|
+
Waiting... (${seconds}s)
|
|
648
|
+
</button>
|
|
649
|
+
<button class="fg-btn fg-btn-secondary" id="fg-follow-retry">
|
|
650
|
+
${ICONS.x}
|
|
651
|
+
Open again
|
|
652
|
+
</button>
|
|
653
|
+
`;
|
|
654
|
+
const confirmBtn = document.getElementById(
|
|
655
|
+
"fg-follow-confirm"
|
|
656
|
+
);
|
|
657
|
+
const retryBtn = document.getElementById("fg-follow-retry");
|
|
658
|
+
const interval = setInterval(() => {
|
|
659
|
+
seconds--;
|
|
660
|
+
if (seconds <= 0) {
|
|
661
|
+
clearInterval(interval);
|
|
662
|
+
if (confirmBtn) {
|
|
663
|
+
confirmBtn.disabled = false;
|
|
664
|
+
confirmBtn.innerHTML = `${ICONS.check} Yes, I followed`;
|
|
665
|
+
}
|
|
666
|
+
} else if (confirmBtn) {
|
|
667
|
+
confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
|
|
668
|
+
}
|
|
669
|
+
}, 1e3);
|
|
670
|
+
confirmBtn?.addEventListener("click", () => {
|
|
671
|
+
this.handleFollowConfirm();
|
|
672
|
+
});
|
|
673
|
+
retryBtn?.addEventListener("click", () => {
|
|
674
|
+
this.handleFollowClick();
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async handleFollowConfirm() {
|
|
679
|
+
if (!this.config?.twitter) return;
|
|
680
|
+
const handle = this.config.twitter.handle;
|
|
681
|
+
await this.complete({
|
|
682
|
+
platform: "twitter",
|
|
683
|
+
action: "follow",
|
|
684
|
+
target: handle
|
|
685
|
+
});
|
|
686
|
+
if (this.config.twitter.tweetId) {
|
|
687
|
+
this.renderRepostStep();
|
|
688
|
+
} else {
|
|
689
|
+
this.renderConfirmStep();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
renderRepostStep() {
|
|
693
|
+
const content = this.getContentElement();
|
|
694
|
+
if (!content || !this.config?.twitter?.tweetId) return;
|
|
695
|
+
content.innerHTML = `
|
|
696
|
+
${this.renderStepIndicator(2)}
|
|
697
|
+
<div class="fg-icon-box fg-success">
|
|
698
|
+
${ICONS.repost}
|
|
699
|
+
</div>
|
|
700
|
+
${this.currentUser ? `
|
|
701
|
+
<div class="fg-user-badge">
|
|
702
|
+
<div class="fg-user-badge-dot"></div>
|
|
703
|
+
<span class="fg-user-badge-text">@${this.currentUser.username}</span>
|
|
704
|
+
</div>
|
|
705
|
+
` : ""}
|
|
706
|
+
<h2 class="fg-title">Step 2: Repost</h2>
|
|
707
|
+
<p class="fg-subtitle" id="fg-repost-subtitle">Repost the tweet to continue</p>
|
|
708
|
+
<div id="fg-repost-actions">
|
|
709
|
+
<button class="fg-btn fg-btn-green" id="fg-repost-btn">
|
|
710
|
+
${ICONS.repost}
|
|
711
|
+
Repost tweet
|
|
712
|
+
</button>
|
|
713
|
+
</div>
|
|
714
|
+
<p class="fg-hint">A new window will open. Return here after reposting.</p>
|
|
715
|
+
`;
|
|
716
|
+
document.getElementById("fg-repost-btn")?.addEventListener("click", () => {
|
|
717
|
+
this.handleRepostClick();
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
handleRepostClick() {
|
|
721
|
+
if (!this.config?.twitter?.tweetId) return;
|
|
722
|
+
const tweetId = this.config.twitter.tweetId;
|
|
723
|
+
this.openIntent({
|
|
724
|
+
platform: "twitter",
|
|
725
|
+
action: "repost",
|
|
726
|
+
target: tweetId
|
|
727
|
+
});
|
|
728
|
+
this.showRepostConfirmation();
|
|
729
|
+
}
|
|
730
|
+
showRepostConfirmation() {
|
|
731
|
+
const subtitle = document.getElementById("fg-repost-subtitle");
|
|
732
|
+
const actions = document.getElementById("fg-repost-actions");
|
|
733
|
+
if (subtitle) {
|
|
734
|
+
subtitle.textContent = "Did you repost the tweet?";
|
|
735
|
+
}
|
|
736
|
+
if (actions) {
|
|
737
|
+
let seconds = WAIT_TIME_SECONDS;
|
|
738
|
+
actions.innerHTML = `
|
|
739
|
+
<div class="fg-info-box">
|
|
740
|
+
<p>You can close the X window after reposting</p>
|
|
741
|
+
</div>
|
|
742
|
+
<button class="fg-btn fg-btn-green" id="fg-repost-confirm" disabled>
|
|
743
|
+
<span class="fg-spinner"></span>
|
|
744
|
+
Waiting... (${seconds}s)
|
|
745
|
+
</button>
|
|
746
|
+
<button class="fg-btn fg-btn-secondary" id="fg-repost-retry">
|
|
747
|
+
${ICONS.repost}
|
|
748
|
+
Open tweet again
|
|
749
|
+
</button>
|
|
750
|
+
`;
|
|
751
|
+
const confirmBtn = document.getElementById(
|
|
752
|
+
"fg-repost-confirm"
|
|
753
|
+
);
|
|
754
|
+
const retryBtn = document.getElementById("fg-repost-retry");
|
|
755
|
+
const interval = setInterval(() => {
|
|
756
|
+
seconds--;
|
|
757
|
+
if (seconds <= 0) {
|
|
758
|
+
clearInterval(interval);
|
|
759
|
+
if (confirmBtn) {
|
|
760
|
+
confirmBtn.disabled = false;
|
|
761
|
+
confirmBtn.innerHTML = `${ICONS.check} Yes, I reposted`;
|
|
762
|
+
}
|
|
763
|
+
} else if (confirmBtn) {
|
|
764
|
+
confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
|
|
765
|
+
}
|
|
766
|
+
}, 1e3);
|
|
767
|
+
confirmBtn?.addEventListener("click", () => {
|
|
768
|
+
this.handleRepostConfirm();
|
|
769
|
+
});
|
|
770
|
+
retryBtn?.addEventListener("click", () => {
|
|
771
|
+
this.handleRepostClick();
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async handleRepostConfirm() {
|
|
776
|
+
if (!this.config?.twitter?.tweetId) return;
|
|
777
|
+
await this.complete({
|
|
778
|
+
platform: "twitter",
|
|
779
|
+
action: "repost",
|
|
780
|
+
target: this.config.twitter.tweetId
|
|
781
|
+
});
|
|
782
|
+
this.renderConfirmStep();
|
|
783
|
+
}
|
|
784
|
+
renderConfirmStep() {
|
|
785
|
+
const content = this.getContentElement();
|
|
786
|
+
if (!content) return;
|
|
787
|
+
content.innerHTML = `
|
|
788
|
+
<h2 class="fg-title">Almost done!</h2>
|
|
789
|
+
<div class="fg-warning-box">
|
|
790
|
+
${ICONS.warning}
|
|
791
|
+
<p><strong>Note:</strong> Access may be revoked if actions are not completed.</p>
|
|
792
|
+
</div>
|
|
793
|
+
<button class="fg-btn fg-btn-green" id="fg-finish-btn">
|
|
794
|
+
${ICONS.check}
|
|
795
|
+
Got it
|
|
796
|
+
</button>
|
|
797
|
+
${this.config?.twitter ? `
|
|
798
|
+
<div class="fg-btn-row">
|
|
799
|
+
<button class="fg-btn fg-btn-secondary" id="fg-redo-follow">
|
|
800
|
+
${ICONS.x}
|
|
801
|
+
Open follow
|
|
802
|
+
</button>
|
|
803
|
+
${this.config.twitter.tweetId ? `
|
|
804
|
+
<button class="fg-btn fg-btn-secondary" id="fg-redo-repost">
|
|
805
|
+
${ICONS.repost}
|
|
806
|
+
Open repost
|
|
807
|
+
</button>
|
|
808
|
+
` : ""}
|
|
809
|
+
</div>
|
|
810
|
+
` : ""}
|
|
811
|
+
`;
|
|
812
|
+
document.getElementById("fg-finish-btn")?.addEventListener("click", () => {
|
|
813
|
+
this.handleFinish();
|
|
814
|
+
});
|
|
815
|
+
document.getElementById("fg-redo-follow")?.addEventListener("click", () => {
|
|
816
|
+
if (this.config?.twitter) {
|
|
817
|
+
this.openIntent({
|
|
818
|
+
platform: "twitter",
|
|
819
|
+
action: "follow",
|
|
820
|
+
target: this.config.twitter.handle
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
document.getElementById("fg-redo-repost")?.addEventListener("click", () => {
|
|
825
|
+
if (this.config?.twitter?.tweetId) {
|
|
826
|
+
this.openIntent({
|
|
827
|
+
platform: "twitter",
|
|
828
|
+
action: "repost",
|
|
829
|
+
target: this.config.twitter.tweetId
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async handleFinish() {
|
|
835
|
+
await this.unlock();
|
|
836
|
+
this.hide();
|
|
837
|
+
this.config?.onComplete?.();
|
|
838
|
+
}
|
|
839
|
+
renderStepIndicator(currentStep) {
|
|
840
|
+
return `
|
|
841
|
+
<div class="fg-step-indicator">
|
|
842
|
+
<div class="fg-step-dot ${currentStep > 1 ? "fg-done" : currentStep === 1 ? "fg-active" : "fg-pending"}">
|
|
843
|
+
${currentStep > 1 ? "\u2713" : "1"}
|
|
844
|
+
</div>
|
|
845
|
+
<div class="fg-step-line ${currentStep > 1 ? "fg-done" : ""}"></div>
|
|
846
|
+
<div class="fg-step-dot ${currentStep > 2 ? "fg-done" : currentStep === 2 ? "fg-active" : "fg-pending"}">
|
|
847
|
+
${currentStep > 2 ? "\u2713" : "2"}
|
|
848
|
+
</div>
|
|
849
|
+
</div>
|
|
850
|
+
`;
|
|
851
|
+
}
|
|
852
|
+
// ============================================
|
|
853
|
+
// Core SDK Methods
|
|
854
|
+
// ============================================
|
|
101
855
|
/**
|
|
102
856
|
* Set the user's social username
|
|
103
|
-
* This is the main entry point - no OAuth needed!
|
|
104
857
|
*/
|
|
105
858
|
setUsername(username, platform = "twitter") {
|
|
106
859
|
if (!this.config) {
|
|
@@ -112,7 +865,7 @@ var FollowGateClient = class {
|
|
|
112
865
|
platform
|
|
113
866
|
};
|
|
114
867
|
if (typeof localStorage !== "undefined") {
|
|
115
|
-
localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
|
|
868
|
+
localStorage.setItem(this.getStorageKey("followgate_user"), JSON.stringify(this.currentUser));
|
|
116
869
|
}
|
|
117
870
|
if (this.config.debug) {
|
|
118
871
|
console.log("[FollowGate] Username set:", normalizedUsername);
|
|
@@ -137,9 +890,9 @@ var FollowGateClient = class {
|
|
|
137
890
|
this.currentUser = null;
|
|
138
891
|
this.completedActions = [];
|
|
139
892
|
if (typeof localStorage !== "undefined") {
|
|
140
|
-
localStorage.removeItem("followgate_user");
|
|
141
|
-
localStorage.removeItem("followgate_actions");
|
|
142
|
-
localStorage.removeItem("followgate_unlocked");
|
|
893
|
+
localStorage.removeItem(this.getStorageKey("followgate_user"));
|
|
894
|
+
localStorage.removeItem(this.getStorageKey("followgate_actions"));
|
|
895
|
+
localStorage.removeItem(this.getStorageKey("followgate_unlocked"));
|
|
143
896
|
}
|
|
144
897
|
if (this.config?.debug) {
|
|
145
898
|
console.log("[FollowGate] Session reset");
|
|
@@ -148,27 +901,15 @@ var FollowGateClient = class {
|
|
|
148
901
|
// ============================================
|
|
149
902
|
// Intent URL Methods
|
|
150
903
|
// ============================================
|
|
151
|
-
/**
|
|
152
|
-
* Get follow intent URL for a platform
|
|
153
|
-
*/
|
|
154
904
|
getFollowUrl(platform, target) {
|
|
155
905
|
return this.buildIntentUrl({ platform, action: "follow", target });
|
|
156
906
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Get repost/retweet intent URL for a platform
|
|
159
|
-
*/
|
|
160
907
|
getRepostUrl(platform, target) {
|
|
161
908
|
return this.buildIntentUrl({ platform, action: "repost", target });
|
|
162
909
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Get like intent URL for a platform
|
|
165
|
-
*/
|
|
166
910
|
getLikeUrl(platform, target) {
|
|
167
911
|
return this.buildIntentUrl({ platform, action: "like", target });
|
|
168
912
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Open intent URL in new window
|
|
171
|
-
*/
|
|
172
913
|
async openIntent(options) {
|
|
173
914
|
if (!this.config) {
|
|
174
915
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
@@ -183,10 +924,6 @@ var FollowGateClient = class {
|
|
|
183
924
|
// ============================================
|
|
184
925
|
// Completion Methods
|
|
185
926
|
// ============================================
|
|
186
|
-
/**
|
|
187
|
-
* Mark an action as completed (trust-first)
|
|
188
|
-
* Call this when user confirms they did the action
|
|
189
|
-
*/
|
|
190
927
|
async complete(options) {
|
|
191
928
|
if (!this.config) {
|
|
192
929
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
@@ -215,16 +952,12 @@ var FollowGateClient = class {
|
|
|
215
952
|
console.log("[FollowGate] Action completed:", options);
|
|
216
953
|
}
|
|
217
954
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Mark the gate as unlocked
|
|
220
|
-
* Call this when all required actions are done
|
|
221
|
-
*/
|
|
222
955
|
async unlock() {
|
|
223
956
|
if (!this.config) {
|
|
224
957
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
225
958
|
}
|
|
226
959
|
if (typeof localStorage !== "undefined") {
|
|
227
|
-
localStorage.setItem("followgate_unlocked", "true");
|
|
960
|
+
localStorage.setItem(this.getStorageKey("followgate_unlocked"), "true");
|
|
228
961
|
}
|
|
229
962
|
await this.trackEvent("gate_unlocked", {
|
|
230
963
|
username: this.currentUser?.username,
|
|
@@ -238,16 +971,10 @@ var FollowGateClient = class {
|
|
|
238
971
|
console.log("[FollowGate] Gate unlocked!");
|
|
239
972
|
}
|
|
240
973
|
}
|
|
241
|
-
/**
|
|
242
|
-
* Check if gate is unlocked
|
|
243
|
-
*/
|
|
244
974
|
isUnlocked() {
|
|
245
975
|
if (typeof localStorage === "undefined") return false;
|
|
246
|
-
return localStorage.getItem("followgate_unlocked") === "true";
|
|
976
|
+
return localStorage.getItem(this.getStorageKey("followgate_unlocked")) === "true";
|
|
247
977
|
}
|
|
248
|
-
/**
|
|
249
|
-
* Get unlock status with details
|
|
250
|
-
*/
|
|
251
978
|
getUnlockStatus() {
|
|
252
979
|
return {
|
|
253
980
|
unlocked: this.isUnlocked(),
|
|
@@ -255,27 +982,18 @@ var FollowGateClient = class {
|
|
|
255
982
|
completedActions: [...this.completedActions]
|
|
256
983
|
};
|
|
257
984
|
}
|
|
258
|
-
/**
|
|
259
|
-
* Get completed actions
|
|
260
|
-
*/
|
|
261
985
|
getCompletedActions() {
|
|
262
986
|
return [...this.completedActions];
|
|
263
987
|
}
|
|
264
988
|
// ============================================
|
|
265
989
|
// Event System
|
|
266
990
|
// ============================================
|
|
267
|
-
/**
|
|
268
|
-
* Register event listener
|
|
269
|
-
*/
|
|
270
991
|
on(event, callback) {
|
|
271
992
|
if (!this.listeners.has(event)) {
|
|
272
993
|
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
273
994
|
}
|
|
274
995
|
this.listeners.get(event).add(callback);
|
|
275
996
|
}
|
|
276
|
-
/**
|
|
277
|
-
* Remove event listener
|
|
278
|
-
*/
|
|
279
997
|
off(event, callback) {
|
|
280
998
|
this.listeners.get(event)?.delete(callback);
|
|
281
999
|
}
|
|
@@ -283,41 +1001,42 @@ var FollowGateClient = class {
|
|
|
283
1001
|
// Private Methods
|
|
284
1002
|
// ============================================
|
|
285
1003
|
/**
|
|
286
|
-
*
|
|
1004
|
+
* Get storage key with optional userId suffix
|
|
1005
|
+
* This ensures unlock status is per-user, not per-browser
|
|
287
1006
|
*/
|
|
1007
|
+
getStorageKey(base) {
|
|
1008
|
+
if (this.config?.userId) {
|
|
1009
|
+
return `${base}_${this.config.userId}`;
|
|
1010
|
+
}
|
|
1011
|
+
return base;
|
|
1012
|
+
}
|
|
288
1013
|
restoreSession() {
|
|
289
1014
|
if (typeof localStorage === "undefined") return;
|
|
290
|
-
const userJson = localStorage.getItem("followgate_user");
|
|
1015
|
+
const userJson = localStorage.getItem(this.getStorageKey("followgate_user"));
|
|
291
1016
|
if (userJson) {
|
|
292
1017
|
try {
|
|
293
1018
|
this.currentUser = JSON.parse(userJson);
|
|
294
1019
|
} catch {
|
|
295
|
-
localStorage.removeItem("followgate_user");
|
|
1020
|
+
localStorage.removeItem(this.getStorageKey("followgate_user"));
|
|
296
1021
|
}
|
|
297
1022
|
}
|
|
298
|
-
const actionsJson = localStorage.getItem("followgate_actions");
|
|
1023
|
+
const actionsJson = localStorage.getItem(this.getStorageKey("followgate_actions"));
|
|
299
1024
|
if (actionsJson) {
|
|
300
1025
|
try {
|
|
301
1026
|
this.completedActions = JSON.parse(actionsJson);
|
|
302
1027
|
} catch {
|
|
303
|
-
localStorage.removeItem("followgate_actions");
|
|
1028
|
+
localStorage.removeItem(this.getStorageKey("followgate_actions"));
|
|
304
1029
|
}
|
|
305
1030
|
}
|
|
306
1031
|
}
|
|
307
|
-
/**
|
|
308
|
-
* Save completed actions to localStorage
|
|
309
|
-
*/
|
|
310
1032
|
saveCompletedActions() {
|
|
311
1033
|
if (typeof localStorage !== "undefined") {
|
|
312
1034
|
localStorage.setItem(
|
|
313
|
-
"followgate_actions",
|
|
1035
|
+
this.getStorageKey("followgate_actions"),
|
|
314
1036
|
JSON.stringify(this.completedActions)
|
|
315
1037
|
);
|
|
316
1038
|
}
|
|
317
1039
|
}
|
|
318
|
-
/**
|
|
319
|
-
* Build intent URL for platform
|
|
320
|
-
*/
|
|
321
1040
|
buildIntentUrl(options) {
|
|
322
1041
|
const { platform, action, target } = options;
|
|
323
1042
|
switch (platform) {
|
|
@@ -376,9 +1095,6 @@ var FollowGateClient = class {
|
|
|
376
1095
|
throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
|
|
377
1096
|
}
|
|
378
1097
|
}
|
|
379
|
-
/**
|
|
380
|
-
* Track analytics event
|
|
381
|
-
*/
|
|
382
1098
|
async trackEvent(event, data) {
|
|
383
1099
|
if (!this.config) return;
|
|
384
1100
|
try {
|