+63
-5
src/components/auth/Login.tsx
+63
-5
src/components/auth/Login.tsx
···
16
16
}>;
17
17
}
18
18
19
+
type LoginStep = 'idle' | 'resolving-handle' | 'resolving-did' | 'connecting-pds' | 'authenticating' | 'success';
20
+
19
21
export default function Login({ onLogin }: LoginProps) {
20
22
const [handle, setHandle] = useState('');
21
23
const [password, setPassword] = useState('');
22
24
const [error, setError] = useState('');
23
25
const [appPasswordAttempts, setAppPasswordAttempts] = useState(0);
26
+
const [loginStep, setLoginStep] = useState<LoginStep>('idle');
24
27
const navigate = useNavigate();
25
28
26
29
const isAppPassword = (password: string) => {
···
28
31
return /^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/.test(password);
29
32
};
30
33
34
+
const getStepMessage = (step: LoginStep) => {
35
+
switch (step) {
36
+
case 'resolving-handle':
37
+
return 'Resolving your handle...';
38
+
case 'resolving-did':
39
+
return 'Resolving your DID...';
40
+
case 'connecting-pds':
41
+
return 'Connecting to your Personal Data Server...';
42
+
case 'authenticating':
43
+
return 'Authenticating your credentials...';
44
+
case 'success':
45
+
return 'Login successful! Redirecting...';
46
+
default:
47
+
return '';
48
+
}
49
+
};
50
+
31
51
const handleSubmit = async (e: React.FormEvent) => {
32
52
e.preventDefault();
33
53
setError('');
54
+
setLoginStep('resolving-handle');
34
55
56
+
// app password check and debug method
35
57
if (isAppPassword(password)) {
36
58
if (appPasswordAttempts < 3) {
37
59
setAppPasswordAttempts(appPasswordAttempts + 1);
38
-
setError(`Warning: You have entered an app password, which does not allow you to migrate your account.`);
60
+
setError(`You have entered an app password, which does not allow for you to migrate your account. Please enter your main account password instead.`);
61
+
setLoginStep('idle');
39
62
return;
40
63
}
41
64
}
42
65
43
66
setHandle(handle.trim());
44
-
setPassword(password.trim());
45
67
46
68
try {
47
69
// Create temporary agent to resolve DID
48
-
const tempAgent = new AtpAgent({ service: 'https://bsky.social' });
70
+
const tempAgent = new AtpAgent({ service: 'https://public.api.bsky.app' });
49
71
50
72
// Get DID document from handle
73
+
setLoginStep('resolving-handle');
51
74
const didResponse = await tempAgent.com.atproto.identity.resolveHandle({
52
75
handle: handle
53
76
});
54
77
78
+
if (!didResponse.success) {
79
+
// Try did:web resolution first
80
+
const domain = handle.split('.').join(':');
81
+
const webDid = `did:web:${domain}`;
82
+
try {
83
+
const webResponse = await fetch(`https://${handle}/.well-known/did.json`);
84
+
if (webResponse.ok) {
85
+
// If successful, continue with the did:web
86
+
didResponse.data.did = webDid;
87
+
} else {
88
+
throw new Error('Invalid handle');
89
+
}
90
+
} catch {
91
+
throw new Error('Invalid handle');
92
+
}
93
+
}
94
+
55
95
// Get PDS endpoint from DID document
96
+
setLoginStep('resolving-did');
56
97
let didDocResponse;
57
98
const did = didResponse.data.did;
58
99
···
72
113
});
73
114
}
74
115
116
+
setLoginStep('connecting-pds');
75
117
const pds = ((didDocResponse.data as unknown) as DidDocument).service.find((s) => s.id === '#atproto_pds')?.serviceEndpoint || 'https://bsky.social';
76
118
77
119
const agent = new AtpAgent({ service: pds });
120
+
121
+
setLoginStep('authenticating');
78
122
await agent.login({ identifier: handle, password });
123
+
124
+
setLoginStep('success');
79
125
onLogin(agent);
80
126
navigate('/actions');
81
127
} catch (err) {
82
128
setError(err instanceof Error ? err.message : 'Login failed');
129
+
setLoginStep('idle');
83
130
}
84
131
};
85
132
···
101
148
placeholder="Handle (e.g., example.bsky.social)"
102
149
value={handle}
103
150
onChange={(e) => setHandle(e.target.value)}
151
+
disabled={loginStep !== 'idle'}
104
152
/>
105
153
</div>
106
154
<div className="form-group">
···
111
159
placeholder="Password"
112
160
value={password}
113
161
onChange={(e) => setPassword(e.target.value)}
162
+
disabled={loginStep !== 'idle'}
114
163
/>
115
164
</div>
116
165
117
166
{error && <div className="error-message">{error}</div>}
167
+
{loginStep !== 'idle' && (
168
+
<div className="loading-message">
169
+
{getStepMessage(loginStep)}
170
+
</div>
171
+
)}
118
172
119
-
<button type="submit" className="submit-button">
120
-
Sign in
173
+
<button
174
+
type="submit"
175
+
className="submit-button"
176
+
disabled={loginStep !== 'idle'}
177
+
>
178
+
{loginStep === 'idle' ? 'Sign in' : 'Signing in...'}
121
179
</button>
122
180
</form>
123
181
</div>
+24
src/styles/App.css
+24
src/styles/App.css
···
421
421
422
422
.footer-link strong {
423
423
font-weight: 600;
424
+
}
425
+
426
+
.loading-message {
427
+
margin: 10px 0;
428
+
padding: 10px;
429
+
background-color: var(--bg-color);
430
+
border-radius: 4px;
431
+
color: var(--text-color);
432
+
font-size: 0.9em;
433
+
text-align: center;
434
+
}
435
+
436
+
.form-input:disabled {
437
+
background-color: var(--bg-color);
438
+
border-color: var(--border-color);
439
+
color: var(--text-light);
440
+
cursor: not-allowed;
441
+
opacity: 0.8;
442
+
}
443
+
444
+
.submit-button:disabled {
445
+
background-color: var(--text-light);
446
+
cursor: not-allowed;
447
+
opacity: 0.8;
424
448
}