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