···
21
21
import CollectionsFeed from './components/CollectionsFeed/CollectionsFeed';
22
22
import Login from './components/Login/Login';
23
23
import LoginCallback from './components/Login/LoginCallback';
24
24
+
import Verifier from './components/Verifier/Verifier';
24
25
import { AuthProvider } from './contexts/AuthContext';
25
26
import "./App.css";
26
27
···
50
51
<Route path="/definitions" element={<Definitions />} />
51
52
<Route path="/leaderboard" element={<Leaderboard />} />
52
53
<Route path="/resources" element={<Resources />} />
54
54
+
<Route path="/verifier" element={<Verifier />} />
53
55
<Route path="/shortcut" element={<Shortcut />} />
54
56
<Route path="/zen" element={<ZenPage />} />
55
57
<Route path="/methodology" element={<ScoringMethodology />} />
···
131
131
items: [
132
132
{ title: "library", path: "/resources" },
133
133
{ title: "alt text rating", path: "/alt-text" },
134
134
-
{ title: "omnifeed", path: "/omnifeed" }
134
134
+
{ title: "omnifeed", path: "/omnifeed" },
135
135
+
{ title: "verifier", path: "/verifier" }
135
136
]
136
137
};
137
138
···
1
1
+
/* frontend-cred-blue/src/components/Verifier/Verifier.css */
2
2
+
3
3
+
/* General container */
4
4
+
.verifier-container {
5
5
+
font-family: "articulat-cf", sans-serif;
6
6
+
max-width: 800px; /* Adjust as needed */
7
7
+
margin: 20px auto;
8
8
+
padding: 20px;
9
9
+
color: var(--text);
10
10
+
background-color: var(--background);
11
11
+
}
12
12
+
13
13
+
.verifier-container h1,
14
14
+
.verifier-container h2 {
15
15
+
color: var(--button-bg); /* Match heading color */
16
16
+
text-align: left;
17
17
+
margin-bottom: 15px;
18
18
+
}
19
19
+
20
20
+
.verifier-container h1 {
21
21
+
font-size: 2em; /* Adjust */
22
22
+
}
23
23
+
24
24
+
.verifier-container h2 {
25
25
+
font-size: 1.5em; /* Adjust */
26
26
+
margin-top: 30px; /* Space between sections */
27
27
+
border-bottom: 1px solid var(--card-border); /* Separator */
28
28
+
padding-bottom: 10px;
29
29
+
}
30
30
+
31
31
+
.verifier-intro-text,
32
32
+
.verifier-section p {
33
33
+
color: var(--text);
34
34
+
line-height: 1.6;
35
35
+
margin-bottom: 10px;
36
36
+
text-align: left;
37
37
+
}
38
38
+
39
39
+
.verifier-page-header {
40
40
+
display: flex;
41
41
+
justify-content: space-between;
42
42
+
align-items: center;
43
43
+
margin-bottom: 15px;
44
44
+
flex-wrap: wrap; /* Allow wrapping on small screens */
45
45
+
gap: 10px;
46
46
+
}
47
47
+
48
48
+
.verifier-user-info {
49
49
+
font-size: 0.9em;
50
50
+
color: var(--text-muted, var(--text));
51
51
+
margin: 0; /* Remove default paragraph margin */
52
52
+
}
53
53
+
54
54
+
/* Buttons */
55
55
+
.verifier-sign-out-button,
56
56
+
.verifier-submit-button,
57
57
+
.verifier-action-button,
58
58
+
.verifier-revoke-button {
59
59
+
background: var(--button-bg);
60
60
+
color: var(--button-text);
61
61
+
border: none;
62
62
+
border-radius: 6px;
63
63
+
padding: 8px 15px; /* Slightly smaller padding */
64
64
+
font-weight: 700;
65
65
+
font-size: 0.9em;
66
66
+
cursor: pointer;
67
67
+
transition: background-color 0.3s ease;
68
68
+
}
69
69
+
70
70
+
.verifier-sign-out-button:hover,
71
71
+
.verifier-submit-button:hover,
72
72
+
.verifier-action-button:hover,
73
73
+
.verifier-revoke-button:hover {
74
74
+
background: var(--button-hover-bg, #0056b3); /* Use main hover color */
75
75
+
}
76
76
+
77
77
+
.verifier-sign-out-button:disabled,
78
78
+
.verifier-submit-button:disabled,
79
79
+
.verifier-action-button:disabled,
80
80
+
.verifier-revoke-button:disabled {
81
81
+
background-color: var(--button-disabled-bg, #cccccc); /* Add disabled style */
82
82
+
cursor: not-allowed;
83
83
+
opacity: 0.7;
84
84
+
}
85
85
+
86
86
+
/* Form Styles */
87
87
+
.verifier-section {
88
88
+
background: var(--navbar-bg);
89
89
+
border: 1px solid var(--card-border);
90
90
+
border-radius: 12px;
91
91
+
padding: 20px;
92
92
+
margin-bottom: 20px;
93
93
+
}
94
94
+
95
95
+
.verifier-input-container {
96
96
+
position: relative; /* For autocomplete positioning */
97
97
+
max-width: 400px; /* Limit width */
98
98
+
}
99
99
+
100
100
+
.verifier-form-container {
101
101
+
display: flex;
102
102
+
gap: 10px;
103
103
+
align-items: center;
104
104
+
flex-wrap: wrap; /* Allow wrapping */
105
105
+
}
106
106
+
107
107
+
.verifier-input-field {
108
108
+
flex-grow: 1; /* Take available space */
109
109
+
border: 2px solid var(--card-border);
110
110
+
border-radius: 6px;
111
111
+
padding: 9px;
112
112
+
font-size: 1em;
113
113
+
background-color: var(--navbar-bg);
114
114
+
color: var(--text);
115
115
+
transition: all 0.3s ease;
116
116
+
font-family: inherit; /* Use main font */
117
117
+
min-width: 200px; /* Ensure minimum width */
118
118
+
}
119
119
+
120
120
+
.verifier-input-field:hover,
121
121
+
.verifier-input-field:focus {
122
122
+
border-color: var(--button-bg);
123
123
+
background-color: var(--background); /* Match main app focus */
124
124
+
outline: none;
125
125
+
}
126
126
+
127
127
+
/* Autocomplete Styles */
128
128
+
.verifier-suggestions-list {
129
129
+
position: absolute;
130
130
+
top: 100%; /* Position below input */
131
131
+
left: 0;
132
132
+
right: 0;
133
133
+
background-color: var(--navbar-bg);
134
134
+
border: 1px solid var(--card-border);
135
135
+
border-top: none; /* Avoid double border */
136
136
+
border-radius: 0 0 6px 6px;
137
137
+
list-style: none;
138
138
+
padding: 0;
139
139
+
margin: 0;
140
140
+
max-height: 200px;
141
141
+
overflow-y: auto;
142
142
+
z-index: 10;
143
143
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
144
144
+
}
145
145
+
146
146
+
.verifier-suggestion-item {
147
147
+
display: flex;
148
148
+
align-items: center;
149
149
+
padding: 10px;
150
150
+
cursor: pointer;
151
151
+
border-bottom: 1px solid var(--card-border);
152
152
+
}
153
153
+
.verifier-suggestion-item:last-child {
154
154
+
border-bottom: none;
155
155
+
}
156
156
+
157
157
+
.verifier-suggestion-item:hover {
158
158
+
background-color: var(--background); /* Use main background for hover */
159
159
+
}
160
160
+
161
161
+
.verifier-suggestion-avatar {
162
162
+
width: 30px;
163
163
+
height: 30px;
164
164
+
border-radius: 50%;
165
165
+
margin-right: 10px;
166
166
+
object-fit: cover;
167
167
+
}
168
168
+
169
169
+
.verifier-suggestion-text {
170
170
+
display: flex;
171
171
+
flex-direction: column;
172
172
+
}
173
173
+
174
174
+
.verifier-suggestion-display-name {
175
175
+
font-weight: bold;
176
176
+
color: var(--text);
177
177
+
}
178
178
+
179
179
+
.verifier-suggestion-handle {
180
180
+
font-size: 0.9em;
181
181
+
color: var(--text-muted, var(--text));
182
182
+
}
183
183
+
184
184
+
/* Status Box */
185
185
+
.verifier-status-box {
186
186
+
padding: 15px;
187
187
+
border-radius: 6px;
188
188
+
margin-top: 15px;
189
189
+
text-align: center;
190
190
+
}
191
191
+
.verifier-status-box-success {
192
192
+
background-color: var(--success-bg, #d4edda); /* Add theme variables */
193
193
+
color: var(--success-text, #155724);
194
194
+
border: 1px solid var(--success-border, #c3e6cb);
195
195
+
}
196
196
+
.verifier-status-box-error {
197
197
+
background-color: var(--error-bg, #f8d7da); /* Add theme variables */
198
198
+
color: var(--error-text, #721c24);
199
199
+
border: 1px solid var(--error-border, #f5c6cb);
200
200
+
}
201
201
+
.verifier-status-box .intent-link {
202
202
+
color: var(--success-text, #155724);
203
203
+
font-weight: bold;
204
204
+
text-decoration: underline;
205
205
+
}
206
206
+
207
207
+
/* Verification Lists */
208
208
+
.verifier-list-header {
209
209
+
display: flex;
210
210
+
justify-content: space-between;
211
211
+
align-items: center;
212
212
+
margin-bottom: 10px; /* Space above list */
213
213
+
}
214
214
+
.verifier-list-header h2 {
215
215
+
margin: 0;
216
216
+
border: none; /* Remove border inherited from h2 general style */
217
217
+
padding: 0;
218
218
+
}
219
219
+
220
220
+
.verifier-list,
221
221
+
.verifier-verifier-list {
222
222
+
list-style: none;
223
223
+
padding: 0;
224
224
+
margin: 0;
225
225
+
}
226
226
+
227
227
+
.verifier-list-item {
228
228
+
display: flex;
229
229
+
justify-content: space-between;
230
230
+
align-items: flex-start; /* Align items to top */
231
231
+
background-color: var(--navbar-bg); /* Match form background */
232
232
+
padding: 15px;
233
233
+
border: 1px solid var(--card-border);
234
234
+
border-radius: 8px;
235
235
+
margin-bottom: 10px;
236
236
+
flex-wrap: wrap; /* Allow actions to wrap */
237
237
+
gap: 10px;
238
238
+
}
239
239
+
.verifier-list-item-content {
240
240
+
flex-grow: 1;
241
241
+
}
242
242
+
.verifier-list-item-handle {
243
243
+
font-size: 0.9em;
244
244
+
color: var(--text-muted, var(--text));
245
245
+
margin: 2px 0;
246
246
+
}
247
247
+
.verifier-list-item-date {
248
248
+
font-size: 0.8em;
249
249
+
color: var(--text-muted, var(--text));
250
250
+
margin-top: 5px;
251
251
+
}
252
252
+
253
253
+
.verifier-list-item-actions {
254
254
+
flex-shrink: 0; /* Prevent button shrinking */
255
255
+
}
256
256
+
257
257
+
.verifier-list-item-invalid {
258
258
+
border-left: 5px solid var(--warning-border, orange); /* Highlight invalid items */
259
259
+
}
260
260
+
261
261
+
.verifier-validity-warning {
262
262
+
margin-top: 10px;
263
263
+
padding: 10px;
264
264
+
background-color: var(--warning-bg, #fff3cd);
265
265
+
border: 1px solid var(--warning-border, #ffeeba);
266
266
+
color: var(--warning-text, #856404);
267
267
+
border-radius: 4px;
268
268
+
font-size: 0.9em;
269
269
+
}
270
270
+
.verifier-validity-warning p {
271
271
+
margin: 5px 0;
272
272
+
color: var(--warning-text, #856404); /* Ensure text color consistency */
273
273
+
text-align: left;
274
274
+
}
275
275
+
276
276
+
/* Network Verifications */
277
277
+
.verifier-check-network-button {
278
278
+
font-size: 0.9em;
279
279
+
}
280
280
+
.verifier-network-status {
281
281
+
font-style: italic;
282
282
+
color: var(--text-muted, var(--text));
283
283
+
margin: 10px 0;
284
284
+
}
285
285
+
.verifier-network-results {
286
286
+
margin-top: 15px;
287
287
+
}
288
288
+
.verifier-network-results .verifier-verifier-list {
289
289
+
margin-bottom: 15px; /* Space between lists */
290
290
+
}
291
291
+
.verifier-additional-context {
292
292
+
font-size: 0.9em;
293
293
+
color: var(--text-muted, var(--text));
294
294
+
margin-top: 15px;
295
295
+
border-top: 1px dashed var(--card-border);
296
296
+
padding-top: 10px;
297
297
+
}
298
298
+
.verifier-share-stats-link {
299
299
+
display: inline-block;
300
300
+
margin-top: 15px;
301
301
+
font-size: 0.9em;
302
302
+
font-weight: bold;
303
303
+
}
304
304
+
305
305
+
306
306
+
/* Official Verifier Status */
307
307
+
.verifier-official-verifier-tooltip {
308
308
+
cursor: help;
309
309
+
display: inline-block;
310
310
+
margin-left: 5px;
311
311
+
font-weight: bold;
312
312
+
border: 1px solid var(--text-muted, #ccc);
313
313
+
border-radius: 50%;
314
314
+
width: 1.2em;
315
315
+
height: 1.2em;
316
316
+
line-height: 1.2em;
317
317
+
text-align: center;
318
318
+
font-size: 0.8em; /* Smaller question mark */
319
319
+
color: var(--text-muted, var(--text));
320
320
+
}
321
321
+
322
322
+
.verifier-official-verifier-note {
323
323
+
font-size: 0.9em;
324
324
+
margin: 5px 0;
325
325
+
padding-left: 5px; /* Indent slightly */
326
326
+
}
327
327
+
328
328
+
.verifier-verified-status { color: var(--success-text, green); }
329
329
+
.verifier-not-verified-status { color: var(--text-muted, grey); }
330
330
+
.verifier-error-status { color: var(--error-text, red); }
331
331
+
.verifier-checking-status, .verifier-idle-status { color: var(--text-muted, grey); }
···
1
1
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
2
2
+
import { useAuth } from '../../contexts/AuthContext'; // Updated import path
3
3
+
import { Agent } from '@atproto/api';
4
4
+
import './Verifier.css'; // Updated CSS import
5
5
+
6
6
+
// Define trusted verifiers (updated list)
7
7
+
const TRUSTED_VERIFIERS = [
8
8
+
'bsky.app',
9
9
+
'nytimes.com',
10
10
+
'wired.com',
11
11
+
'theathletic.bsky.social'
12
12
+
];
13
13
+
14
14
+
// Helper function to fetch all paginated results using a specific agent instance
15
15
+
async function fetchAllPaginated(agentInstance, apiMethod, initialParams) {
16
16
+
let results = [];
17
17
+
let cursor = initialParams.cursor;
18
18
+
const params = { ...initialParams };
19
19
+
let operationName = apiMethod.name; // Get the name for logging
20
20
+
21
21
+
// Attempt to determine a more specific name if bound
22
22
+
if (apiMethod.name === 'bound dispatch') {
23
23
+
const boundFnString = apiMethod.toString();
24
24
+
// This is hacky, relies on internal representation which might change
25
25
+
const match = boundFnString.match(/Target function: (\w+)/);
26
26
+
if (match && match[1]) operationName = match[1];
27
27
+
}
28
28
+
29
29
+
do {
30
30
+
try {
31
31
+
if (cursor) {
32
32
+
params.cursor = cursor;
33
33
+
}
34
34
+
// Call the method bound to the correct agent context
35
35
+
const response = await apiMethod(params);
36
36
+
const listKey = Object.keys(response.data).find(key => Array.isArray(response.data[key]));
37
37
+
if (listKey && response.data[listKey]) {
38
38
+
results = results.concat(response.data[listKey]);
39
39
+
}
40
40
+
cursor = response.data.cursor;
41
41
+
} catch (error) {
42
42
+
// Use the determined operation name in the error message
43
43
+
console.error(`Error during paginated fetch for ${operationName}:`, error);
44
44
+
cursor = undefined;
45
45
+
}
46
46
+
} while (cursor);
47
47
+
48
48
+
return results;
49
49
+
}
50
50
+
51
51
+
// Updated function to get PDS endpoint from PLC directory OR well-known URI for did:web
52
52
+
async function getPdsEndpoint(did) {
53
53
+
let didDocUrl;
54
54
+
if (did.startsWith('did:plc:')) {
55
55
+
didDocUrl = `https://plc.directory/${did}`;
56
56
+
} else if (did.startsWith('did:web:')) {
57
57
+
const domain = did.substring(8); // Extract domain after 'did:web:'
58
58
+
// Decode percent-encoded characters in domain (e.g., for ports)
59
59
+
const decodedDomain = decodeURIComponent(domain);
60
60
+
didDocUrl = `https://${decodedDomain}/.well-known/did.json`;
61
61
+
} else {
62
62
+
console.warn(`Unsupported DID method for PDS lookup: ${did}`);
63
63
+
return null;
64
64
+
}
65
65
+
66
66
+
try {
67
67
+
console.log(`Fetching DID document from: ${didDocUrl}`); // Log the URL being fetched
68
68
+
const response = await fetch(didDocUrl);
69
69
+
if (!response.ok) {
70
70
+
console.warn(`Could not resolve DID document for ${did} at ${didDocUrl}: ${response.status}`);
71
71
+
return null;
72
72
+
}
73
73
+
const didDoc = await response.json();
74
74
+
const service = didDoc.service?.find(s => s.type === 'AtprotoPersonalDataServer');
75
75
+
const endpoint = service?.serviceEndpoint || null;
76
76
+
if (!endpoint) {
77
77
+
console.warn(`No AtprotoPersonalDataServer service endpoint found in DID document for ${did}`);
78
78
+
}
79
79
+
return endpoint;
80
80
+
} catch (error) {
81
81
+
console.error(`Error fetching or parsing DID document for ${did} from ${didDocUrl}:`, error);
82
82
+
return null;
83
83
+
}
84
84
+
}
85
85
+
86
86
+
// --- Debounce Hook ---
87
87
+
function useDebounce(value, delay) {
88
88
+
const [debouncedValue, setDebouncedValue] = useState(value);
89
89
+
90
90
+
useEffect(() => {
91
91
+
const handler = setTimeout(() => {
92
92
+
setDebouncedValue(value);
93
93
+
}, delay);
94
94
+
95
95
+
// Cancel the timeout if value changes (also on delay change or unmount)
96
96
+
return () => {
97
97
+
clearTimeout(handler);
98
98
+
};
99
99
+
}, [value, delay]);
100
100
+
101
101
+
return debouncedValue;
102
102
+
}
103
103
+
// --- End Debounce Hook ---
104
104
+
105
105
+
// Renamed component to Verifier
106
106
+
function Verifier() {
107
107
+
// Use the main app's AuthContext
108
108
+
const { session, loading: isAuthLoading, error: authError, logout: signOut } = useAuth();
109
109
+
const [targetHandle, setTargetHandle] = useState('');
110
110
+
const [statusMessage, setStatusMessage] = useState('');
111
111
+
const [isVerifying, setIsVerifying] = useState(false);
112
112
+
const [isRevoking, setIsRevoking] = useState(false);
113
113
+
const [agent, setAgent] = useState(null);
114
114
+
const [userInfo, setUserInfo] = useState(null);
115
115
+
const [verifications, setVerifications] = useState([]);
116
116
+
const [isLoadingVerifications, setIsLoadingVerifications] = useState(false);
117
117
+
const [networkVerifications, setNetworkVerifications] = useState({
118
118
+
mutualsVerifiedMe: [],
119
119
+
followsVerifiedMe: [],
120
120
+
mutualsVerifiedAnyone: 0,
121
121
+
followsVerifiedAnyone: 0,
122
122
+
fetchedMutualsCount: 0,
123
123
+
fetchedFollowsCount: 0,
124
124
+
});
125
125
+
const [isLoadingNetwork, setIsLoadingNetwork] = useState(false);
126
126
+
const [networkChecked, setNetworkChecked] = useState(false);
127
127
+
const [isCheckingValidity, setIsCheckingValidity] = useState(false);
128
128
+
const [networkStatusMessage, setNetworkStatusMessage] = useState('');
129
129
+
const [officialVerifiersStatus, setOfficialVerifiersStatus] = useState({}); // Stores status per verifier identifier
130
130
+
131
131
+
// --- Autocomplete State --- (Keep as is)
132
132
+
const [suggestions, setSuggestions] = useState([]);
133
133
+
const [isFetchingSuggestions, setIsFetchingSuggestions] = useState(false);
134
134
+
const [showSuggestions, setShowSuggestions] = useState(false);
135
135
+
const debouncedSearchTerm = useDebounce(targetHandle, 300); // 300ms debounce
136
136
+
const suggestionsRef = useRef(null); // Ref for suggestions container
137
137
+
const inputRef = useRef(null); // Ref for input field
138
138
+
// --- End Autocomplete State ---
139
139
+
140
140
+
useEffect(() => {
141
141
+
// If session exists, create an Agent instance
142
142
+
if (session) {
143
143
+
// The session object from the main AuthContext might be structured differently.
144
144
+
// Assuming it has an `accessJwt` and `did` (or `sub`)
145
145
+
const agentInstance = new Agent({
146
146
+
service: 'https://bsky.social', // Or get from session if available
147
147
+
session: session // Pass the session object directly
148
148
+
});
149
149
+
setAgent(agentInstance);
150
150
+
151
151
+
// Fetch logged-in user's profile info using the authenticated API
152
152
+
agentInstance.api.app.bsky.actor.getProfile({ actor: session.did /* Ensure correct DID property */ })
153
153
+
.then(res => {
154
154
+
console.log('Logged-in user profile fetched successfully:', res.data);
155
155
+
setUserInfo(res.data);
156
156
+
})
157
157
+
.catch(err => {
158
158
+
console.error("Failed to fetch user profile:", err);
159
159
+
// Attempt to use basic info from session as fallback
160
160
+
setUserInfo({ handle: session.handle, displayName: session.displayName || session.handle, did: session.did });
161
161
+
});
162
162
+
} else {
163
163
+
setAgent(null);
164
164
+
setUserInfo(null);
165
165
+
}
166
166
+
// No redirection here, handled by main app routing if needed
167
167
+
}, [session]);
168
168
+
169
169
+
// Fetch all verification records created by the current user
170
170
+
const fetchVerifications = async () => {
171
171
+
if (!agent || !session) return;
172
172
+
173
173
+
setIsLoadingVerifications(true);
174
174
+
try {
175
175
+
const response = await agent.api.com.atproto.repo.listRecords({
176
176
+
repo: session.did, // Use session.did
177
177
+
collection: 'app.bsky.graph.verification',
178
178
+
limit: 100,
179
179
+
});
180
180
+
181
181
+
console.log('Fetched verifications:', response.data);
182
182
+
183
183
+
// If we have records, set them in state
184
184
+
if (response.data.records) {
185
185
+
const formattedVerifications = response.data.records.map(record => ({
186
186
+
uri: record.uri,
187
187
+
cid: record.cid,
188
188
+
handle: record.value.handle,
189
189
+
displayName: record.value.displayName,
190
190
+
subject: record.value.subject,
191
191
+
createdAt: record.value.createdAt,
192
192
+
isValid: true, // Default, will be checked later
193
193
+
validityChecked: false
194
194
+
}));
195
195
+
setVerifications(formattedVerifications);
196
196
+
197
197
+
// Check validity of each verification
198
198
+
checkVerificationsValidity(formattedVerifications);
199
199
+
} else {
200
200
+
setVerifications([]);
201
201
+
}
202
202
+
} catch (error) {
203
203
+
console.error('Failed to fetch verifications:', error);
204
204
+
setStatusMessage(`Failed to load verifications: ${error.message || 'Unknown error'}`);
205
205
+
} finally {
206
206
+
setIsLoadingVerifications(false);
207
207
+
}
208
208
+
};
209
209
+
210
210
+
// Check if verifications are still valid (handle/displayName still match)
211
211
+
const checkVerificationsValidity = async (verificationsList) => {
212
212
+
if (!agent || verificationsList.length === 0) return;
213
213
+
214
214
+
setIsCheckingValidity(true);
215
215
+
const updatedVerifications = [...verificationsList];
216
216
+
217
217
+
try {
218
218
+
// Process in batches to avoid too many concurrent requests
219
219
+
const batchSize = 5;
220
220
+
for (let i = 0; i < updatedVerifications.length; i += batchSize) {
221
221
+
const batch = updatedVerifications.slice(i, i + batchSize);
222
222
+
223
223
+
await Promise.all(batch.map(async (verification, index) => {
224
224
+
try {
225
225
+
// Get current profile data
226
226
+
const profileRes = await agent.api.app.bsky.actor.getProfile({
227
227
+
actor: verification.handle
228
228
+
});
229
229
+
230
230
+
// Check if handle and displayName still match
231
231
+
const currentHandle = profileRes.data.handle;
232
232
+
const currentDisplayName = profileRes.data.displayName || profileRes.data.handle;
233
233
+
234
234
+
// Update verification validity
235
235
+
const batchIndex = i + index;
236
236
+
updatedVerifications[batchIndex].validityChecked = true;
237
237
+
updatedVerifications[batchIndex].isValid =
238
238
+
currentHandle === verification.handle &&
239
239
+
currentDisplayName === verification.displayName;
240
240
+
241
241
+
// If not valid, store current values for reference
242
242
+
if (!updatedVerifications[batchIndex].isValid) {
243
243
+
updatedVerifications[batchIndex].currentHandle = currentHandle;
244
244
+
updatedVerifications[batchIndex].currentDisplayName = currentDisplayName;
245
245
+
}
246
246
+
247
247
+
// Update state as we go to show progress
248
248
+
setVerifications([...updatedVerifications]);
249
249
+
} catch (err) {
250
250
+
console.error(`Failed to check validity for ${verification.handle}:`, err);
251
251
+
// Mark as could not check
252
252
+
const batchIndex = i + index;
253
253
+
updatedVerifications[batchIndex].validityChecked = true;
254
254
+
updatedVerifications[batchIndex].isValid = false;
255
255
+
updatedVerifications[batchIndex].validityError = true;
256
256
+
}
257
257
+
}));
258
258
+
}
259
259
+
260
260
+
console.log('Verified all records validity:', updatedVerifications);
261
261
+
} catch (error) {
262
262
+
console.error('Failed to check verifications validity:', error);
263
263
+
} finally {
264
264
+
setIsCheckingValidity(false);
265
265
+
}
266
266
+
};
267
267
+
268
268
+
// Updated function: Check mutuals (authenticated) and all follows (public)
269
269
+
const checkNetworkVerifications = async () => {
270
270
+
// Ensure authenticated agent is available for mutuals check
271
271
+
if (!agent || !session || !userInfo) return;
272
272
+
273
273
+
setIsLoadingNetwork(true);
274
274
+
setNetworkChecked(false);
275
275
+
// Reset state
276
276
+
setNetworkVerifications({
277
277
+
mutualsVerifiedMe: [], followsVerifiedMe: [],
278
278
+
mutualsVerifiedAnyone: 0, followsVerifiedAnyone: 0,
279
279
+
fetchedMutualsCount: 0, fetchedFollowsCount: 0
280
280
+
});
281
281
+
setNetworkStatusMessage("Fetching network lists (mutuals, follows)...");
282
282
+
283
283
+
const publicAgent = new Agent({ service: 'https://public.api.bsky.app' });
284
284
+
285
285
+
try {
286
286
+
// Fetch follows (public) and known followers/mutuals (authenticated)
287
287
+
const [follows, mutuals] = await Promise.all([
288
288
+
fetchAllPaginated(publicAgent, publicAgent.api.app.bsky.graph.getFollows.bind(publicAgent.api.app.bsky.graph), { actor: session.did, limit: 100 }), // Use session.did
289
289
+
// Use the main authenticated agent for getKnownFollowers
290
290
+
fetchAllPaginated(agent, agent.api.app.bsky.graph.getKnownFollowers.bind(agent.api.app.bsky.graph), { actor: session.did, limit: 100 }) // Use session.did
291
291
+
]);
292
292
+
293
293
+
console.log(`Fetched ${follows.length} follows (public), ${mutuals.length} mutuals (authenticated).`); // Updated log
294
294
+
setNetworkStatusMessage(`Fetched ${follows.length} follows, ${mutuals.length} mutuals. Discovering PDS and checking verifications...`);
295
295
+
296
296
+
// Update fetched counts
297
297
+
setNetworkVerifications(prev => ({
298
298
+
...prev,
299
299
+
fetchedMutualsCount: mutuals.length,
300
300
+
fetchedFollowsCount: follows.length,
301
301
+
}));
302
302
+
303
303
+
const followsSet = new Set(follows.map(f => f.did));
304
304
+
const mutualsSet = new Set(mutuals.map(m => m.did));
305
305
+
306
306
+
const allProfilesMap = new Map();
307
307
+
[...follows, ...mutuals].forEach(user => {
308
308
+
if (!allProfilesMap.has(user.did)) {
309
309
+
allProfilesMap.set(user.did, user);
310
310
+
}
311
311
+
});
312
312
+
313
313
+
const uniqueUserDids = Array.from(allProfilesMap.keys());
314
314
+
315
315
+
if (uniqueUserDids.length === 0) {
316
316
+
setNetworkStatusMessage("No mutuals or follows found to check.");
317
317
+
setIsLoadingNetwork(false);
318
318
+
setNetworkChecked(true);
319
319
+
return;
320
320
+
}
321
321
+
322
322
+
let results = {
323
323
+
mutualsVerifiedMe: [], followsVerifiedMe: [],
324
324
+
mutualsVerifiedAnyone: 0, followsVerifiedAnyone: 0
325
325
+
};
326
326
+
327
327
+
const batchSize = 5;
328
328
+
for (let i = 0; i < uniqueUserDids.length; i += batchSize) {
329
329
+
const batchDids = uniqueUserDids.slice(i, i + batchSize);
330
330
+
setNetworkStatusMessage(`Checking network... (${i + batchDids.length}/${uniqueUserDids.length})`);
331
331
+
332
332
+
await Promise.all(batchDids.map(async (did) => {
333
333
+
const profile = allProfilesMap.get(did);
334
334
+
if (!profile) return;
335
335
+
336
336
+
const isMutual = mutualsSet.has(did);
337
337
+
const isFollow = followsSet.has(did);
338
338
+
339
339
+
const pdsEndpoint = await getPdsEndpoint(did);
340
340
+
if (!pdsEndpoint) {
341
341
+
console.warn(`Skipping verification check for ${profile.handle} (no PDS found).`);
342
342
+
return;
343
343
+
}
344
344
+
345
345
+
let foundVerificationForMe = null;
346
346
+
let hasVerifiedAnyone = false;
347
347
+
let listRecordsCursor = undefined;
348
348
+
349
349
+
do {
350
350
+
try {
351
351
+
const listParams = new URLSearchParams({ repo: did, collection: 'app.bsky.graph.verification', limit: '100' });
352
352
+
if (listRecordsCursor) listParams.set('cursor', listRecordsCursor);
353
353
+
const listRecordsUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.listRecords?${listParams.toString()}`;
354
354
+
const listResponse = await fetch(listRecordsUrl);
355
355
+
if (!listResponse.ok) { break; }
356
356
+
const listData = await listResponse.json();
357
357
+
const records = listData.records || [];
358
358
+
if (records.length > 0) {
359
359
+
hasVerifiedAnyone = true;
360
360
+
const matchingRecord = records.find(record => record.value?.subject === session.did); // Use session.did
361
361
+
if (matchingRecord) { foundVerificationForMe = matchingRecord; break; }
362
362
+
}
363
363
+
listRecordsCursor = listData.cursor;
364
364
+
} catch (err) {
365
365
+
console.error(`Network error fetching listRecords for ${did} from ${pdsEndpoint}:`, err);
366
366
+
listRecordsCursor = undefined;
367
367
+
}
368
368
+
} while (listRecordsCursor);
369
369
+
370
370
+
if (hasVerifiedAnyone) {
371
371
+
if (isMutual) results.mutualsVerifiedAnyone++;
372
372
+
if (isFollow) results.followsVerifiedAnyone++;
373
373
+
}
374
374
+
if (foundVerificationForMe) {
375
375
+
const accountInfo = { ...profile, verification: foundVerificationForMe };
376
376
+
if (isMutual) results.mutualsVerifiedMe.push(accountInfo);
377
377
+
if (isFollow) results.followsVerifiedMe.push(accountInfo);
378
378
+
}
379
379
+
380
380
+
}));
381
381
+
382
382
+
setNetworkVerifications(prev => ({
383
383
+
...prev,
384
384
+
mutualsVerifiedMe: [...results.mutualsVerifiedMe],
385
385
+
followsVerifiedMe: [...results.followsVerifiedMe],
386
386
+
mutualsVerifiedAnyone: results.mutualsVerifiedAnyone,
387
387
+
followsVerifiedAnyone: results.followsVerifiedAnyone,
388
388
+
}));
389
389
+
}
390
390
+
391
391
+
console.log('Network check complete. Results:', results);
392
392
+
setNetworkStatusMessage("Network verification check complete.");
393
393
+
394
394
+
} catch (error) {
395
395
+
// Catch errors from initial Promise.all or other setup issues
396
396
+
console.error('Fatal error during network verification check:', error);
397
397
+
setStatusMessage(`Fatal error checking network: ${error.message || 'Unknown error'}`);
398
398
+
setNetworkStatusMessage("");
399
399
+
} finally {
400
400
+
setIsLoadingNetwork(false);
401
401
+
setNetworkChecked(true);
402
402
+
}
403
403
+
};
404
404
+
405
405
+
// Call fetchVerifications when agent is available
406
406
+
useEffect(() => {
407
407
+
if (agent) {
408
408
+
fetchVerifications();
409
409
+
}
410
410
+
}, [agent]);
411
411
+
412
412
+
// Updated function to check each official verifier individually
413
413
+
const checkOfficialVerification = async () => {
414
414
+
if (!agent || !session) return;
415
415
+
416
416
+
// Initialize status for all verifiers to 'checking'
417
417
+
const initialStatuses = {};
418
418
+
TRUSTED_VERIFIERS.forEach(id => { initialStatuses[id] = 'checking'; });
419
419
+
setOfficialVerifiersStatus(initialStatuses);
420
420
+
421
421
+
const publicAgent = new Agent({ service: 'https://public.api.bsky.app' });
422
422
+
423
423
+
// Use Promise.all to run checks concurrently (optional, but can be faster)
424
424
+
await Promise.all(TRUSTED_VERIFIERS.map(async (verifierIdentifier) => {
425
425
+
let verifierDid = null;
426
426
+
let verifierHandle = verifierIdentifier;
427
427
+
let currentStatus = 'checking'; // Status for this specific verifier
428
428
+
429
429
+
try {
430
430
+
// Resolve handle/DID
431
431
+
if (!verifierIdentifier.startsWith('did:')) {
432
432
+
const resolveResult = await publicAgent.resolveHandle({ handle: verifierIdentifier });
433
433
+
verifierDid = resolveResult.data.did;
434
434
+
} else {
435
435
+
verifierDid = verifierIdentifier;
436
436
+
try {
437
437
+
const profileRes = await publicAgent.api.app.bsky.actor.getProfile({ actor: verifierDid });
438
438
+
verifierHandle = profileRes.data.handle;
439
439
+
} catch (profileError) { /* ignore */ }
440
440
+
}
441
441
+
if (!verifierDid) throw new Error('Could not resolve identifier');
442
442
+
443
443
+
// Discover PDS
444
444
+
const pdsEndpoint = await getPdsEndpoint(verifierDid);
445
445
+
if (!pdsEndpoint) throw new Error('Could not find PDS');
446
446
+
447
447
+
// Paginate through their listRecords
448
448
+
let listRecordsCursor = undefined;
449
449
+
let foundMatch = false;
450
450
+
do {
451
451
+
const listParams = new URLSearchParams({ repo: verifierDid, collection: 'app.bsky.graph.verification', limit: '100' });
452
452
+
if (listRecordsCursor) listParams.set('cursor', listRecordsCursor);
453
453
+
const listRecordsUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.listRecords?${listParams.toString()}`;
454
454
+
const listResponse = await fetch(listRecordsUrl);
455
455
+
456
456
+
if (!listResponse.ok) {
457
457
+
// Treat 400 (repo/collection not found) as simply not verified by this one
458
458
+
if (listResponse.status !== 400) {
459
459
+
console.warn(`Failed fetch for ${verifierHandle}: ${listResponse.status}`);
460
460
+
throw new Error(`Fetch failed with status ${listResponse.status}`); // Throw for other errors
461
461
+
}
462
462
+
break; // Stop checking this verifier on 400 or other errors
463
463
+
}
464
464
+
465
465
+
const listData = await listResponse.json();
466
466
+
const records = listData.records || [];
467
467
+
const matchingRecord = records.find(record => record.value?.subject === session.did); // Use session.did
468
468
+
469
469
+
if (matchingRecord) {
470
470
+
console.log(`Found official verification by ${verifierHandle}`);
471
471
+
currentStatus = 'verified';
472
472
+
foundMatch = true;
473
473
+
break; // Exit pagination loop for THIS verifier
474
474
+
}
475
475
+
listRecordsCursor = listData.cursor;
476
476
+
} while (listRecordsCursor);
477
477
+
478
478
+
// If loop completed without finding a match for this verifier
479
479
+
if (!foundMatch) {
480
480
+
currentStatus = 'not_verified';
481
481
+
}
482
482
+
483
483
+
} catch (error) {
484
484
+
console.error(`Error checking official verifier ${verifierIdentifier}:`, error);
485
485
+
currentStatus = 'error'; // Set status to error for this specific verifier
486
486
+
}
487
487
+
488
488
+
// Update the state for this specific verifier
489
489
+
setOfficialVerifiersStatus(prev => ({ ...prev, [verifierIdentifier]: currentStatus }));
490
490
+
491
491
+
})); // End Promise.all map
492
492
+
493
493
+
console.log("Finished checking all official verifiers.");
494
494
+
495
495
+
}; // End checkOfficialVerification
496
496
+
497
497
+
// Effect to check official verification status on load
498
498
+
useEffect(() => {
499
499
+
// Run check when agent/session are ready
500
500
+
if (agent && session?.did) { // Changed from session.sub
501
501
+
checkOfficialVerification();
502
502
+
}
503
503
+
// Run once when agent/session become available
504
504
+
}, [agent, session]);
505
505
+
506
506
+
// --- Fetch Autocomplete Suggestions --- (Keep as is)
507
507
+
const fetchSuggestions = useCallback(async (query) => {
508
508
+
if (!query || query.trim().length < 2) { // Minimum 2 chars to search
509
509
+
setSuggestions([]);
510
510
+
// Don't explicitly setShowSuggestions(false) here, let onChange handle it
511
511
+
return;
512
512
+
}
513
513
+
514
514
+
setIsFetchingSuggestions(true);
515
515
+
// Don't set showSuggestions(true) here either, should be true already if we got here
516
516
+
517
517
+
try {
518
518
+
const url = new URL('https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead');
519
519
+
url.searchParams.append('q', query);
520
520
+
url.searchParams.append('limit', '5'); // Fetch 5 suggestions
521
521
+
522
522
+
const response = await fetch(url.toString());
523
523
+
if (!response.ok) {
524
524
+
throw new Error(`HTTP error! status: ${response.status}`);
525
525
+
}
526
526
+
const data = await response.json();
527
527
+
console.log('Suggestions fetched:', data.actors);
528
528
+
setSuggestions(data.actors || []);
529
529
+
} catch (error) {
530
530
+
console.error('Failed to fetch suggestions:', error);
531
531
+
setSuggestions([]); // Clear suggestions on error
532
532
+
} finally {
533
533
+
setIsFetchingSuggestions(false);
534
534
+
}
535
535
+
}, []);
536
536
+
537
537
+
// Effect to fetch suggestions based on debounced search term AND if suggestions should be shown
538
538
+
useEffect(() => {
539
539
+
// Only fetch if the user is likely typing (suggestions are meant to be shown)
540
540
+
// and the term is long enough.
541
541
+
if (debouncedSearchTerm && showSuggestions) {
542
542
+
fetchSuggestions(debouncedSearchTerm);
543
543
+
} else if (!debouncedSearchTerm) { // Always clear if term is empty
544
544
+
setSuggestions([]);
545
545
+
// setShowSuggestions(false); // Let onChange handle hiding when empty
546
546
+
}
547
547
+
// If showSuggestions is false (e.g., after a click), this effect won't trigger a fetch.
548
548
+
}, [debouncedSearchTerm, fetchSuggestions, showSuggestions]); // Add showSuggestions dependency
549
549
+
550
550
+
// --- Click Outside Handler for Suggestions --- (Keep as is)
551
551
+
useEffect(() => {
552
552
+
function handleClickOutside(event) {
553
553
+
if (suggestionsRef.current && !suggestionsRef.current.contains(event.target) &&
554
554
+
inputRef.current && !inputRef.current.contains(event.target)) {
555
555
+
setShowSuggestions(false);
556
556
+
}
557
557
+
}
558
558
+
// Bind the event listener
559
559
+
document.addEventListener("mousedown", handleClickOutside);
560
560
+
return () => {
561
561
+
// Unbind the event listener on clean up
562
562
+
document.removeEventListener("mousedown", handleClickOutside);
563
563
+
};
564
564
+
}, [suggestionsRef, inputRef]); // Add inputRef dependency
565
565
+
// --- End Click Outside Handler ---
566
566
+
567
567
+
// --- handleVerify --- (Keep as is)
568
568
+
const handleVerify = async (e) => {
569
569
+
e.preventDefault();
570
570
+
if (!agent || !session) {
571
571
+
setStatusMessage('Error: Not logged in or agent not initialized.');
572
572
+
return;
573
573
+
}
574
574
+
if (!targetHandle) {
575
575
+
setStatusMessage('Please enter a handle to verify.');
576
576
+
return;
577
577
+
}
578
578
+
setIsVerifying(true);
579
579
+
setStatusMessage(`Verifying ${targetHandle}...`);
580
580
+
setShowSuggestions(false); // Hide suggestions when submitting
581
581
+
582
582
+
try {
583
583
+
// 1. Get profile of targetHandle (resolve handle to DID and get display name)
584
584
+
setStatusMessage(`Fetching profile for ${targetHandle}...`);
585
585
+
// Use the proper API namespace method
586
586
+
const profileRes = await agent.api.app.bsky.actor.getProfile({ actor: targetHandle });
587
587
+
const targetDid = profileRes.data.did;
588
588
+
const targetDisplayName = profileRes.data.displayName || profileRes.data.handle;
589
589
+
console.log('Target Profile:', profileRes.data);
590
590
+
591
591
+
// 2. Construct the verification record object
592
592
+
const verificationRecord = {
593
593
+
$type: 'app.bsky.graph.verification', // Using the type you provided
594
594
+
subject: targetDid,
595
595
+
handle: targetHandle, // Include handle for context
596
596
+
displayName: targetDisplayName, // Include display name
597
597
+
createdAt: new Date().toISOString(),
598
598
+
};
599
599
+
console.log('Verification Record to Create:', verificationRecord);
600
600
+
601
601
+
// 3. Create the record using the agent's com.atproto.repo.createRecord method
602
602
+
setStatusMessage(`Creating verification record for ${targetHandle} on your profile...`);
603
603
+
604
604
+
// The correct method is repo.createRecord, not createRecord
605
605
+
const createRes = await agent.api.com.atproto.repo.createRecord({
606
606
+
repo: session.did, // Use session.did
607
607
+
collection: 'app.bsky.graph.verification', // The NSID of the record type
608
608
+
record: verificationRecord,
609
609
+
});
610
610
+
611
611
+
console.log('Create Record Response:', createRes);
612
612
+
613
613
+
// --- Construct Success Message with Intent Link --- (Keep as is)
614
614
+
const verifiedHandle = targetHandle; // Capture handle for this success
615
615
+
const postText = `I just verified @${verifiedHandle} using Bluesky's new decentralized verification system. Try verifying someone yourself using @cred.blue's new verification tool: https://cred.blue/verify`;
616
616
+
const encodedText = encodeURIComponent(postText);
617
617
+
const intentUrl = `https://bsky.app/intent/compose?text=${encodedText}`;
618
618
+
619
619
+
const successMessageJSX = (
620
620
+
<>
621
621
+
Successfully created verification record for {verifiedHandle}!{' '}
622
622
+
<a href={intentUrl} target="_blank" rel="noopener noreferrer" className="verifier-intent-link"> {/* Use plain class */}
623
623
+
Post on Bluesky to let them know.
624
624
+
</a>
625
625
+
</>
626
626
+
);
627
627
+
setStatusMessage(successMessageJSX); // Set JSX as status message
628
628
+
// --- End Intent Link Construction ---
629
629
+
630
630
+
setTargetHandle(''); // Clear input on success
631
631
+
632
632
+
// Refresh the list of verifications
633
633
+
fetchVerifications();
634
634
+
635
635
+
} catch (error) {
636
636
+
console.error('Verification failed:', error);
637
637
+
setStatusMessage(`Verification failed: ${error.message || 'Unknown error'}`);
638
638
+
} finally {
639
639
+
setIsVerifying(false);
640
640
+
}
641
641
+
};
642
642
+
// --- End handleVerify ---
643
643
+
644
644
+
// Function to revoke (delete) a verification - (Keep as is)
645
645
+
const handleRevoke = async (verification) => {
646
646
+
if (!agent || !session) {
647
647
+
setStatusMessage('Error: Not logged in or agent not initialized.');
648
648
+
return;
649
649
+
}
650
650
+
651
651
+
setIsRevoking(true);
652
652
+
setStatusMessage(`Revoking verification for ${verification.handle}...`);
653
653
+
654
654
+
try {
655
655
+
// Extract rkey from URI
656
656
+
// URI format: at://did:plc:xxx/app.bsky.graph.verification/rkey
657
657
+
const parts = verification.uri.split('/');
658
658
+
const rkey = parts[parts.length - 1];
659
659
+
660
660
+
await agent.api.com.atproto.repo.deleteRecord({
661
661
+
repo: session.did, // Use session.did
662
662
+
collection: 'app.bsky.graph.verification',
663
663
+
rkey: rkey
664
664
+
});
665
665
+
666
666
+
console.log('Revoked verification for:', verification.handle);
667
667
+
setStatusMessage(`Successfully revoked verification for ${verification.handle}`);
668
668
+
669
669
+
// Refresh the list of verifications
670
670
+
fetchVerifications();
671
671
+
} catch (error) {
672
672
+
console.error('Revocation failed:', error);
673
673
+
setStatusMessage(`Revocation failed: ${error.message || 'Unknown error'}`);
674
674
+
} finally {
675
675
+
setIsRevoking(false);
676
676
+
}
677
677
+
};
678
678
+
679
679
+
// --- handleSuggestionClick --- (Keep as is)
680
680
+
const handleSuggestionClick = (handle) => {
681
681
+
setTargetHandle(handle);
682
682
+
setSuggestions([]);
683
683
+
setShowSuggestions(false);
684
684
+
inputRef.current?.focus(); // Keep focus on input after selection
685
685
+
};
686
686
+
// --- End handleSuggestionClick ---
687
687
+
688
688
+
// AuthProvider handles redirection if not logged in during its initial load
689
689
+
if (isAuthLoading) {
690
690
+
return <p>Loading authentication...</p>;
691
691
+
}
692
692
+
693
693
+
if (authError) {
694
694
+
return <p>Authentication Error: {authError}. <a href="/login">Please login</a>.</p>;
695
695
+
}
696
696
+
697
697
+
// Display message if session is not available but not loading/erroring
698
698
+
if (!session && !isAuthLoading && !authError) {
699
699
+
return (
700
700
+
<div className="verifier-container">
701
701
+
<h1>Bluesky Verifier Tool</h1>
702
702
+
<p>Please <a href="/login">login with Bluesky</a> to use the verifier tool.</p>
703
703
+
</div>
704
704
+
);
705
705
+
}
706
706
+
707
707
+
// Update combined loading state
708
708
+
const isAnyOperationInProgress = isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity;
709
709
+
710
710
+
// Update tooltip construction to use the (potentially resolved) handles
711
711
+
const trustedVerifiersTooltip = `Checking if any of these Trusted Verifiers have created a verification record for your DID: ${TRUSTED_VERIFIERS.join(', ')}.`;
712
712
+
713
713
+
// Use standard class names, not styles object
714
714
+
return (
715
715
+
<div className="verifier-container">
716
716
+
<h1>Bluesky Verifier Tool</h1>
717
717
+
<p className="verifier-intro-text">
718
718
+
With Bluesky's new decentralized verification system, anyone can verify anyone else and any Bluesky client can choose which accounts to treat as "Trusted Verifiers". It's a first-of-its-kind verification system for a mainstream social platform of this size. Try verifying an account for yourself or check to see who has verified you!
719
719
+
</p>
720
720
+
<div className="verifier-page-header">
721
721
+
<p className="verifier-user-info">Logged in as: {userInfo ? `${userInfo.displayName} (@${userInfo.handle})` : session?.did}</p> {/* Safely access session.did */}
722
722
+
<button
723
723
+
onClick={signOut}
724
724
+
disabled={isAnyOperationInProgress}
725
725
+
className="verifier-sign-out-button"
726
726
+
>
727
727
+
Sign Out
728
728
+
</button>
729
729
+
</div>
730
730
+
<hr />
731
731
+
732
732
+
{/* Verification form */}
733
733
+
<div className="verifier-section">
734
734
+
<h2>Verify a Bluesky User</h2>
735
735
+
<p>Enter the handle of the user you want to verify (e.g., targetuser.bsky.social):</p>
736
736
+
{/* --- Input Container for Autocomplete Positioning --- */}
737
737
+
<div className="verifier-input-container">
738
738
+
<form onSubmit={handleVerify} className="verifier-form-container" style={{ marginBottom: 0 }}>
739
739
+
<input
740
740
+
ref={inputRef} // Assign ref
741
741
+
type="text"
742
742
+
value={targetHandle}
743
743
+
onChange={(e) => {
744
744
+
const newValue = e.target.value;
745
745
+
setTargetHandle(newValue);
746
746
+
if (newValue.length >= 2) {
747
747
+
setShowSuggestions(true);
748
748
+
} else {
749
749
+
setShowSuggestions(false);
750
750
+
setSuggestions([]);
751
751
+
}
752
752
+
}}
753
753
+
onFocus={() => {
754
754
+
if (targetHandle.length >= 2) {
755
755
+
setShowSuggestions(true);
756
756
+
}
757
757
+
}}
758
758
+
placeholder="targetuser.bsky.social"
759
759
+
disabled={isAnyOperationInProgress}
760
760
+
required
761
761
+
className="verifier-input-field"
762
762
+
autoComplete="off"
763
763
+
/>
764
764
+
<button
765
765
+
type="submit"
766
766
+
disabled={isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity}
767
767
+
className="verifier-submit-button"
768
768
+
>
769
769
+
{isVerifying ? 'Verifying...' : 'Create Verification Record'}
770
770
+
</button>
771
771
+
</form>
772
772
+
{/* --- Suggestions Dropdown --- */}
773
773
+
{showSuggestions && (suggestions.length > 0 || isFetchingSuggestions) && (
774
774
+
<ul className="verifier-suggestions-list" ref={suggestionsRef}>
775
775
+
{isFetchingSuggestions && suggestions.length === 0 ? (
776
776
+
<li className="verifier-suggestion-item">Loading...</li>
777
777
+
) : (
778
778
+
suggestions.map((actor) => (
779
779
+
<li
780
780
+
key={actor.did}
781
781
+
className="verifier-suggestion-item"
782
782
+
onMouseDown={(e) => {
783
783
+
e.preventDefault();
784
784
+
handleSuggestionClick(actor.handle);
785
785
+
}}
786
786
+
>
787
787
+
<img src={actor.avatar} alt="" className="verifier-suggestion-avatar" />
788
788
+
<div className="verifier-suggestion-text">
789
789
+
<span className="verifier-suggestion-display-name">{actor.displayName || actor.handle}</span>
790
790
+
<span className="verifier-suggestion-handle">@{actor.handle}</span>
791
791
+
</div>
792
792
+
</li>
793
793
+
))
794
794
+
)}
795
795
+
{suggestions.length === 0 && !isFetchingSuggestions && targetHandle.length >= 2 && (
796
796
+
<li className="verifier-suggestion-item">No users found matching "{targetHandle}"</li>
797
797
+
)}
798
798
+
</ul>
799
799
+
)}
800
800
+
</div>
801
801
+
</div>
802
802
+
803
803
+
{/* Global Status message */}
804
804
+
{statusMessage && (
805
805
+
<div className={`
806
806
+
verifier-status-box
807
807
+
${ typeof statusMessage === 'string' && (statusMessage.includes('failed') || statusMessage.includes('Error'))
808
808
+
? 'verifier-status-box-error'
809
809
+
: 'verifier-status-box-success'
810
810
+
}
811
811
+
`}>
812
812
+
<p>{statusMessage}</p>
813
813
+
</div>
814
814
+
)}
815
815
+
816
816
+
{/* Updated Official Verifiers section */}
817
817
+
<div className="verifier-section">
818
818
+
<div style={{display: 'flex', alignItems: 'center', marginBottom: '10px'}}>
819
819
+
<h2 style={{ display: 'inline-block', marginRight: '8px', marginBottom: 0, border: 'none', padding: 0 }}>Your Verification Status</h2>
820
820
+
<span
821
821
+
title={trustedVerifiersTooltip}
822
822
+
className="verifier-official-verifier-tooltip"
823
823
+
style={{ fontSize: '1.2em' }}
824
824
+
>
825
825
+
(?)
826
826
+
</span>
827
827
+
</div>
828
828
+
829
829
+
{/* Map over trusted verifiers and display individual status */}
830
830
+
<div>
831
831
+
{TRUSTED_VERIFIERS.map(verifierId => {
832
832
+
const status = officialVerifiersStatus[verifierId] || 'idle';
833
833
+
let message = '...';
834
834
+
let icon = '⏳';
835
835
+
let statusClass = '';
836
836
+
837
837
+
switch (status) {
838
838
+
case 'checking':
839
839
+
message = `Checking ${verifierId}...`;
840
840
+
icon = '⏳';
841
841
+
statusClass = 'verifier-checking-status';
842
842
+
break;
843
843
+
case 'verified':
844
844
+
message = `Verified by ${verifierId}.`;
845
845
+
icon = '✅';
846
846
+
statusClass = 'verifier-verified-status';
847
847
+
break;
848
848
+
case 'not_verified':
849
849
+
message = `Not verified by ${verifierId}.`;
850
850
+
icon = '❌';
851
851
+
statusClass = 'verifier-not-verified-status';
852
852
+
break;
853
853
+
case 'error':
854
854
+
message = `Error checking ${verifierId}.`;
855
855
+
icon = '⚠️';
856
856
+
statusClass = 'verifier-error-status';
857
857
+
break;
858
858
+
default: // idle
859
859
+
message = `Pending check for ${verifierId}.`;
860
860
+
icon = '⏳';
861
861
+
statusClass = 'verifier-idle-status';
862
862
+
}
863
863
+
864
864
+
return (
865
865
+
<p key={verifierId} className={`verifier-official-verifier-note ${statusClass}`}>
866
866
+
{icon} {message}
867
867
+
</p>
868
868
+
);
869
869
+
})}
870
870
+
</div>
871
871
+
</div>
872
872
+
873
873
+
{/* Updated section for Network Verifications */}
874
874
+
<div className="verifier-section">
875
875
+
<div className="verifier-list-header">
876
876
+
<h2>Who's Verified You?</h2>
877
877
+
<button
878
878
+
onClick={checkNetworkVerifications}
879
879
+
disabled={isAnyOperationInProgress}
880
880
+
className="verifier-action-button verifier-check-network-button"
881
881
+
>
882
882
+
{isLoadingNetwork ? 'Checking Network...' : 'Check Network Now'}
883
883
+
</button>
884
884
+
</div>
885
885
+
886
886
+
{/* Display local status message */}
887
887
+
{(isLoadingNetwork || networkStatusMessage) && (
888
888
+
<p className="verifier-network-status">{networkStatusMessage}</p>
889
889
+
)}
890
890
+
891
891
+
{!isLoadingNetwork && networkChecked && (
892
892
+
<div className="verifier-network-results">
893
893
+
{/* --- Mutuals Verified Me --- */}
894
894
+
<p>
895
895
+
{networkVerifications.mutualsVerifiedMe.length > 0
896
896
+
? `${networkVerifications.mutualsVerifiedMe.length} mutual(s) have verified you:`
897
897
+
: "None of your mutuals have verified you yet."}
898
898
+
</p>
899
899
+
{networkVerifications.mutualsVerifiedMe.length > 0 && (
900
900
+
<ul className="verifier-verifier-list">
901
901
+
{networkVerifications.mutualsVerifiedMe.map(account => (
902
902
+
<li key={account.did}>
903
903
+
{account.displayName} (@{account.handle})
904
904
+
</li>
905
905
+
))}
906
906
+
</ul>
907
907
+
)}
908
908
+
909
909
+
{/* --- Follows Verified Me --- */}
910
910
+
<p style={{marginTop: '15px'}}>
911
911
+
{networkVerifications.followsVerifiedMe.length > 0
912
912
+
? `${networkVerifications.followsVerifiedMe.length} account(s) you follow have verified you:`
913
913
+
: "None of the accounts you follow have verified you yet."}
914
914
+
</p>
915
915
+
{networkVerifications.followsVerifiedMe.length > 0 && (
916
916
+
<ul className="verifier-verifier-list">
917
917
+
{networkVerifications.followsVerifiedMe.map(account => (
918
918
+
<li key={account.did}>
919
919
+
{account.displayName} (@{account.handle})
920
920
+
</li>
921
921
+
))}
922
922
+
</ul>
923
923
+
)}
924
924
+
925
925
+
{/* --- Additional Context - Verified Others --- */}
926
926
+
<div className="verifier-additional-context">
927
927
+
<p>
928
928
+
{networkVerifications.mutualsVerifiedAnyone} of your {networkVerifications.fetchedMutualsCount} fetched mutuals have verified others.
929
929
+
</p>
930
930
+
<p>
931
931
+
{networkVerifications.followsVerifiedAnyone} of the {networkVerifications.fetchedFollowsCount} accounts you follow have verified others.
932
932
+
</p>
933
933
+
</div>
934
934
+
935
935
+
{/* --- Network Stats Share Link --- */}
936
936
+
{(() => { // IIFE to encapsulate logic
937
937
+
const statsText = `Here are my expanded verification stats:\n\n` +
938
938
+
`${networkVerifications.mutualsVerifiedMe.length} of my mutuals have verified me\n` +
939
939
+
`${networkVerifications.followsVerifiedMe.length} account(s) that I follow have verified me\n` +
940
940
+
`${networkVerifications.mutualsVerifiedAnyone} of my ${networkVerifications.fetchedMutualsCount} mutuals have verified others\n` +
941
941
+
`${networkVerifications.followsVerifiedAnyone} of the ${networkVerifications.fetchedFollowsCount} accounts I follow have verified others\n\n` +
942
942
+
`See who in your network has verified you here: https://cred.blue/verify`;
943
943
+
const encodedStatsText = encodeURIComponent(statsText);
944
944
+
const statsIntentUrl = `https://bsky.app/intent/compose?text=${encodedStatsText}`;
945
945
+
946
946
+
return (
947
947
+
<a
948
948
+
href={statsIntentUrl}
949
949
+
target="_blank"
950
950
+
rel="noopener noreferrer"
951
951
+
className="verifier-share-stats-link"
952
952
+
>
953
953
+
Share your verification stats on Bluesky!
954
954
+
</a>
955
955
+
);
956
956
+
})()}
957
957
+
958
958
+
</div>
959
959
+
)}
960
960
+
{!isLoadingNetwork && !networkChecked && (
961
961
+
<p>Click "Check Network Now" to see verifications from your network.</p>
962
962
+
)}
963
963
+
</div>
964
964
+
965
965
+
{/* List of verified accounts */}
966
966
+
<div className="verifier-section">
967
967
+
<div className="verifier-list-header">
968
968
+
<h2>Accounts You've Verified</h2>
969
969
+
<button
970
970
+
onClick={() => fetchVerifications()}
971
971
+
disabled={isAnyOperationInProgress}
972
972
+
className="verifier-action-button verifier-refresh-button"
973
973
+
>
974
974
+
Refresh List
975
975
+
</button>
976
976
+
</div>
977
977
+
{isLoadingVerifications ? (
978
978
+
<p>Loading verifications...</p>
979
979
+
) : verifications.length === 0 ? (
980
980
+
<p>You haven't verified any accounts yet.</p>
981
981
+
) : (
982
982
+
<ul className="verifier-list">
983
983
+
{verifications.map((verification) => (
984
984
+
<li
985
985
+
key={verification.uri}
986
986
+
className={`
987
987
+
verifier-list-item
988
988
+
${verification.validityChecked && !verification.isValid ? 'verifier-list-item-invalid' : ''}
989
989
+
`}
990
990
+
>
991
991
+
<div className="verifier-list-item-content">
992
992
+
<div style={{ fontWeight: 'bold' }}>{verification.displayName}</div>
993
993
+
<div className="verifier-list-item-handle">@{verification.handle}</div>
994
994
+
<div className="verifier-list-item-date">
995
995
+
Verified: {new Date(verification.createdAt).toLocaleString()}
996
996
+
</div>
997
997
+
998
998
+
{verification.validityChecked && !verification.isValid && (
999
999
+
<div className="verifier-validity-warning">
1000
1000
+
{verification.validityError ? (
1001
1001
+
<p>⚠️ Could not verify current profile data</p>
1002
1002
+
) : (
1003
1003
+
<>
1004
1004
+
<p><strong>⚠️ Profile has changed since verification</strong></p>
1005
1005
+
<p>
1006
1006
+
<span>Current handle: @{verification.currentHandle}</span><br />
1007
1007
+
<span>Current display name: {verification.currentDisplayName}</span>
1008
1008
+
</p>
1009
1009
+
</>
1010
1010
+
)}
1011
1011
+
</div>
1012
1012
+
)}
1013
1013
+
</div>
1014
1014
+
<div className="verifier-list-item-actions">
1015
1015
+
<button
1016
1016
+
onClick={() => handleRevoke(verification)}
1017
1017
+
disabled={isAnyOperationInProgress}
1018
1018
+
className="verifier-revoke-button"
1019
1019
+
>
1020
1020
+
{isRevoking ? 'Revoking...' : 'Revoke Verification'}
1021
1021
+
</button>
1022
1022
+
</div>
1023
1023
+
</li>
1024
1024
+
))}
1025
1025
+
</ul>
1026
1026
+
)}
1027
1027
+
</div>
1028
1028
+
</div>
1029
1029
+
);
1030
1030
+
}
1031
1031
+
1032
1032
+
export default Verifier;