this repo has no description
1<script lang="ts">
2 import { confirmSignup, resendVerification, getAuthState } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4
5 const STORAGE_KEY = 'bspds_pending_verification'
6
7 interface PendingVerification {
8 did: string
9 handle: string
10 channel: string
11 }
12
13 let pendingVerification = $state<PendingVerification | null>(null)
14 let verificationCode = $state('')
15 let submitting = $state(false)
16 let resendingCode = $state(false)
17 let error = $state<string | null>(null)
18 let resendMessage = $state<string | null>(null)
19
20 const auth = getAuthState()
21
22 $effect(() => {
23 if (auth.session) {
24 clearPendingVerification()
25 navigate('/dashboard')
26 }
27 })
28
29 $effect(() => {
30 const stored = localStorage.getItem(STORAGE_KEY)
31 if (stored) {
32 try {
33 pendingVerification = JSON.parse(stored)
34 } catch {
35 pendingVerification = null
36 }
37 }
38 })
39
40 function clearPendingVerification() {
41 localStorage.removeItem(STORAGE_KEY)
42 pendingVerification = null
43 }
44
45 async function handleVerification(e: Event) {
46 e.preventDefault()
47 if (!pendingVerification || !verificationCode.trim()) return
48
49 submitting = true
50 error = null
51
52 try {
53 await confirmSignup(pendingVerification.did, verificationCode.trim())
54 clearPendingVerification()
55 navigate('/dashboard')
56 } catch (e: any) {
57 error = e.message || 'Verification failed'
58 } finally {
59 submitting = false
60 }
61 }
62
63 async function handleResendCode() {
64 if (!pendingVerification || resendingCode) return
65
66 resendingCode = true
67 resendMessage = null
68 error = null
69
70 try {
71 await resendVerification(pendingVerification.did)
72 resendMessage = 'Verification code resent!'
73 } catch (e: any) {
74 error = e.message || 'Failed to resend code'
75 } finally {
76 resendingCode = false
77 }
78 }
79
80 function channelLabel(ch: string): string {
81 switch (ch) {
82 case 'email': return 'Email'
83 case 'discord': return 'Discord'
84 case 'telegram': return 'Telegram'
85 case 'signal': return 'Signal'
86 default: return ch
87 }
88 }
89</script>
90
91<div class="verify-container">
92 {#if error}
93 <div class="error">{error}</div>
94 {/if}
95
96 {#if pendingVerification}
97 <h1>Verify Your Account</h1>
98 <p class="subtitle">
99 We've sent a verification code to your {channelLabel(pendingVerification.channel)}.
100 Enter it below to complete registration.
101 </p>
102 <p class="handle-info">Verifying account: <strong>@{pendingVerification.handle}</strong></p>
103
104 {#if resendMessage}
105 <div class="success">{resendMessage}</div>
106 {/if}
107
108 <form onsubmit={(e) => { e.preventDefault(); handleVerification(e); }}>
109 <div class="field">
110 <label for="verification-code">Verification Code</label>
111 <input
112 id="verification-code"
113 type="text"
114 bind:value={verificationCode}
115 placeholder="Enter 6-digit code"
116 disabled={submitting}
117 required
118 maxlength="6"
119 inputmode="numeric"
120 autocomplete="one-time-code"
121 />
122 </div>
123
124 <button type="submit" disabled={submitting || !verificationCode.trim()}>
125 {submitting ? 'Verifying...' : 'Verify Account'}
126 </button>
127
128 <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}>
129 {resendingCode ? 'Resending...' : 'Resend Code'}
130 </button>
131 </form>
132
133 <p class="cancel-link">
134 <a href="#/register" onclick={() => clearPendingVerification()}>Start over with a different account</a>
135 </p>
136 {:else}
137 <h1>Account Verification</h1>
138 <p class="subtitle">No pending verification found.</p>
139 <p class="no-pending-info">
140 If you recently created an account and need to verify it, you may need to create a new account.
141 If you already verified your account, you can sign in.
142 </p>
143 <div class="actions">
144 <a href="#/register" class="btn">Create Account</a>
145 <a href="#/login" class="btn secondary">Sign In</a>
146 </div>
147 {/if}
148</div>
149
150<style>
151 .verify-container {
152 max-width: 400px;
153 margin: 4rem auto;
154 padding: 2rem;
155 }
156
157 h1 {
158 margin: 0 0 0.5rem 0;
159 }
160
161 .subtitle {
162 color: var(--text-secondary);
163 margin: 0 0 1rem 0;
164 }
165
166 .handle-info {
167 font-size: 0.9rem;
168 color: var(--text-secondary);
169 margin: 0 0 1.5rem 0;
170 }
171
172 .no-pending-info {
173 color: var(--text-secondary);
174 margin: 1rem 0 1.5rem 0;
175 }
176
177 form {
178 display: flex;
179 flex-direction: column;
180 gap: 1rem;
181 }
182
183 .field {
184 display: flex;
185 flex-direction: column;
186 gap: 0.25rem;
187 }
188
189 label {
190 font-size: 0.875rem;
191 font-weight: 500;
192 }
193
194 input {
195 padding: 0.75rem;
196 border: 1px solid var(--border-color-light);
197 border-radius: 4px;
198 font-size: 1rem;
199 background: var(--bg-input);
200 color: var(--text-primary);
201 }
202
203 input:focus {
204 outline: none;
205 border-color: var(--accent);
206 }
207
208 button, .btn {
209 padding: 0.75rem;
210 background: var(--accent);
211 color: white;
212 border: none;
213 border-radius: 4px;
214 font-size: 1rem;
215 cursor: pointer;
216 text-decoration: none;
217 text-align: center;
218 display: inline-block;
219 }
220
221 button:hover:not(:disabled), .btn:hover {
222 background: var(--accent-hover);
223 }
224
225 button:disabled {
226 opacity: 0.6;
227 cursor: not-allowed;
228 }
229
230 button.secondary, .btn.secondary {
231 background: transparent;
232 color: var(--accent);
233 border: 1px solid var(--accent);
234 }
235
236 button.secondary:hover:not(:disabled), .btn.secondary:hover {
237 background: var(--accent);
238 color: white;
239 }
240
241 .error {
242 padding: 0.75rem;
243 background: var(--error-bg);
244 border: 1px solid var(--error-border);
245 border-radius: 4px;
246 color: var(--error-text);
247 margin-bottom: 1rem;
248 }
249
250 .success {
251 padding: 0.75rem;
252 background: var(--success-bg);
253 border: 1px solid var(--success-border);
254 border-radius: 4px;
255 color: var(--success-text);
256 margin-bottom: 1rem;
257 }
258
259 .cancel-link {
260 text-align: center;
261 margin-top: 1.5rem;
262 font-size: 0.875rem;
263 }
264
265 .cancel-link a {
266 color: var(--text-secondary);
267 }
268
269 .actions {
270 display: flex;
271 gap: 1rem;
272 }
273
274 .actions .btn {
275 flex: 1;
276 }
277</style>