@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.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,335 +12,979 @@ 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
|
-
|
|
19
|
-
|
|
387
|
+
completedActions = [];
|
|
388
|
+
modalElement = null;
|
|
389
|
+
stylesInjected = false;
|
|
20
390
|
/**
|
|
21
391
|
* Initialize the SDK
|
|
22
|
-
* @throws {FollowGateError} If configuration is invalid
|
|
23
392
|
*/
|
|
24
393
|
init(config) {
|
|
25
394
|
if (!config.appId || typeof config.appId !== "string") {
|
|
26
395
|
throw new FollowGateError(
|
|
27
396
|
"[FollowGate] Missing or invalid appId",
|
|
28
397
|
"INVALID_APP_ID",
|
|
29
|
-
"Get your App ID from https://followgate.app/dashboard
|
|
398
|
+
"Get your App ID from https://followgate.app/dashboard"
|
|
30
399
|
);
|
|
31
400
|
}
|
|
32
401
|
if (config.appId.trim() === "" || config.appId === "undefined") {
|
|
33
402
|
throw new FollowGateError(
|
|
34
403
|
"[FollowGate] appId is empty or undefined",
|
|
35
404
|
"EMPTY_APP_ID",
|
|
36
|
-
"
|
|
405
|
+
"Check that NEXT_PUBLIC_FOLLOWGATE_APP_ID is set"
|
|
37
406
|
);
|
|
38
407
|
}
|
|
39
408
|
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
40
409
|
throw new FollowGateError(
|
|
41
410
|
"[FollowGate] Missing or invalid apiKey",
|
|
42
411
|
"INVALID_API_KEY",
|
|
43
|
-
"Get your API Key from https://followgate.app/dashboard
|
|
412
|
+
"Get your API Key from https://followgate.app/dashboard"
|
|
44
413
|
);
|
|
45
414
|
}
|
|
46
415
|
if (config.apiKey.trim() === "" || config.apiKey === "undefined") {
|
|
47
416
|
throw new FollowGateError(
|
|
48
417
|
"[FollowGate] apiKey is empty or undefined",
|
|
49
418
|
"EMPTY_API_KEY",
|
|
50
|
-
"
|
|
419
|
+
"Set NEXT_PUBLIC_FOLLOWGATE_API_KEY and rebuild"
|
|
51
420
|
);
|
|
52
421
|
}
|
|
53
422
|
if (!isValidApiKeyFormat(config.apiKey)) {
|
|
54
423
|
throw new FollowGateError(
|
|
55
|
-
`[FollowGate] Invalid API key format
|
|
424
|
+
`[FollowGate] Invalid API key format`,
|
|
56
425
|
"INVALID_API_KEY_FORMAT",
|
|
57
|
-
'API keys should start with "fg_live_"
|
|
426
|
+
'API keys should start with "fg_live_" or "fg_test_"'
|
|
58
427
|
);
|
|
59
428
|
}
|
|
60
429
|
this.config = {
|
|
61
430
|
...config,
|
|
62
|
-
apiUrl: config.apiUrl || DEFAULT_API_URL
|
|
431
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
432
|
+
theme: config.theme || "dark",
|
|
433
|
+
accentColor: config.accentColor || "#6366f1"
|
|
63
434
|
};
|
|
64
|
-
this.handleAuthCallback();
|
|
65
435
|
this.restoreSession();
|
|
66
436
|
if (config.debug) {
|
|
67
437
|
console.log("[FollowGate] Initialized with appId:", config.appId);
|
|
68
|
-
console.log(
|
|
69
|
-
"[FollowGate] API Key:",
|
|
70
|
-
config.apiKey.substring(0, 12) + "..."
|
|
71
|
-
);
|
|
72
438
|
if (this.currentUser) {
|
|
73
439
|
console.log("[FollowGate] Restored user:", this.currentUser.username);
|
|
74
440
|
}
|
|
75
441
|
}
|
|
76
442
|
}
|
|
443
|
+
// ============================================
|
|
444
|
+
// Modal UI Methods
|
|
445
|
+
// ============================================
|
|
77
446
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
447
|
+
* Show the FollowGate modal
|
|
448
|
+
* If user is already unlocked, calls onComplete immediately
|
|
80
449
|
*/
|
|
81
|
-
|
|
450
|
+
show() {
|
|
82
451
|
if (!this.config) {
|
|
83
452
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
84
453
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
console.log("[FollowGate] Starting auth flow:", authUrl);
|
|
89
|
-
}
|
|
90
|
-
if (options.popup) {
|
|
91
|
-
const popup = window.open(
|
|
92
|
-
authUrl,
|
|
93
|
-
"followgate_auth",
|
|
94
|
-
"width=600,height=700"
|
|
95
|
-
);
|
|
96
|
-
if (!popup) {
|
|
97
|
-
this.emit("error", { message: "Popup blocked" });
|
|
454
|
+
if (this.isUnlocked()) {
|
|
455
|
+
if (this.config.debug) {
|
|
456
|
+
console.log("[FollowGate] Already unlocked, skipping modal");
|
|
98
457
|
}
|
|
99
|
-
|
|
100
|
-
|
|
458
|
+
this.config.onComplete?.();
|
|
459
|
+
return;
|
|
101
460
|
}
|
|
461
|
+
this.injectStyles();
|
|
462
|
+
this.createModal();
|
|
102
463
|
}
|
|
103
464
|
/**
|
|
104
|
-
*
|
|
465
|
+
* Hide the modal
|
|
105
466
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
475
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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("--fg-accent", this.config.accentColor);
|
|
485
|
+
document.documentElement.style.setProperty("--fg-accent-hover", this.config.accentColor);
|
|
486
|
+
}
|
|
487
|
+
this.stylesInjected = true;
|
|
114
488
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
489
|
+
createModal() {
|
|
490
|
+
if (typeof document === "undefined") return;
|
|
491
|
+
const backdrop = document.createElement("div");
|
|
492
|
+
backdrop.className = "fg-modal-backdrop";
|
|
493
|
+
backdrop.innerHTML = `
|
|
494
|
+
<div class="fg-modal">
|
|
495
|
+
<div id="fg-content"></div>
|
|
496
|
+
<div class="fg-footer">
|
|
497
|
+
<p>Powered by <a href="https://followgate.app" target="_blank" rel="noopener">FollowGate</a></p>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
`;
|
|
501
|
+
document.body.appendChild(backdrop);
|
|
502
|
+
this.modalElement = backdrop;
|
|
503
|
+
requestAnimationFrame(() => {
|
|
504
|
+
backdrop.classList.add("fg-visible");
|
|
505
|
+
});
|
|
506
|
+
if (this.hasUsername()) {
|
|
507
|
+
this.renderFollowStep();
|
|
508
|
+
} else {
|
|
509
|
+
this.renderUsernameStep();
|
|
126
510
|
}
|
|
127
|
-
|
|
128
|
-
|
|
511
|
+
}
|
|
512
|
+
getContentElement() {
|
|
513
|
+
return document.getElementById("fg-content");
|
|
514
|
+
}
|
|
515
|
+
renderUsernameStep() {
|
|
516
|
+
const content = this.getContentElement();
|
|
517
|
+
if (!content) return;
|
|
518
|
+
content.innerHTML = `
|
|
519
|
+
<div class="fg-icon-box">
|
|
520
|
+
${ICONS.x}
|
|
521
|
+
</div>
|
|
522
|
+
<h2 class="fg-title">Unlock Free Access</h2>
|
|
523
|
+
<p class="fg-subtitle">Enter your X username to get started</p>
|
|
524
|
+
<div class="fg-input-wrapper">
|
|
525
|
+
<span class="fg-input-prefix">@</span>
|
|
526
|
+
<input type="text" class="fg-input" id="fg-username-input" placeholder="your_username" autofocus>
|
|
527
|
+
</div>
|
|
528
|
+
<button class="fg-btn fg-btn-primary" id="fg-username-submit" disabled>
|
|
529
|
+
Continue
|
|
530
|
+
</button>
|
|
531
|
+
<p class="fg-hint">No login required \u2013 just your username</p>
|
|
532
|
+
`;
|
|
533
|
+
const input = document.getElementById("fg-username-input");
|
|
534
|
+
const btn = document.getElementById("fg-username-submit");
|
|
535
|
+
input?.addEventListener("input", () => {
|
|
536
|
+
btn.disabled = !input.value.trim();
|
|
537
|
+
});
|
|
538
|
+
input?.addEventListener("keydown", (e) => {
|
|
539
|
+
if (e.key === "Enter" && input.value.trim()) {
|
|
540
|
+
this.handleUsernameSubmit(input.value.trim());
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
btn?.addEventListener("click", () => {
|
|
544
|
+
if (input?.value.trim()) {
|
|
545
|
+
this.handleUsernameSubmit(input.value.trim());
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
setTimeout(() => input?.focus(), 100);
|
|
549
|
+
}
|
|
550
|
+
handleUsernameSubmit(username) {
|
|
551
|
+
const normalized = username.replace(/^@/, "");
|
|
552
|
+
this.setUsername(normalized);
|
|
553
|
+
this.renderFollowStep();
|
|
554
|
+
}
|
|
555
|
+
renderFollowStep() {
|
|
556
|
+
const content = this.getContentElement();
|
|
557
|
+
if (!content || !this.config?.twitter) return;
|
|
558
|
+
const handle = this.config.twitter.handle;
|
|
559
|
+
const hasRepost = !!this.config.twitter.tweetId;
|
|
560
|
+
content.innerHTML = `
|
|
561
|
+
${hasRepost ? this.renderStepIndicator(1) : ""}
|
|
562
|
+
<div class="fg-icon-box">
|
|
563
|
+
${ICONS.x}
|
|
564
|
+
</div>
|
|
565
|
+
${this.currentUser ? `
|
|
566
|
+
<div class="fg-user-badge">
|
|
567
|
+
<div class="fg-user-badge-dot"></div>
|
|
568
|
+
<span class="fg-user-badge-text">@${this.currentUser.username}</span>
|
|
569
|
+
</div>
|
|
570
|
+
` : ""}
|
|
571
|
+
<h2 class="fg-title">${hasRepost ? "Step 1: Follow" : "Follow to Continue"}</h2>
|
|
572
|
+
<p class="fg-subtitle" id="fg-follow-subtitle">Follow @${handle} on X</p>
|
|
573
|
+
<div id="fg-follow-actions">
|
|
574
|
+
<button class="fg-btn fg-btn-dark" id="fg-follow-btn">
|
|
575
|
+
${ICONS.x}
|
|
576
|
+
Follow @${handle}
|
|
577
|
+
</button>
|
|
578
|
+
</div>
|
|
579
|
+
<p class="fg-hint">A new window will open. Return here after following.</p>
|
|
580
|
+
`;
|
|
581
|
+
document.getElementById("fg-follow-btn")?.addEventListener("click", () => {
|
|
582
|
+
this.handleFollowClick();
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
handleFollowClick() {
|
|
586
|
+
if (!this.config?.twitter) return;
|
|
587
|
+
const handle = this.config.twitter.handle;
|
|
588
|
+
this.openIntent({
|
|
589
|
+
platform: "twitter",
|
|
590
|
+
action: "follow",
|
|
591
|
+
target: handle
|
|
592
|
+
});
|
|
593
|
+
this.showFollowConfirmation();
|
|
594
|
+
}
|
|
595
|
+
showFollowConfirmation() {
|
|
596
|
+
if (!this.config?.twitter) return;
|
|
597
|
+
const handle = this.config.twitter.handle;
|
|
598
|
+
const subtitle = document.getElementById("fg-follow-subtitle");
|
|
599
|
+
const actions = document.getElementById("fg-follow-actions");
|
|
600
|
+
if (subtitle) {
|
|
601
|
+
subtitle.textContent = `Did you follow @${handle}?`;
|
|
602
|
+
}
|
|
603
|
+
if (actions) {
|
|
604
|
+
let seconds = WAIT_TIME_SECONDS;
|
|
605
|
+
actions.innerHTML = `
|
|
606
|
+
<div class="fg-info-box">
|
|
607
|
+
<p>You can close the X window after following</p>
|
|
608
|
+
</div>
|
|
609
|
+
<button class="fg-btn fg-btn-green" id="fg-follow-confirm" disabled>
|
|
610
|
+
<span class="fg-spinner"></span>
|
|
611
|
+
Waiting... (${seconds}s)
|
|
612
|
+
</button>
|
|
613
|
+
<button class="fg-btn fg-btn-secondary" id="fg-follow-retry">
|
|
614
|
+
${ICONS.x}
|
|
615
|
+
Open again
|
|
616
|
+
</button>
|
|
617
|
+
`;
|
|
618
|
+
const confirmBtn = document.getElementById("fg-follow-confirm");
|
|
619
|
+
const retryBtn = document.getElementById("fg-follow-retry");
|
|
620
|
+
const interval = setInterval(() => {
|
|
621
|
+
seconds--;
|
|
622
|
+
if (seconds <= 0) {
|
|
623
|
+
clearInterval(interval);
|
|
624
|
+
if (confirmBtn) {
|
|
625
|
+
confirmBtn.disabled = false;
|
|
626
|
+
confirmBtn.innerHTML = `${ICONS.check} Yes, I followed`;
|
|
627
|
+
}
|
|
628
|
+
} else if (confirmBtn) {
|
|
629
|
+
confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
|
|
630
|
+
}
|
|
631
|
+
}, 1e3);
|
|
632
|
+
confirmBtn?.addEventListener("click", () => {
|
|
633
|
+
this.handleFollowConfirm();
|
|
634
|
+
});
|
|
635
|
+
retryBtn?.addEventListener("click", () => {
|
|
636
|
+
this.handleFollowClick();
|
|
637
|
+
});
|
|
129
638
|
}
|
|
130
639
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
640
|
+
async handleFollowConfirm() {
|
|
641
|
+
if (!this.config?.twitter) return;
|
|
642
|
+
const handle = this.config.twitter.handle;
|
|
643
|
+
await this.complete({
|
|
644
|
+
platform: "twitter",
|
|
645
|
+
action: "follow",
|
|
646
|
+
target: handle
|
|
647
|
+
});
|
|
648
|
+
if (this.config.twitter.tweetId) {
|
|
649
|
+
this.renderRepostStep();
|
|
650
|
+
} else {
|
|
651
|
+
this.renderConfirmStep();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
renderRepostStep() {
|
|
655
|
+
const content = this.getContentElement();
|
|
656
|
+
if (!content || !this.config?.twitter?.tweetId) return;
|
|
657
|
+
content.innerHTML = `
|
|
658
|
+
${this.renderStepIndicator(2)}
|
|
659
|
+
<div class="fg-icon-box fg-success">
|
|
660
|
+
${ICONS.repost}
|
|
661
|
+
</div>
|
|
662
|
+
${this.currentUser ? `
|
|
663
|
+
<div class="fg-user-badge">
|
|
664
|
+
<div class="fg-user-badge-dot"></div>
|
|
665
|
+
<span class="fg-user-badge-text">@${this.currentUser.username}</span>
|
|
666
|
+
</div>
|
|
667
|
+
` : ""}
|
|
668
|
+
<h2 class="fg-title">Step 2: Repost</h2>
|
|
669
|
+
<p class="fg-subtitle" id="fg-repost-subtitle">Repost the tweet to continue</p>
|
|
670
|
+
<div id="fg-repost-actions">
|
|
671
|
+
<button class="fg-btn fg-btn-green" id="fg-repost-btn">
|
|
672
|
+
${ICONS.repost}
|
|
673
|
+
Repost tweet
|
|
674
|
+
</button>
|
|
675
|
+
</div>
|
|
676
|
+
<p class="fg-hint">A new window will open. Return here after reposting.</p>
|
|
677
|
+
`;
|
|
678
|
+
document.getElementById("fg-repost-btn")?.addEventListener("click", () => {
|
|
679
|
+
this.handleRepostClick();
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
handleRepostClick() {
|
|
683
|
+
if (!this.config?.twitter?.tweetId) return;
|
|
684
|
+
const tweetId = this.config.twitter.tweetId;
|
|
685
|
+
this.openIntent({
|
|
686
|
+
platform: "twitter",
|
|
687
|
+
action: "repost",
|
|
688
|
+
target: tweetId
|
|
689
|
+
});
|
|
690
|
+
this.showRepostConfirmation();
|
|
691
|
+
}
|
|
692
|
+
showRepostConfirmation() {
|
|
693
|
+
const subtitle = document.getElementById("fg-repost-subtitle");
|
|
694
|
+
const actions = document.getElementById("fg-repost-actions");
|
|
695
|
+
if (subtitle) {
|
|
696
|
+
subtitle.textContent = "Did you repost the tweet?";
|
|
697
|
+
}
|
|
698
|
+
if (actions) {
|
|
699
|
+
let seconds = WAIT_TIME_SECONDS;
|
|
700
|
+
actions.innerHTML = `
|
|
701
|
+
<div class="fg-info-box">
|
|
702
|
+
<p>You can close the X window after reposting</p>
|
|
703
|
+
</div>
|
|
704
|
+
<button class="fg-btn fg-btn-green" id="fg-repost-confirm" disabled>
|
|
705
|
+
<span class="fg-spinner"></span>
|
|
706
|
+
Waiting... (${seconds}s)
|
|
707
|
+
</button>
|
|
708
|
+
<button class="fg-btn fg-btn-secondary" id="fg-repost-retry">
|
|
709
|
+
${ICONS.repost}
|
|
710
|
+
Open tweet again
|
|
711
|
+
</button>
|
|
712
|
+
`;
|
|
713
|
+
const confirmBtn = document.getElementById("fg-repost-confirm");
|
|
714
|
+
const retryBtn = document.getElementById("fg-repost-retry");
|
|
715
|
+
const interval = setInterval(() => {
|
|
716
|
+
seconds--;
|
|
717
|
+
if (seconds <= 0) {
|
|
718
|
+
clearInterval(interval);
|
|
719
|
+
if (confirmBtn) {
|
|
720
|
+
confirmBtn.disabled = false;
|
|
721
|
+
confirmBtn.innerHTML = `${ICONS.check} Yes, I reposted`;
|
|
722
|
+
}
|
|
723
|
+
} else if (confirmBtn) {
|
|
724
|
+
confirmBtn.innerHTML = `<span class="fg-spinner"></span> Waiting... (${seconds}s)`;
|
|
725
|
+
}
|
|
726
|
+
}, 1e3);
|
|
727
|
+
confirmBtn?.addEventListener("click", () => {
|
|
728
|
+
this.handleRepostConfirm();
|
|
729
|
+
});
|
|
730
|
+
retryBtn?.addEventListener("click", () => {
|
|
731
|
+
this.handleRepostClick();
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async handleRepostConfirm() {
|
|
736
|
+
if (!this.config?.twitter?.tweetId) return;
|
|
737
|
+
await this.complete({
|
|
738
|
+
platform: "twitter",
|
|
739
|
+
action: "repost",
|
|
740
|
+
target: this.config.twitter.tweetId
|
|
741
|
+
});
|
|
742
|
+
this.renderConfirmStep();
|
|
136
743
|
}
|
|
744
|
+
renderConfirmStep() {
|
|
745
|
+
const content = this.getContentElement();
|
|
746
|
+
if (!content) return;
|
|
747
|
+
content.innerHTML = `
|
|
748
|
+
<h2 class="fg-title">Almost done!</h2>
|
|
749
|
+
<div class="fg-warning-box">
|
|
750
|
+
${ICONS.warning}
|
|
751
|
+
<p><strong>Note:</strong> Access may be revoked if actions are not completed.</p>
|
|
752
|
+
</div>
|
|
753
|
+
<button class="fg-btn fg-btn-green" id="fg-finish-btn">
|
|
754
|
+
${ICONS.check}
|
|
755
|
+
Got it
|
|
756
|
+
</button>
|
|
757
|
+
${this.config?.twitter ? `
|
|
758
|
+
<div class="fg-btn-row">
|
|
759
|
+
<button class="fg-btn fg-btn-secondary" id="fg-redo-follow">
|
|
760
|
+
${ICONS.x}
|
|
761
|
+
Open follow
|
|
762
|
+
</button>
|
|
763
|
+
${this.config.twitter.tweetId ? `
|
|
764
|
+
<button class="fg-btn fg-btn-secondary" id="fg-redo-repost">
|
|
765
|
+
${ICONS.repost}
|
|
766
|
+
Open repost
|
|
767
|
+
</button>
|
|
768
|
+
` : ""}
|
|
769
|
+
</div>
|
|
770
|
+
` : ""}
|
|
771
|
+
`;
|
|
772
|
+
document.getElementById("fg-finish-btn")?.addEventListener("click", () => {
|
|
773
|
+
this.handleFinish();
|
|
774
|
+
});
|
|
775
|
+
document.getElementById("fg-redo-follow")?.addEventListener("click", () => {
|
|
776
|
+
if (this.config?.twitter) {
|
|
777
|
+
this.openIntent({
|
|
778
|
+
platform: "twitter",
|
|
779
|
+
action: "follow",
|
|
780
|
+
target: this.config.twitter.handle
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
document.getElementById("fg-redo-repost")?.addEventListener("click", () => {
|
|
785
|
+
if (this.config?.twitter?.tweetId) {
|
|
786
|
+
this.openIntent({
|
|
787
|
+
platform: "twitter",
|
|
788
|
+
action: "repost",
|
|
789
|
+
target: this.config.twitter.tweetId
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async handleFinish() {
|
|
795
|
+
await this.unlock();
|
|
796
|
+
this.hide();
|
|
797
|
+
this.config?.onComplete?.();
|
|
798
|
+
}
|
|
799
|
+
renderStepIndicator(currentStep) {
|
|
800
|
+
return `
|
|
801
|
+
<div class="fg-step-indicator">
|
|
802
|
+
<div class="fg-step-dot ${currentStep > 1 ? "fg-done" : currentStep === 1 ? "fg-active" : "fg-pending"}">
|
|
803
|
+
${currentStep > 1 ? "\u2713" : "1"}
|
|
804
|
+
</div>
|
|
805
|
+
<div class="fg-step-line ${currentStep > 1 ? "fg-done" : ""}"></div>
|
|
806
|
+
<div class="fg-step-dot ${currentStep > 2 ? "fg-done" : currentStep === 2 ? "fg-active" : "fg-pending"}">
|
|
807
|
+
${currentStep > 2 ? "\u2713" : "2"}
|
|
808
|
+
</div>
|
|
809
|
+
</div>
|
|
810
|
+
`;
|
|
811
|
+
}
|
|
812
|
+
// ============================================
|
|
813
|
+
// Core SDK Methods
|
|
814
|
+
// ============================================
|
|
137
815
|
/**
|
|
138
|
-
* Set
|
|
816
|
+
* Set the user's social username
|
|
139
817
|
*/
|
|
140
|
-
setUsername(username) {
|
|
141
|
-
if (!this.
|
|
142
|
-
throw new Error(
|
|
143
|
-
"[FollowGate] No pending username state. User is either not authenticated or username is already set."
|
|
144
|
-
);
|
|
818
|
+
setUsername(username, platform = "twitter") {
|
|
819
|
+
if (!this.config) {
|
|
820
|
+
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
145
821
|
}
|
|
146
822
|
const normalizedUsername = username.startsWith("@") ? username.slice(1) : username;
|
|
147
823
|
this.currentUser = {
|
|
148
|
-
userId: "user_input",
|
|
149
824
|
username: normalizedUsername,
|
|
150
|
-
platform
|
|
825
|
+
platform
|
|
151
826
|
};
|
|
152
|
-
this.authToken = this.pendingUsername.token;
|
|
153
827
|
if (typeof localStorage !== "undefined") {
|
|
154
|
-
localStorage.setItem("followgate_token", this.authToken);
|
|
155
828
|
localStorage.setItem("followgate_user", JSON.stringify(this.currentUser));
|
|
156
|
-
localStorage.removeItem("followgate_pending_username");
|
|
157
829
|
}
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
if (this.config?.debug) {
|
|
161
|
-
console.log("[FollowGate] Username set manually:", normalizedUsername);
|
|
830
|
+
if (this.config.debug) {
|
|
831
|
+
console.log("[FollowGate] Username set:", normalizedUsername);
|
|
162
832
|
}
|
|
163
833
|
}
|
|
164
834
|
/**
|
|
165
|
-
*
|
|
835
|
+
* Get current user
|
|
166
836
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const params = new URLSearchParams(window.location.search);
|
|
170
|
-
const token = params.get("followgate_token");
|
|
171
|
-
const username = params.get("followgate_user");
|
|
172
|
-
const needsUsername = params.get("followgate_needs_username") === "true";
|
|
173
|
-
if (token && needsUsername) {
|
|
174
|
-
this.pendingUsername = {
|
|
175
|
-
needsUsername: true,
|
|
176
|
-
token
|
|
177
|
-
};
|
|
178
|
-
if (typeof localStorage !== "undefined") {
|
|
179
|
-
localStorage.setItem(
|
|
180
|
-
"followgate_pending_username",
|
|
181
|
-
JSON.stringify(this.pendingUsername)
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
const url = new URL(window.location.href);
|
|
185
|
-
url.searchParams.delete("followgate_token");
|
|
186
|
-
url.searchParams.delete("followgate_needs_username");
|
|
187
|
-
window.history.replaceState({}, "", url.toString());
|
|
188
|
-
if (this.config?.debug) {
|
|
189
|
-
console.log("[FollowGate] OAuth successful, username input needed");
|
|
190
|
-
}
|
|
191
|
-
} else if (token && username) {
|
|
192
|
-
this.authToken = token;
|
|
193
|
-
this.currentUser = {
|
|
194
|
-
userId: "",
|
|
195
|
-
// Will be set from token verification
|
|
196
|
-
username,
|
|
197
|
-
platform: "twitter"
|
|
198
|
-
};
|
|
199
|
-
if (typeof localStorage !== "undefined") {
|
|
200
|
-
localStorage.setItem("followgate_token", token);
|
|
201
|
-
localStorage.setItem(
|
|
202
|
-
"followgate_user",
|
|
203
|
-
JSON.stringify(this.currentUser)
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
const url = new URL(window.location.href);
|
|
207
|
-
url.searchParams.delete("followgate_token");
|
|
208
|
-
url.searchParams.delete("followgate_user");
|
|
209
|
-
window.history.replaceState({}, "", url.toString());
|
|
210
|
-
this.emit("authenticated", this.currentUser);
|
|
211
|
-
if (this.config?.debug) {
|
|
212
|
-
console.log("[FollowGate] User authenticated:", username);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
837
|
+
getUser() {
|
|
838
|
+
return this.currentUser;
|
|
215
839
|
}
|
|
216
840
|
/**
|
|
217
|
-
*
|
|
841
|
+
* Check if username is set
|
|
218
842
|
*/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const pendingJson = localStorage.getItem("followgate_pending_username");
|
|
222
|
-
if (pendingJson) {
|
|
223
|
-
try {
|
|
224
|
-
this.pendingUsername = JSON.parse(pendingJson);
|
|
225
|
-
if (this.config?.debug) {
|
|
226
|
-
console.log("[FollowGate] Restored pending username state");
|
|
227
|
-
}
|
|
228
|
-
return;
|
|
229
|
-
} catch {
|
|
230
|
-
localStorage.removeItem("followgate_pending_username");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
const token = localStorage.getItem("followgate_token");
|
|
234
|
-
const userJson = localStorage.getItem("followgate_user");
|
|
235
|
-
if (token && userJson) {
|
|
236
|
-
try {
|
|
237
|
-
this.authToken = token;
|
|
238
|
-
this.currentUser = JSON.parse(userJson);
|
|
239
|
-
} catch {
|
|
240
|
-
localStorage.removeItem("followgate_token");
|
|
241
|
-
localStorage.removeItem("followgate_user");
|
|
242
|
-
}
|
|
243
|
-
}
|
|
843
|
+
hasUsername() {
|
|
844
|
+
return this.currentUser !== null;
|
|
244
845
|
}
|
|
245
846
|
/**
|
|
246
|
-
*
|
|
847
|
+
* Clear stored session
|
|
247
848
|
*/
|
|
248
|
-
|
|
849
|
+
reset() {
|
|
850
|
+
this.currentUser = null;
|
|
851
|
+
this.completedActions = [];
|
|
852
|
+
if (typeof localStorage !== "undefined") {
|
|
853
|
+
localStorage.removeItem("followgate_user");
|
|
854
|
+
localStorage.removeItem("followgate_actions");
|
|
855
|
+
localStorage.removeItem("followgate_unlocked");
|
|
856
|
+
}
|
|
857
|
+
if (this.config?.debug) {
|
|
858
|
+
console.log("[FollowGate] Session reset");
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// ============================================
|
|
862
|
+
// Intent URL Methods
|
|
863
|
+
// ============================================
|
|
864
|
+
getFollowUrl(platform, target) {
|
|
865
|
+
return this.buildIntentUrl({ platform, action: "follow", target });
|
|
866
|
+
}
|
|
867
|
+
getRepostUrl(platform, target) {
|
|
868
|
+
return this.buildIntentUrl({ platform, action: "repost", target });
|
|
869
|
+
}
|
|
870
|
+
getLikeUrl(platform, target) {
|
|
871
|
+
return this.buildIntentUrl({ platform, action: "like", target });
|
|
872
|
+
}
|
|
873
|
+
async openIntent(options) {
|
|
249
874
|
if (!this.config) {
|
|
250
875
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
251
876
|
}
|
|
252
877
|
const url = this.buildIntentUrl(options);
|
|
253
878
|
if (this.config.debug) {
|
|
254
|
-
console.log("[FollowGate] Opening:", url);
|
|
879
|
+
console.log("[FollowGate] Opening intent:", url);
|
|
255
880
|
}
|
|
256
|
-
await this.trackEvent("
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
881
|
+
await this.trackEvent("intent_opened", { ...options });
|
|
882
|
+
window.open(url, "_blank", "width=600,height=700");
|
|
883
|
+
}
|
|
884
|
+
// ============================================
|
|
885
|
+
// Completion Methods
|
|
886
|
+
// ============================================
|
|
887
|
+
async complete(options) {
|
|
888
|
+
if (!this.config) {
|
|
889
|
+
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
890
|
+
}
|
|
891
|
+
if (!this.currentUser) {
|
|
892
|
+
throw new Error("[FollowGate] No username set. Call setUsername() first.");
|
|
893
|
+
}
|
|
894
|
+
const alreadyCompleted = this.completedActions.some(
|
|
895
|
+
(a) => a.platform === options.platform && a.action === options.action && a.target === options.target
|
|
896
|
+
);
|
|
897
|
+
if (!alreadyCompleted) {
|
|
898
|
+
this.completedActions.push(options);
|
|
899
|
+
this.saveCompletedActions();
|
|
261
900
|
}
|
|
262
|
-
await this.trackEvent("
|
|
901
|
+
await this.trackEvent("action_completed", {
|
|
902
|
+
...options,
|
|
903
|
+
username: this.currentUser.username
|
|
904
|
+
});
|
|
263
905
|
this.emit("complete", {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
target: options.target
|
|
906
|
+
...options,
|
|
907
|
+
username: this.currentUser.username
|
|
267
908
|
});
|
|
909
|
+
if (this.config.debug) {
|
|
910
|
+
console.log("[FollowGate] Action completed:", options);
|
|
911
|
+
}
|
|
268
912
|
}
|
|
269
|
-
|
|
270
|
-
* Verify follow status (for Pro/Business tiers with OAuth)
|
|
271
|
-
*/
|
|
272
|
-
async verify(options) {
|
|
913
|
+
async unlock() {
|
|
273
914
|
if (!this.config) {
|
|
274
915
|
throw new Error("[FollowGate] SDK not initialized. Call init() first.");
|
|
275
916
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
method: "POST",
|
|
279
|
-
headers: {
|
|
280
|
-
"Content-Type": "application/json",
|
|
281
|
-
"X-API-Key": this.config.apiKey
|
|
282
|
-
},
|
|
283
|
-
body: JSON.stringify({
|
|
284
|
-
platform: options.platform,
|
|
285
|
-
action: options.action,
|
|
286
|
-
target: options.target,
|
|
287
|
-
externalUserId: options.userId
|
|
288
|
-
})
|
|
289
|
-
});
|
|
290
|
-
const data = await response.json();
|
|
291
|
-
return data.success && data.data?.verified === true;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (this.config.debug) {
|
|
294
|
-
console.error("[FollowGate] Verification error:", error);
|
|
295
|
-
}
|
|
296
|
-
return false;
|
|
917
|
+
if (typeof localStorage !== "undefined") {
|
|
918
|
+
localStorage.setItem("followgate_unlocked", "true");
|
|
297
919
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"Content-Type": "application/json",
|
|
309
|
-
"X-API-Key": this.config.apiKey
|
|
310
|
-
},
|
|
311
|
-
body: JSON.stringify({
|
|
312
|
-
event,
|
|
313
|
-
platform: options.platform,
|
|
314
|
-
action: options.action,
|
|
315
|
-
target: options.target,
|
|
316
|
-
externalUserId: options.userId
|
|
317
|
-
})
|
|
318
|
-
});
|
|
319
|
-
} catch (error) {
|
|
320
|
-
if (this.config.debug) {
|
|
321
|
-
console.warn("[FollowGate] Failed to track event:", error);
|
|
322
|
-
}
|
|
920
|
+
await this.trackEvent("gate_unlocked", {
|
|
921
|
+
username: this.currentUser?.username,
|
|
922
|
+
actions: this.completedActions
|
|
923
|
+
});
|
|
924
|
+
this.emit("unlocked", {
|
|
925
|
+
username: this.currentUser?.username,
|
|
926
|
+
actions: this.completedActions
|
|
927
|
+
});
|
|
928
|
+
if (this.config.debug) {
|
|
929
|
+
console.log("[FollowGate] Gate unlocked!");
|
|
323
930
|
}
|
|
324
931
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
932
|
+
isUnlocked() {
|
|
933
|
+
if (typeof localStorage === "undefined") return false;
|
|
934
|
+
return localStorage.getItem("followgate_unlocked") === "true";
|
|
935
|
+
}
|
|
936
|
+
getUnlockStatus() {
|
|
937
|
+
return {
|
|
938
|
+
unlocked: this.isUnlocked(),
|
|
939
|
+
username: this.currentUser?.username,
|
|
940
|
+
completedActions: [...this.completedActions]
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
getCompletedActions() {
|
|
944
|
+
return [...this.completedActions];
|
|
945
|
+
}
|
|
946
|
+
// ============================================
|
|
947
|
+
// Event System
|
|
948
|
+
// ============================================
|
|
328
949
|
on(event, callback) {
|
|
329
950
|
if (!this.listeners.has(event)) {
|
|
330
951
|
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
331
952
|
}
|
|
332
953
|
this.listeners.get(event).add(callback);
|
|
333
954
|
}
|
|
334
|
-
/**
|
|
335
|
-
* Remove event listener
|
|
336
|
-
*/
|
|
337
955
|
off(event, callback) {
|
|
338
956
|
this.listeners.get(event)?.delete(callback);
|
|
339
957
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
958
|
+
// ============================================
|
|
959
|
+
// Private Methods
|
|
960
|
+
// ============================================
|
|
961
|
+
restoreSession() {
|
|
962
|
+
if (typeof localStorage === "undefined") return;
|
|
963
|
+
const userJson = localStorage.getItem("followgate_user");
|
|
964
|
+
if (userJson) {
|
|
965
|
+
try {
|
|
966
|
+
this.currentUser = JSON.parse(userJson);
|
|
967
|
+
} catch {
|
|
968
|
+
localStorage.removeItem("followgate_user");
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const actionsJson = localStorage.getItem("followgate_actions");
|
|
972
|
+
if (actionsJson) {
|
|
973
|
+
try {
|
|
974
|
+
this.completedActions = JSON.parse(actionsJson);
|
|
975
|
+
} catch {
|
|
976
|
+
localStorage.removeItem("followgate_actions");
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
saveCompletedActions() {
|
|
981
|
+
if (typeof localStorage !== "undefined") {
|
|
982
|
+
localStorage.setItem(
|
|
983
|
+
"followgate_actions",
|
|
984
|
+
JSON.stringify(this.completedActions)
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
343
988
|
buildIntentUrl(options) {
|
|
344
989
|
const { platform, action, target } = options;
|
|
345
990
|
switch (platform) {
|
|
@@ -398,6 +1043,27 @@ var FollowGateClient = class {
|
|
|
398
1043
|
throw new Error(`[FollowGate] Unsupported LinkedIn action: ${action}`);
|
|
399
1044
|
}
|
|
400
1045
|
}
|
|
1046
|
+
async trackEvent(event, data) {
|
|
1047
|
+
if (!this.config) return;
|
|
1048
|
+
try {
|
|
1049
|
+
await fetch(`${this.config.apiUrl}/api/v1/events`, {
|
|
1050
|
+
method: "POST",
|
|
1051
|
+
headers: {
|
|
1052
|
+
"Content-Type": "application/json",
|
|
1053
|
+
"X-API-Key": this.config.apiKey
|
|
1054
|
+
},
|
|
1055
|
+
body: JSON.stringify({
|
|
1056
|
+
event,
|
|
1057
|
+
appId: this.config.appId,
|
|
1058
|
+
...data
|
|
1059
|
+
})
|
|
1060
|
+
});
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
if (this.config.debug) {
|
|
1063
|
+
console.warn("[FollowGate] Failed to track event:", error);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
401
1067
|
emit(event, data) {
|
|
402
1068
|
this.listeners.get(event)?.forEach((callback) => callback(data));
|
|
403
1069
|
}
|