An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(identity-wallet): add OAuth frontend screens and auth_ready listener (MM-149 phase 7, tasks 3-4)

- Task 3: Create AuthenticatingScreen.svelte component
- Auto-invokes startOAuthFlow() on mount
- Shows spinner + status text while authenticating
- Calls onresolved() on success, onfailed() on error
- Follows DIDCeremonyScreen pattern with Svelte 5 runes

- Task 4: Update +page.svelte with three new OAuth steps
- Add three step types: authenticating, authenticated, auth_failed
- Add authError state to track OAuth errors
- Add onMount listener for auth_ready event from Rust backend
(emitted when app restarts with stored tokens in Keychain)
- Add rendering blocks for all three steps:
- authenticating: delegates to AuthenticatingScreen component
- authenticated: shows success message (ready for use)
- auth_failed: shows error code + Try again/Start over buttons
- Update complete step with Continue button to transition to authenticating
- Add oauth-screen and oauth-actions CSS classes

Verifies MM-149.AC7.1-4, AC8.1-2 (all OAuth-related acceptance criteria).

authored by

Malpercio and committed by
Tangled
20468459 94ca3be1

+179 -2
+61
apps/identity-wallet/src/lib/components/onboarding/AuthenticatingScreen.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { startOAuthFlow, type OAuthError } from '$lib/ipc'; 4 + 5 + let { 6 + onresolved, 7 + onfailed, 8 + }: { 9 + onresolved: () => void; 10 + onfailed: (err: OAuthError) => void; 11 + } = $props(); 12 + 13 + async function authenticate() { 14 + try { 15 + await startOAuthFlow(); 16 + onresolved(); 17 + } catch (raw) { 18 + onfailed(raw as OAuthError); 19 + } 20 + } 21 + 22 + onMount(() => { 23 + authenticate(); 24 + }); 25 + </script> 26 + 27 + <div class="screen"> 28 + <div class="spinner" aria-label="Loading"></div> 29 + <p class="status">Opening browser for authentication…</p> 30 + </div> 31 + 32 + <style> 33 + .screen { 34 + display: flex; 35 + flex-direction: column; 36 + align-items: center; 37 + justify-content: center; 38 + height: 100%; 39 + gap: 24px; 40 + padding: 32px; 41 + } 42 + 43 + .spinner { 44 + width: 40px; 45 + height: 40px; 46 + border: 4px solid #e5e7eb; 47 + border-top-color: #007aff; 48 + border-radius: 50%; 49 + animation: spin 0.8s linear infinite; 50 + } 51 + 52 + @keyframes spin { 53 + to { transform: rotate(360deg); } 54 + } 55 + 56 + .status { 57 + text-align: center; 58 + color: #6b7280; 59 + font-size: 1rem; 60 + } 61 + </style>
+118 -2
apps/identity-wallet/src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 + import { listen } from '@tauri-apps/api/event'; 3 + import { onMount } from 'svelte'; 2 4 import WelcomeScreen from '$lib/components/onboarding/WelcomeScreen.svelte'; 3 5 import ClaimCodeScreen from '$lib/components/onboarding/ClaimCodeScreen.svelte'; 4 6 import EmailScreen from '$lib/components/onboarding/EmailScreen.svelte'; ··· 8 10 import DIDCeremonyScreen from '$lib/components/onboarding/DIDCeremonyScreen.svelte'; 9 11 import DIDSuccessScreen from '$lib/components/onboarding/DIDSuccessScreen.svelte'; 10 12 import ShamirBackupScreen from '$lib/components/onboarding/ShamirBackupScreen.svelte'; 11 - import { createAccount, type CreateAccountError } from '$lib/ipc'; 13 + import AuthenticatingScreen from '$lib/components/onboarding/AuthenticatingScreen.svelte'; 14 + import { createAccount, type CreateAccountError, type OAuthError } from '$lib/ipc'; 12 15 13 16 // ── Onboarding step type ───────────────────────────────────────────────── 14 17 // ··· 30 33 | 'did_ceremony' 31 34 | 'did_success' 32 35 | 'shamir_backup' 33 - | 'complete'; 36 + | 'complete' 37 + | 'authenticating' 38 + | 'authenticated' 39 + | 'auth_failed'; 34 40 35 41 // ── State ──────────────────────────────────────────────────────────────── 36 42 ··· 45 51 {} 46 52 ); 47 53 54 + let authError = $state<OAuthError | null>(null); 55 + 48 56 // ── Navigation helpers ─────────────────────────────────────────────────── 49 57 50 58 function goTo(next: OnboardingStep) { 51 59 errors = {}; 52 60 step = next; 53 61 } 62 + 63 + // ── OAuth event listener ────────────────────────────────────────────────── 64 + 65 + onMount(() => { 66 + listen('auth_ready', () => { 67 + goTo('authenticated'); 68 + }); 69 + // Note: We intentionally don't await listen() or return a cleanup function here. 70 + // Svelte 5's onMount does not await async cleanup return values (it would receive a 71 + // Promise, not the unlisten function). Since +page.svelte is the root page and never 72 + // unmounts during the app lifecycle, the listener persists for the app's lifetime, 73 + // which is the correct behavior. 74 + }); 54 75 55 76 // ── Account creation ───────────────────────────────────────────────────── 56 77 ··· 169 190 <div class="complete-icon" aria-hidden="true">✓</div> 170 191 <h2>You're All Set!</h2> 171 192 <p>Your identity is ready. Your recovery key has been safely backed up.</p> 193 + <button class="cta" onclick={() => goTo('authenticating')}> 194 + Continue 195 + </button> 196 + </div> 197 + 198 + {:else if step === 'authenticating'} 199 + <AuthenticatingScreen 200 + onresolved={() => goTo('authenticated')} 201 + onfailed={(err) => { 202 + authError = err; 203 + goTo('auth_failed'); 204 + }} 205 + /> 206 + 207 + {:else if step === 'authenticated'} 208 + <div class="oauth-screen"> 209 + <div class="oauth-icon" aria-hidden="true">✓</div> 210 + <h2 class="oauth-title">Authenticated</h2> 211 + <p class="oauth-body">Your identity wallet is ready.</p> 212 + </div> 213 + 214 + {:else if step === 'auth_failed'} 215 + <div class="oauth-screen"> 216 + <div class="oauth-icon" aria-hidden="true">✗</div> 217 + <h2 class="oauth-title">Authentication Failed</h2> 218 + {#if authError} 219 + <p class="oauth-error-code">{authError.code}</p> 220 + {/if} 221 + <div class="oauth-actions"> 222 + <button 223 + class="cta" 224 + onclick={() => { 225 + authError = null; 226 + goTo('authenticating'); 227 + }} 228 + > 229 + Try again 230 + </button> 231 + <button 232 + class="cta cta--secondary" 233 + onclick={() => { 234 + authError = null; 235 + goTo('welcome'); 236 + }} 237 + > 238 + Start over 239 + </button> 240 + </div> 172 241 </div> 173 242 {/if} 174 243 </div> ··· 214 283 font-size: 0.95rem; 215 284 color: #6b7280; 216 285 margin: 0; 286 + } 287 + 288 + .oauth-screen { 289 + display: flex; 290 + flex-direction: column; 291 + align-items: center; 292 + justify-content: center; 293 + height: 100%; 294 + gap: 24px; 295 + padding: 32px; 296 + text-align: center; 297 + } 298 + 299 + .oauth-icon { 300 + font-size: 3rem; 301 + } 302 + 303 + .oauth-title { 304 + font-size: 1.5rem; 305 + font-weight: 700; 306 + color: #111827; 307 + margin: 0; 308 + } 309 + 310 + .oauth-body { 311 + color: #6b7280; 312 + font-size: 1rem; 313 + margin: 0; 314 + } 315 + 316 + .oauth-error-code { 317 + font-family: monospace; 318 + font-size: 0.875rem; 319 + color: #6b7280; 320 + margin: 0; 321 + } 322 + 323 + .oauth-actions { 324 + display: flex; 325 + flex-direction: column; 326 + gap: 12px; 327 + width: 100%; 328 + } 329 + 330 + .cta--secondary { 331 + background: #f3f4f6; 332 + color: #374151; 217 333 } 218 334 </style>