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 import { _ } from '../lib/i18n'
5
6 const STORAGE_KEY = 'tranquil_pds_pending_verification'
7
8 interface PendingVerification {
9 did: string
10 handle: string
11 channel: string
12 }
13
14 let pendingVerification = $state<PendingVerification | null>(null)
15 let verificationCode = $state('')
16 let submitting = $state(false)
17 let resendingCode = $state(false)
18 let error = $state<string | null>(null)
19 let resendMessage = $state<string | null>(null)
20
21 const auth = getAuthState()
22
23 $effect(() => {
24 if (auth.session) {
25 clearPendingVerification()
26 navigate('/dashboard')
27 }
28 })
29
30 $effect(() => {
31 const stored = localStorage.getItem(STORAGE_KEY)
32 if (stored) {
33 try {
34 pendingVerification = JSON.parse(stored)
35 } catch {
36 pendingVerification = null
37 }
38 }
39 })
40
41 function clearPendingVerification() {
42 localStorage.removeItem(STORAGE_KEY)
43 pendingVerification = null
44 }
45
46 async function handleVerification(e: Event) {
47 e.preventDefault()
48 if (!pendingVerification || !verificationCode.trim()) return
49
50 submitting = true
51 error = null
52
53 try {
54 await confirmSignup(pendingVerification.did, verificationCode.trim())
55 clearPendingVerification()
56 navigate('/dashboard')
57 } catch (e: any) {
58 error = e.message || 'Verification failed'
59 } finally {
60 submitting = false
61 }
62 }
63
64 async function handleResendCode() {
65 if (!pendingVerification || resendingCode) return
66
67 resendingCode = true
68 resendMessage = null
69 error = null
70
71 try {
72 await resendVerification(pendingVerification.did)
73 resendMessage = 'Verification code resent!'
74 } catch (e: any) {
75 error = e.message || 'Failed to resend code'
76 } finally {
77 resendingCode = false
78 }
79 }
80
81 function channelLabel(ch: string): string {
82 switch (ch) {
83 case 'email': return $_('register.email')
84 case 'discord': return $_('register.discord')
85 case 'telegram': return $_('register.telegram')
86 case 'signal': return $_('register.signal')
87 default: return ch
88 }
89 }
90</script>
91
92<div class="verify-page">
93 {#if error}
94 <div class="message error">{error}</div>
95 {/if}
96
97 {#if pendingVerification}
98 <h1>{$_('verify.title')}</h1>
99 <p class="subtitle">
100 {$_('verify.subtitle', { values: { channel: channelLabel(pendingVerification.channel) } })}
101 </p>
102 <p class="handle-info">{$_('verify.verifyingAccount', { values: { handle: pendingVerification.handle } })}</p>
103
104 {#if resendMessage}
105 <div class="message success">{resendMessage}</div>
106 {/if}
107
108 <form onsubmit={(e) => { e.preventDefault(); handleVerification(e); }}>
109 <div class="field">
110 <label for="verification-code">{$_('verify.codeLabel')}</label>
111 <input
112 id="verification-code"
113 type="text"
114 bind:value={verificationCode}
115 placeholder={$_('verify.codePlaceholder')}
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 ? $_('verify.verifying') : $_('verify.verifyButton')}
126 </button>
127
128 <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}>
129 {resendingCode ? $_('verify.resending') : $_('verify.resendCode')}
130 </button>
131 </form>
132
133 <p class="link-text">
134 <a href="#/register" onclick={() => clearPendingVerification()}>{$_('verify.startOver')}</a>
135 </p>
136 {:else}
137 <h1>{$_('verify.title')}</h1>
138 <p class="subtitle">{$_('verify.noPending')}</p>
139 <p class="info-text">{$_('verify.noPendingInfo')}</p>
140
141 <div class="actions">
142 <a href="#/register" class="btn">{$_('verify.createAccount')}</a>
143 <a href="#/login" class="btn secondary">{$_('verify.signIn')}</a>
144 </div>
145 {/if}
146</div>
147
148<style>
149 .verify-page {
150 max-width: var(--width-sm);
151 margin: var(--space-9) auto;
152 padding: var(--space-7);
153 }
154
155 h1 {
156 margin: 0 0 var(--space-3) 0;
157 }
158
159 .subtitle {
160 color: var(--text-secondary);
161 margin: 0 0 var(--space-4) 0;
162 }
163
164 .handle-info {
165 font-size: var(--text-sm);
166 color: var(--text-secondary);
167 margin: 0 0 var(--space-6) 0;
168 }
169
170 .info-text {
171 color: var(--text-secondary);
172 margin: var(--space-4) 0 var(--space-6) 0;
173 }
174
175 form {
176 display: flex;
177 flex-direction: column;
178 gap: var(--space-4);
179 }
180
181 .link-text {
182 text-align: center;
183 margin-top: var(--space-6);
184 font-size: var(--text-sm);
185 }
186
187 .link-text a {
188 color: var(--text-secondary);
189 }
190
191 .actions {
192 display: flex;
193 gap: var(--space-4);
194 }
195
196 .btn {
197 flex: 1;
198 display: inline-block;
199 padding: var(--space-4);
200 background: var(--accent);
201 color: var(--text-inverse);
202 border: none;
203 border-radius: var(--radius-md);
204 font-size: var(--text-base);
205 font-weight: var(--font-medium);
206 cursor: pointer;
207 text-decoration: none;
208 text-align: center;
209 }
210
211 .btn:hover {
212 background: var(--accent-hover);
213 text-decoration: none;
214 }
215
216 .btn.secondary {
217 background: transparent;
218 color: var(--accent);
219 border: 1px solid var(--accent);
220 }
221
222 .btn.secondary:hover {
223 background: var(--accent);
224 color: var(--text-inverse);
225 }
226</style>