@followgate/js 0.4.0 → 0.6.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 +66 -70
- package/dist/index.d.ts +66 -70
- package/dist/index.js +886 -220
- package/dist/index.mjs +886 -220
- 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,335 +38,979 @@ 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
|
-
|
|
45
|
-
|
|
413
|
+
completedActions = [];
|
|
414
|
+
modalElement = null;
|
|
415
|
+
stylesInjected = false;
|
|
46
416
|
/**
|
|
47
417
|
* Initialize the SDK
|
|
48
|
-
* @throws {FollowGateError} If configuration is invalid
|
|
49
418
|
*/
|
|
50
419
|
init(config) {
|
|
51
420
|
if (!config.appId || typeof config.appId !== "string") {
|
|
52
421
|
throw new FollowGateError(
|
|
53
422
|
"[FollowGate] Missing or invalid appId",
|
|
54
423
|
"INVALID_APP_ID",
|
|
55
|
-
"Get your App ID from https://followgate.app/dashboard
|
|
424
|
+
"Get your App ID from https://followgate.app/dashboard"
|
|
56
425
|
);
|
|
57
426
|
}
|
|
58
427
|
if (config.appId.trim() === "" || config.appId === "undefined") {
|
|
59
428
|
throw new FollowGateError(
|
|
60
429
|
"[FollowGate] appId is empty or undefined",
|
|
61
430
|
"EMPTY_APP_ID",
|
|
62
|
-
"
|
|
431
|
+
"Check that NEXT_PUBLIC_FOLLOWGATE_APP_ID is set"
|
|
63
432
|
);
|
|
64
433
|
}
|
|
65
434
|
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
66
435
|
throw new FollowGateError(
|
|
67
436
|
"[FollowGate] Missing or invalid apiKey",
|
|
68
437
|
"INVALID_API_KEY",
|
|
69
|
-
"Get your API Key from https://followgate.app/dashboard
|
|
438
|
+
"Get your API Key from https://followgate.app/dashboard"
|
|
70
439
|
);
|
|
71
440
|
}
|
|
72
441
|
if (config.apiKey.trim() === "" || config.apiKey === "undefined") {
|
|
73
442
|
throw new FollowGateError(
|
|
74
443
|
"[FollowGate] apiKey is empty or undefined",
|
|
75
444
|
"EMPTY_API_KEY",
|
|
76
|
-
"
|
|
445
|
+
"Set NEXT_PUBLIC_FOLLOWGATE_API_KEY and rebuild"
|
|
77
446
|
);
|
|
78
447
|
}
|
|
79
448
|
if (!isValidApiKeyFormat(config.apiKey)) {
|
|
80
449
|
throw new FollowGateError(
|
|
81
|
-
`[FollowGate] Invalid API key format
|
|
450
|
+
`[FollowGate] Invalid API key format`,
|
|
82
451
|
"INVALID_API_KEY_FORMAT",
|
|
83
|
-
'API keys should start with "fg_live_"
|
|
452
|
+
'API keys should start with "fg_live_" or "fg_test_"'
|
|
84
453
|
);
|
|
85
454
|
}
|
|
86
455
|
this.config = {
|
|
87
456
|
...config,
|
|
88
|
-
apiUrl: config.apiUrl || DEFAULT_API_URL
|
|
457
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
458
|
+
theme: config.theme || "dark",
|
|
459
|
+
accentColor: config.accentColor || "#6366f1"
|
|
89
460
|
};
|
|
90
|
-
this.handleAuthCallback();
|
|
91
461
|
this.restoreSession();
|
|
92
462
|
if (config.debug) {
|
|
93
463
|
console.log("[FollowGate] Initialized with appId:", config.appId);
|
|
94
|
-
console.log(
|
|
95
|
-
"[FollowGate] API Key:",
|
|
96
|
-
config.apiKey.substring(0, 12) + "..."
|
|
97
|
-
);
|
|
98
464
|
if (this.currentUser) {
|
|
99
465
|
console.log("[FollowGate] Restored user:", this.currentUser.username);
|
|
100
466
|
}
|
|
101
467
|
}
|
|
102
468
|
}
|
|
469
|
+
// ============================================
|
|
470
|
+
// Modal UI Methods
|
|
471
|
+
// ============================================
|
|
103
472
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
473
|
+
* Show the FollowGate modal
|
|
474
|
+
* If user is already unlocked, calls onComplete immediately
|
|
106
475
|
*/
|
|
107
|
-
|
|
476
|
+
show() {
|
|
108
477
|
if (!this.config) {
|
|
109
478
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
110
479
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
console.log("[FollowGate] Starting auth flow:", authUrl);
|
|
115
|
-
}
|
|
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" });
|
|
480
|
+
if (this.isUnlocked()) {
|
|
481
|
+
if (this.config.debug) {
|
|
482
|
+
console.log("[FollowGate] Already unlocked, skipping modal");
|
|
124
483
|
}
|
|
125
|
-
|
|
126
|
-
|
|
484
|
+
this.config.onComplete?.();
|
|
485
|
+
return;
|
|
127
486
|
}
|
|
487
|
+
this.injectStyles();
|
|
488
|
+
this.createModal();
|
|
128
489
|
}
|
|
129
490
|
/**
|
|
130
|
-
*
|
|
491
|
+
* Hide the modal
|
|
131
492
|
*/
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
501
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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("--fg-accent", this.config.accentColor);
|
|
511
|
+
document.documentElement.style.setProperty("--fg-accent-hover", this.config.accentColor);
|
|
512
|
+
}
|
|
513
|
+
this.stylesInjected = true;
|
|
140
514
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
515
|
+
createModal() {
|
|
516
|
+
if (typeof document === "undefined") return;
|
|
517
|
+
const backdrop = document.createElement("div");
|
|
518
|
+
backdrop.className = "fg-modal-backdrop";
|
|
519
|
+
backdrop.innerHTML = `
|
|
520
|
+
<div class="fg-modal">
|
|
521
|
+
<div id="fg-content"></div>
|
|
522
|
+
<div class="fg-footer">
|
|
523
|
+
<p>Powered by <a href="https://followgate.app" target="_blank" rel="noopener">FollowGate</a></p>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
`;
|
|
527
|
+
document.body.appendChild(backdrop);
|
|
528
|
+
this.modalElement = backdrop;
|
|
529
|
+
requestAnimationFrame(() => {
|
|
530
|
+
backdrop.classList.add("fg-visible");
|
|
531
|
+
});
|
|
532
|
+
if (this.hasUsername()) {
|
|
533
|
+
this.renderFollowStep();
|
|
534
|
+
} else {
|
|
535
|
+
this.renderUsernameStep();
|
|
152
536
|
}
|
|
153
|
-
|
|
154
|
-
|
|
537
|
+
}
|
|
538
|
+
getContentElement() {
|
|
539
|
+
return document.getElementById("fg-content");
|
|
540
|
+
}
|
|
541
|
+
renderUsernameStep() {
|
|
542
|
+
const content = this.getContentElement();
|
|
543
|
+
if (!content) return;
|
|
544
|
+
content.innerHTML = `
|
|
545
|
+
<div class="fg-icon-box">
|
|
546
|
+
${ICONS.x}
|
|
547
|
+
</div>
|
|
548
|
+
<h2 class="fg-title">Unlock Free Access</h2>
|
|
549
|
+
<p class="fg-subtitle">Enter your X username to get started</p>
|
|
550
|
+
<div class="fg-input-wrapper">
|
|
551
|
+
<span class="fg-input-prefix">@</span>
|
|
552
|
+
<input type="text" class="fg-input" id="fg-username-input" placeholder="your_username" autofocus>
|
|
553
|
+
</div>
|
|
554
|
+
<button class="fg-btn fg-btn-primary" id="fg-username-submit" disabled>
|
|
555
|
+
Continue
|
|
556
|
+
</button>
|
|
557
|
+
<p class="fg-hint">No login required \u2013 just your username</p>
|
|
558
|
+
`;
|
|
559
|
+
const input = document.getElementById("fg-username-input");
|
|
560
|
+
const btn = document.getElementById("fg-username-submit");
|
|
561
|
+
input?.addEventListener("input", () => {
|
|
562
|
+
btn.disabled = !input.value.trim();
|
|
563
|
+
});
|
|
564
|
+
input?.addEventListener("keydown", (e) => {
|
|
565
|
+
if (e.key === "Enter" && input.value.trim()) {
|
|
566
|
+
this.handleUsernameSubmit(input.value.trim());
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
btn?.addEventListener("click", () => {
|
|
570
|
+
if (input?.value.trim()) {
|
|
571
|
+
this.handleUsernameSubmit(input.value.trim());
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
setTimeout(() => input?.focus(), 100);
|
|
575
|
+
}
|
|
576
|
+
handleUsernameSubmit(username) {
|
|
577
|
+
const normalized = username.replace(/^@/, "");
|
|
578
|
+
this.setUsername(normalized);
|
|
579
|
+
this.renderFollowStep();
|
|
580
|
+
}
|
|
581
|
+
renderFollowStep() {
|
|
582
|
+
const content = this.getContentElement();
|
|
583
|
+
if (!content || !this.config?.twitter) return;
|
|
584
|
+
const handle = this.config.twitter.handle;
|
|
585
|
+
const hasRepost = !!this.config.twitter.tweetId;
|
|
586
|
+
content.innerHTML = `
|
|
587
|
+
${hasRepost ? this.renderStepIndicator(1) : ""}
|
|
588
|
+
<div class="fg-icon-box">
|
|
589
|
+
${ICONS.x}
|
|
590
|
+
</div>
|
|
591
|
+
${this.currentUser ? `
|
|
592
|
+
<div class="fg-user-badge">
|
|
593
|
+
<div class="fg-user-badge-dot"></div>
|
|
594
|
+
<span class="fg-user-badge-text">@${this.currentUser.username}</span>
|
|
595
|
+
</div>
|
|
596
|
+
` : ""}
|
|
597
|
+
<h2 class="fg-title">${hasRepost ? "Step 1: Follow" : "Follow to Continue"}</h2>
|
|
598
|
+
<p class="fg-subtitle" id="fg-follow-subtitle">Follow @${handle} on X</p>
|
|
599
|
+
<div id="fg-follow-actions">
|
|
600
|
+
<button class="fg-btn fg-btn-dark" id="fg-follow-btn">
|
|
601
|
+
${ICONS.x}
|
|
602
|
+
Follow @${handle}
|
|
603
|
+
</button>
|
|
604
|
+
</div>
|
|
605
|
+
<p class="fg-hint">A new window will open. Return here after following.</p>
|
|
606
|
+
`;
|
|
607
|
+
document.getElementById("fg-follow-btn")?.addEventListener("click", () => {
|
|
608
|
+
this.handleFollowClick();
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
handleFollowClick() {
|
|
612
|
+
if (!this.config?.twitter) return;
|
|
613
|
+
const handle = this.config.twitter.handle;
|
|
614
|
+
this.openIntent({
|
|
615
|
+
platform: "twitter",
|
|
616
|
+
action: "follow",
|
|
617
|
+
target: handle
|
|
618
|
+
});
|
|
619
|
+
this.showFollowConfirmation();
|
|
620
|
+
}
|
|
621
|
+
showFollowConfirmation() {
|
|
622
|
+
if (!this.config?.twitter) return;
|
|
623
|
+
const handle = this.config.twitter.handle;
|
|
624
|
+
const subtitle = document.getElementById("fg-follow-subtitle");
|
|
625
|
+
const actions = document.getElementById("fg-follow-actions");
|
|
626
|
+
if (subtitle) {
|
|
627
|
+
subtitle.textContent = `Did you follow @${handle}?`;
|
|
628
|
+
}
|
|
629
|
+
if (actions) {
|
|
630
|
+
let seconds = WAIT_TIME_SECONDS;
|
|
631
|
+
actions.innerHTML = `
|
|
632
|
+
<div class="fg-info-box">
|
|
633
|
+
<p>You can close the X window after following</p>
|
|
634
|
+
</div>
|
|
635
|
+
<button class="fg-btn fg-btn-green" id="fg-follow-confirm" disabled>
|
|
636
|
+
<span class="fg-spinner"></span>
|
|
637
|
+
Waiting... (${seconds}s)
|
|
638
|
+
</button>
|
|
639
|
+
<button class="fg-btn fg-btn-secondary" id="fg-follow-retry">
|
|
640
|
+
${ICONS.x}
|
|
641
|
+
Open again
|
|
642
|
+
</button>
|
|
643
|
+
`;
|
|
644
|
+
const confirmBtn = document.getElementById("fg-follow-confirm");
|
|
645
|
+
const retryBtn = document.getElementById("fg-follow-retry");
|
|
646
|
+
const interval = setInterval(() => {
|
|
647
|
+
seconds--;
|
|
648
|
+
if (seconds <= 0) {
|
|
649
|
+
clearInterval(interval);
|
|
650
|
+
if (confirmBtn) {
|
|
651
|
+
confirmBtn.disabled = false;
|
|
652
|
+
confirmBtn.innerHTML = `${ICONS.check} Yes, I followed`;
|
|
653
|
+
}
|
|
654
|
+
} else if (confirmBtn) {
|
|
655
|
+
confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
|
|
656
|
+
}
|
|
657
|
+
}, 1e3);
|
|
658
|
+
confirmBtn?.addEventListener("click", () => {
|
|
659
|
+
this.handleFollowConfirm();
|
|
660
|
+
});
|
|
661
|
+
retryBtn?.addEventListener("click", () => {
|
|
662
|
+
this.handleFollowClick();
|
|
663
|
+
});
|
|
155
664
|
}
|
|
156
665
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
666
|
+
async handleFollowConfirm() {
|
|
667
|
+
if (!this.config?.twitter) return;
|
|
668
|
+
const handle = this.config.twitter.handle;
|
|
669
|
+
await this.complete({
|
|
670
|
+
platform: "twitter",
|
|
671
|
+
action: "follow",
|
|
672
|
+
target: handle
|
|
673
|
+
});
|
|
674
|
+
if (this.config.twitter.tweetId) {
|
|
675
|
+
this.renderRepostStep();
|
|
676
|
+
} else {
|
|
677
|
+
this.renderConfirmStep();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
renderRepostStep() {
|
|
681
|
+
const content = this.getContentElement();
|
|
682
|
+
if (!content || !this.config?.twitter?.tweetId) return;
|
|
683
|
+
content.innerHTML = `
|
|
684
|
+
${this.renderStepIndicator(2)}
|
|
685
|
+
<div class="fg-icon-box fg-success">
|
|
686
|
+
${ICONS.repost}
|
|
687
|
+
</div>
|
|
688
|
+
${this.currentUser ? `
|
|
689
|
+
<div class="fg-user-badge">
|
|
690
|
+
<div class="fg-user-badge-dot"></div>
|
|
691
|
+
<span class="fg-user-badge-text">@${this.currentUser.username}</span>
|
|
692
|
+
</div>
|
|
693
|
+
` : ""}
|
|
694
|
+
<h2 class="fg-title">Step 2: Repost</h2>
|
|
695
|
+
<p class="fg-subtitle" id="fg-repost-subtitle">Repost the tweet to continue</p>
|
|
696
|
+
<div id="fg-repost-actions">
|
|
697
|
+
<button class="fg-btn fg-btn-green" id="fg-repost-btn">
|
|
698
|
+
${ICONS.repost}
|
|
699
|
+
Repost tweet
|
|
700
|
+
</button>
|
|
701
|
+
</div>
|
|
702
|
+
<p class="fg-hint">A new window will open. Return here after reposting.</p>
|
|
703
|
+
`;
|
|
704
|
+
document.getElementById("fg-repost-btn")?.addEventListener("click", () => {
|
|
705
|
+
this.handleRepostClick();
|
|
706
|
+
});
|
|
162
707
|
}
|
|
708
|
+
handleRepostClick() {
|
|
709
|
+
if (!this.config?.twitter?.tweetId) return;
|
|
710
|
+
const tweetId = this.config.twitter.tweetId;
|
|
711
|
+
this.openIntent({
|
|
712
|
+
platform: "twitter",
|
|
713
|
+
action: "repost",
|
|
714
|
+
target: tweetId
|
|
715
|
+
});
|
|
716
|
+
this.showRepostConfirmation();
|
|
717
|
+
}
|
|
718
|
+
showRepostConfirmation() {
|
|
719
|
+
const subtitle = document.getElementById("fg-repost-subtitle");
|
|
720
|
+
const actions = document.getElementById("fg-repost-actions");
|
|
721
|
+
if (subtitle) {
|
|
722
|
+
subtitle.textContent = "Did you repost the tweet?";
|
|
723
|
+
}
|
|
724
|
+
if (actions) {
|
|
725
|
+
let seconds = WAIT_TIME_SECONDS;
|
|
726
|
+
actions.innerHTML = `
|
|
727
|
+
<div class="fg-info-box">
|
|
728
|
+
<p>You can close the X window after reposting</p>
|
|
729
|
+
</div>
|
|
730
|
+
<button class="fg-btn fg-btn-green" id="fg-repost-confirm" disabled>
|
|
731
|
+
<span class="fg-spinner"></span>
|
|
732
|
+
Waiting... (${seconds}s)
|
|
733
|
+
</button>
|
|
734
|
+
<button class="fg-btn fg-btn-secondary" id="fg-repost-retry">
|
|
735
|
+
${ICONS.repost}
|
|
736
|
+
Open tweet again
|
|
737
|
+
</button>
|
|
738
|
+
`;
|
|
739
|
+
const confirmBtn = document.getElementById("fg-repost-confirm");
|
|
740
|
+
const retryBtn = document.getElementById("fg-repost-retry");
|
|
741
|
+
const interval = setInterval(() => {
|
|
742
|
+
seconds--;
|
|
743
|
+
if (seconds <= 0) {
|
|
744
|
+
clearInterval(interval);
|
|
745
|
+
if (confirmBtn) {
|
|
746
|
+
confirmBtn.disabled = false;
|
|
747
|
+
confirmBtn.innerHTML = `${ICONS.check} Yes, I reposted`;
|
|
748
|
+
}
|
|
749
|
+
} else if (confirmBtn) {
|
|
750
|
+
confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
|
|
751
|
+
}
|
|
752
|
+
}, 1e3);
|
|
753
|
+
confirmBtn?.addEventListener("click", () => {
|
|
754
|
+
this.handleRepostConfirm();
|
|
755
|
+
});
|
|
756
|
+
retryBtn?.addEventListener("click", () => {
|
|
757
|
+
this.handleRepostClick();
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async handleRepostConfirm() {
|
|
762
|
+
if (!this.config?.twitter?.tweetId) return;
|
|
763
|
+
await this.complete({
|
|
764
|
+
platform: "twitter",
|
|
765
|
+
action: "repost",
|
|
766
|
+
target: this.config.twitter.tweetId
|
|
767
|
+
});
|
|
768
|
+
this.renderConfirmStep();
|
|
769
|
+
}
|
|
770
|
+
renderConfirmStep() {
|
|
771
|
+
const content = this.getContentElement();
|
|
772
|
+
if (!content) return;
|
|
773
|
+
content.innerHTML = `
|
|
774
|
+
<h2 class="fg-title">Almost done!</h2>
|
|
775
|
+
<div class="fg-warning-box">
|
|
776
|
+
${ICONS.warning}
|
|
777
|
+
<p><strong>Note:</strong> Access may be revoked if actions are not completed.</p>
|
|
778
|
+
</div>
|
|
779
|
+
<button class="fg-btn fg-btn-green" id="fg-finish-btn">
|
|
780
|
+
${ICONS.check}
|
|
781
|
+
Got it
|
|
782
|
+
</button>
|
|
783
|
+
${this.config?.twitter ? `
|
|
784
|
+
<div class="fg-btn-row">
|
|
785
|
+
<button class="fg-btn fg-btn-secondary" id="fg-redo-follow">
|
|
786
|
+
${ICONS.x}
|
|
787
|
+
Open follow
|
|
788
|
+
</button>
|
|
789
|
+
${this.config.twitter.tweetId ? `
|
|
790
|
+
<button class="fg-btn fg-btn-secondary" id="fg-redo-repost">
|
|
791
|
+
${ICONS.repost}
|
|
792
|
+
Open repost
|
|
793
|
+
</button>
|
|
794
|
+
` : ""}
|
|
795
|
+
</div>
|
|
796
|
+
` : ""}
|
|
797
|
+
`;
|
|
798
|
+
document.getElementById("fg-finish-btn")?.addEventListener("click", () => {
|
|
799
|
+
this.handleFinish();
|
|
800
|
+
});
|
|
801
|
+
document.getElementById("fg-redo-follow")?.addEventListener("click", () => {
|
|
802
|
+
if (this.config?.twitter) {
|
|
803
|
+
this.openIntent({
|
|
804
|
+
platform: "twitter",
|
|
805
|
+
action: "follow",
|
|
806
|
+
target: this.config.twitter.handle
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
document.getElementById("fg-redo-repost")?.addEventListener("click", () => {
|
|
811
|
+
if (this.config?.twitter?.tweetId) {
|
|
812
|
+
this.openIntent({
|
|
813
|
+
platform: "twitter",
|
|
814
|
+
action: "repost",
|
|
815
|
+
target: this.config.twitter.tweetId
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
async handleFinish() {
|
|
821
|
+
await this.unlock();
|
|
822
|
+
this.hide();
|
|
823
|
+
this.config?.onComplete?.();
|
|
824
|
+
}
|
|
825
|
+
renderStepIndicator(currentStep) {
|
|
826
|
+
return `
|
|
827
|
+
<div class="fg-step-indicator">
|
|
828
|
+
<div class="fg-step-dot ${currentStep > 1 ? "fg-done" : currentStep === 1 ? "fg-active" : "fg-pending"}">
|
|
829
|
+
${currentStep > 1 ? "\u2713" : "1"}
|
|
830
|
+
</div>
|
|
831
|
+
<div class="fg-step-line ${currentStep > 1 ? "fg-done" : ""}"></div>
|
|
832
|
+
<div class="fg-step-dot ${currentStep > 2 ? "fg-done" : currentStep === 2 ? "fg-active" : "fg-pending"}">
|
|
833
|
+
${currentStep > 2 ? "\u2713" : "2"}
|
|
834
|
+
</div>
|
|
835
|
+
</div>
|
|
836
|
+
`;
|
|
837
|
+
}
|
|
838
|
+
// ============================================
|
|
839
|
+
// Core SDK Methods
|
|
840
|
+
// ============================================
|
|
163
841
|
/**
|
|
164
|
-
* Set
|
|
842
|
+
* Set the user's social username
|
|
165
843
|
*/
|
|
166
|
-
setUsername(username) {
|
|
167
|
-
if (!this.
|
|
168
|
-
throw new Error(
|
|
169
|
-
"[FollowGate] No pending username state. User is either not authenticated or username is already set."
|
|
170
|
-
);
|
|
844
|
+
setUsername(username, platform = "twitter") {
|
|
845
|
+
if (!this.config) {
|
|
846
|
+
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
171
847
|
}
|
|
172
848
|
const normalizedUsername = username.startsWith("@") ? username.slice(1) : username;
|
|
173
849
|
this.currentUser = {
|
|
174
|
-
userId: "user_input",
|
|
175
850
|
username: normalizedUsername,
|
|
176
|
-
platform
|
|
851
|
+
platform
|
|
177
852
|
};
|
|
178
|
-
this.authToken = this.pendingUsername.token;
|
|
179
853
|
if (typeof localStorage !== "undefined") {
|
|
180
|
-
localStorage.setItem("followgate_token", this.authToken);
|
|
181
854
|
localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
|
|
182
|
-
localStorage.removeItem("followgate_pending_username");
|
|
183
855
|
}
|
|
184
|
-
this.
|
|
185
|
-
|
|
186
|
-
if (this.config?.debug) {
|
|
187
|
-
console.log("[FollowGate] Username set manually:", normalizedUsername);
|
|
856
|
+
if (this.config.debug) {
|
|
857
|
+
console.log("[FollowGate] Username set:", normalizedUsername);
|
|
188
858
|
}
|
|
189
859
|
}
|
|
190
860
|
/**
|
|
191
|
-
*
|
|
861
|
+
* Get current user
|
|
192
862
|
*/
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
}
|
|
863
|
+
getUser() {
|
|
864
|
+
return this.currentUser;
|
|
241
865
|
}
|
|
242
866
|
/**
|
|
243
|
-
*
|
|
867
|
+
* Check if username is set
|
|
244
868
|
*/
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
}
|
|
258
|
-
}
|
|
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
|
-
}
|
|
269
|
-
}
|
|
869
|
+
hasUsername() {
|
|
870
|
+
return this.currentUser !== null;
|
|
270
871
|
}
|
|
271
872
|
/**
|
|
272
|
-
*
|
|
873
|
+
* Clear stored session
|
|
273
874
|
*/
|
|
274
|
-
|
|
875
|
+
reset() {
|
|
876
|
+
this.currentUser = null;
|
|
877
|
+
this.completedActions = [];
|
|
878
|
+
if (typeof localStorage !== "undefined") {
|
|
879
|
+
localStorage.removeItem("followgate_user");
|
|
880
|
+
localStorage.removeItem("followgate_actions");
|
|
881
|
+
localStorage.removeItem("followgate_unlocked");
|
|
882
|
+
}
|
|
883
|
+
if (this.config?.debug) {
|
|
884
|
+
console.log("[FollowGate] Session reset");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// ============================================
|
|
888
|
+
// Intent URL Methods
|
|
889
|
+
// ============================================
|
|
890
|
+
getFollowUrl(platform, target) {
|
|
891
|
+
return this.buildIntentUrl({ platform, action: "follow", target });
|
|
892
|
+
}
|
|
893
|
+
getRepostUrl(platform, target) {
|
|
894
|
+
return this.buildIntentUrl({ platform, action: "repost", target });
|
|
895
|
+
}
|
|
896
|
+
getLikeUrl(platform, target) {
|
|
897
|
+
return this.buildIntentUrl({ platform, action: "like", target });
|
|
898
|
+
}
|
|
899
|
+
async openIntent(options) {
|
|
275
900
|
if (!this.config) {
|
|
276
901
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
277
902
|
}
|
|
278
903
|
const url = this.buildIntentUrl(options);
|
|
279
904
|
if (this.config.debug) {
|
|
280
|
-
console.log("[FollowGate] Opening:", url);
|
|
905
|
+
console.log("[FollowGate] Opening intent:", url);
|
|
281
906
|
}
|
|
282
|
-
await this.trackEvent("
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
907
|
+
await this.trackEvent("intent_opened", { ...options });
|
|
908
|
+
window.open(url, "_blank", "width=600,height=700");
|
|
909
|
+
}
|
|
910
|
+
// ============================================
|
|
911
|
+
// Completion Methods
|
|
912
|
+
// ============================================
|
|
913
|
+
async complete(options) {
|
|
914
|
+
if (!this.config) {
|
|
915
|
+
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
916
|
+
}
|
|
917
|
+
if (!this.currentUser) {
|
|
918
|
+
throw new Error("[FollowGate] No username set. Call setUsername() first.");
|
|
919
|
+
}
|
|
920
|
+
const alreadyCompleted = this.completedActions.some(
|
|
921
|
+
(a) => a.platform === options.platform && a.action === options.action && a.target === options.target
|
|
922
|
+
);
|
|
923
|
+
if (!alreadyCompleted) {
|
|
924
|
+
this.completedActions.push(options);
|
|
925
|
+
this.saveCompletedActions();
|
|
287
926
|
}
|
|
288
|
-
await this.trackEvent("
|
|
927
|
+
await this.trackEvent("action_completed", {
|
|
928
|
+
...options,
|
|
929
|
+
username: this.currentUser.username
|
|
930
|
+
});
|
|
289
931
|
this.emit("complete", {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
target: options.target
|
|
932
|
+
...options,
|
|
933
|
+
username: this.currentUser.username
|
|
293
934
|
});
|
|
935
|
+
if (this.config.debug) {
|
|
936
|
+
console.log("[FollowGate] Action completed:", options);
|
|
937
|
+
}
|
|
294
938
|
}
|
|
295
|
-
|
|
296
|
-
* Verify follow status (for Pro/Business tiers with OAuth)
|
|
297
|
-
*/
|
|
298
|
-
async verify(options) {
|
|
939
|
+
async unlock() {
|
|
299
940
|
if (!this.config) {
|
|
300
941
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
301
942
|
}
|
|
302
|
-
|
|
303
|
-
|
|
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;
|
|
943
|
+
if (typeof localStorage !== "undefined") {
|
|
944
|
+
localStorage.setItem("followgate_unlocked", "true");
|
|
323
945
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
}
|
|
946
|
+
await this.trackEvent("gate_unlocked", {
|
|
947
|
+
username: this.currentUser?.username,
|
|
948
|
+
actions: this.completedActions
|
|
949
|
+
});
|
|
950
|
+
this.emit("unlocked", {
|
|
951
|
+
username: this.currentUser?.username,
|
|
952
|
+
actions: this.completedActions
|
|
953
|
+
});
|
|
954
|
+
if (this.config.debug) {
|
|
955
|
+
console.log("[FollowGate] Gate unlocked!");
|
|
349
956
|
}
|
|
350
957
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
958
|
+
isUnlocked() {
|
|
959
|
+
if (typeof localStorage === "undefined") return false;
|
|
960
|
+
return localStorage.getItem("followgate_unlocked") === "true";
|
|
961
|
+
}
|
|
962
|
+
getUnlockStatus() {
|
|
963
|
+
return {
|
|
964
|
+
unlocked: this.isUnlocked(),
|
|
965
|
+
username: this.currentUser?.username,
|
|
966
|
+
completedActions: [...this.completedActions]
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
getCompletedActions() {
|
|
970
|
+
return [...this.completedActions];
|
|
971
|
+
}
|
|
972
|
+
// ============================================
|
|
973
|
+
// Event System
|
|
974
|
+
// ============================================
|
|
354
975
|
on(event, callback) {
|
|
355
976
|
if (!this.listeners.has(event)) {
|
|
356
977
|
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
357
978
|
}
|
|
358
979
|
this.listeners.get(event).add(callback);
|
|
359
980
|
}
|
|
360
|
-
/**
|
|
361
|
-
* Remove event listener
|
|
362
|
-
*/
|
|
363
981
|
off(event, callback) {
|
|
364
982
|
this.listeners.get(event)?.delete(callback);
|
|
365
983
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
984
|
+
// ============================================
|
|
985
|
+
// Private Methods
|
|
986
|
+
// ============================================
|
|
987
|
+
restoreSession() {
|
|
988
|
+
if (typeof localStorage === "undefined") return;
|
|
989
|
+
const userJson = localStorage.getItem("followgate_user");
|
|
990
|
+
if (userJson) {
|
|
991
|
+
try {
|
|
992
|
+
this.currentUser = JSON.parse(userJson);
|
|
993
|
+
} catch {
|
|
994
|
+
localStorage.removeItem("followgate_user");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const actionsJson = localStorage.getItem("followgate_actions");
|
|
998
|
+
if (actionsJson) {
|
|
999
|
+
try {
|
|
1000
|
+
this.completedActions = JSON.parse(actionsJson);
|
|
1001
|
+
} catch {
|
|
1002
|
+
localStorage.removeItem("followgate_actions");
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
saveCompletedActions() {
|
|
1007
|
+
if (typeof localStorage !== "undefined") {
|
|
1008
|
+
localStorage.setItem(
|
|
1009
|
+
"followgate_actions",
|
|
1010
|
+
JSON.stringify(this.completedActions)
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
369
1014
|
buildIntentUrl(options) {
|
|
370
1015
|
const { platform, action, target } = options;
|
|
371
1016
|
switch (platform) {
|
|
@@ -424,6 +1069,27 @@ var FollowGateClient = class {
|
|
|
424
1069
|
throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
|
|
425
1070
|
}
|
|
426
1071
|
}
|
|
1072
|
+
async trackEvent(event, data) {
|
|
1073
|
+
if (!this.config) return;
|
|
1074
|
+
try {
|
|
1075
|
+
await fetch(`${this.config.apiUrl}/api/v1/events`, {
|
|
1076
|
+
method: "POST",
|
|
1077
|
+
headers: {
|
|
1078
|
+
"Content-Type": "application/json",
|
|
1079
|
+
"X-API-Key": this.config.apiKey
|
|
1080
|
+
},
|
|
1081
|
+
body: JSON.stringify({
|
|
1082
|
+
event,
|
|
1083
|
+
appId: this.config.appId,
|
|
1084
|
+
...data
|
|
1085
|
+
})
|
|
1086
|
+
});
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
if (this.config.debug) {
|
|
1089
|
+
console.warn("[FollowGate] Failed to track event:", error);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
427
1093
|
emit(event, data) {
|
|
428
1094
|
this.listeners.get(event)?.forEach((callback) => callback(data));
|
|
429
1095
|
}
|