An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

feat(identity-wallet): implement onboarding state machine in +page.svelte

authored by malpercio.dev and committed by

Tangled 83f0818d 6702cd61

+132 -51
+132 -51
apps/identity-wallet/src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { greet } from '$lib/ipc'; 2 + import WelcomeScreen from '$lib/components/onboarding/WelcomeScreen.svelte'; 3 + import ClaimCodeScreen from '$lib/components/onboarding/ClaimCodeScreen.svelte'; 4 + import EmailScreen from '$lib/components/onboarding/EmailScreen.svelte'; 5 + import HandleScreen from '$lib/components/onboarding/HandleScreen.svelte'; 6 + import LoadingScreen from '$lib/components/onboarding/LoadingScreen.svelte'; 7 + import { createAccount, type CreateAccountError } from '$lib/ipc'; 8 + 9 + // ── Onboarding step type ───────────────────────────────────────────────── 10 + 11 + type OnboardingStep = 12 + | 'welcome' 13 + | 'claim_code' 14 + | 'email' 15 + | 'handle' 16 + | 'loading' 17 + | 'did_ceremony'; 18 + 19 + // ── State ──────────────────────────────────────────────────────────────── 3 20 4 - let name = $state('World'); 5 - let greetMsg = $state(''); 21 + let step = $state<OnboardingStep>('welcome'); 22 + let form = $state({ claimCode: '', email: '', handle: '' }); 6 23 7 - async function handleGreet() { 8 - greetMsg = await greet(name); 24 + /** 25 + * Per-field error messages displayed by each screen. 26 + * Cleared when the user navigates forward to the next step. 27 + */ 28 + let errors = $state<{ claimCode?: string; email?: string; handle?: string }>( 29 + {} 30 + ); 31 + 32 + // ── Navigation helpers ─────────────────────────────────────────────────── 33 + 34 + function goTo(next: OnboardingStep) { 35 + errors = {}; 36 + step = next; 37 + } 38 + 39 + // ── Account creation ───────────────────────────────────────────────────── 40 + 41 + async function submitAccount() { 42 + step = 'loading'; 43 + errors = {}; 44 + 45 + try { 46 + const result = await createAccount({ 47 + claimCode: form.claimCode, 48 + email: form.email, 49 + handle: form.handle, 50 + }); 51 + 52 + if (result.nextStep === 'did_creation') { 53 + step = 'did_ceremony'; 54 + } else { 55 + // Unexpected nextStep value — treat as success and advance anyway. 56 + step = 'did_ceremony'; 57 + } 58 + } catch (raw: unknown) { 59 + // Guard against non-CreateAccountError shapes (e.g. JS runtime errors). 60 + if ( 61 + typeof raw === 'object' && 62 + raw !== null && 63 + 'code' in raw && 64 + typeof (raw as CreateAccountError).code === 'string' 65 + ) { 66 + handleError(raw as CreateAccountError); 67 + } else { 68 + errors.handle = "Couldn't reach the server. Check your connection."; 69 + step = 'handle'; 70 + } 71 + } 72 + } 73 + 74 + function handleError(err: CreateAccountError) { 75 + switch (err.code) { 76 + case 'EXPIRED_CODE': 77 + errors.claimCode = 'This claim code has expired. Please request a new one.'; 78 + step = 'claim_code'; 79 + break; 80 + case 'REDEEMED_CODE': 81 + errors.claimCode = 'This claim code has already been used.'; 82 + step = 'claim_code'; 83 + break; 84 + case 'EMAIL_TAKEN': 85 + errors.email = 'An account with that email already exists.'; 86 + step = 'email'; 87 + break; 88 + case 'HANDLE_TAKEN': 89 + errors.handle = 'That handle is taken. Please choose another.'; 90 + step = 'handle'; 91 + break; 92 + case 'NETWORK_ERROR': 93 + case 'UNKNOWN': 94 + default: 95 + errors.handle = "Couldn't reach the server. Check your connection."; 96 + step = 'handle'; 97 + break; 98 + } 9 99 } 10 100 </script> 11 101 12 - <main> 13 - <h1>Identity Wallet</h1> 14 - <div class="greet-form"> 15 - <input 16 - type="text" 17 - bind:value={name} 18 - placeholder="Enter a name" 102 + <div class="app"> 103 + {#if step === 'welcome'} 104 + <WelcomeScreen onstart={() => goTo('claim_code')} /> 105 + {:else if step === 'claim_code'} 106 + <ClaimCodeScreen 107 + bind:value={form.claimCode} 108 + error={errors.claimCode} 109 + onnext={() => goTo('email')} 19 110 /> 20 - <button onclick={handleGreet}>Greet</button> 21 - </div> 22 - {#if greetMsg} 23 - <p class="greeting">{greetMsg}</p> 111 + {:else if step === 'email'} 112 + <EmailScreen 113 + bind:value={form.email} 114 + error={errors.email} 115 + onnext={() => goTo('handle')} 116 + /> 117 + {:else if step === 'handle'} 118 + <HandleScreen 119 + bind:value={form.handle} 120 + error={errors.handle} 121 + onnext={submitAccount} 122 + /> 123 + {:else if step === 'loading'} 124 + <LoadingScreen statusText="Creating your account…" /> 125 + {:else if step === 'did_ceremony'} 126 + <div class="placeholder"> 127 + <h2>Account Created!</h2> 128 + <p>DID ceremony coming soon…</p> 129 + </div> 24 130 {/if} 25 - </main> 131 + </div> 26 132 27 133 <style> 28 - main { 134 + .app { 135 + height: 100vh; 29 136 display: flex; 30 137 flex-direction: column; 31 - align-items: center; 32 - justify-content: center; 33 - min-height: 100vh; 34 - padding: 1rem; 35 - font-family: system-ui, sans-serif; 36 - box-sizing: border-box; 37 138 } 38 139 39 - .greet-form { 140 + .placeholder { 40 141 display: flex; 41 142 flex-direction: column; 42 - gap: 0.5rem; 43 - width: 100%; 44 - max-width: 280px; 45 - margin-top: 1rem; 46 - } 47 - 48 - input, 49 - button { 50 - padding: 0.5rem; 51 - font-size: 1rem; 52 - border-radius: 4px; 53 - border: 1px solid #ccc; 54 - box-sizing: border-box; 55 - width: 100%; 56 - } 57 - 58 - button { 59 - cursor: pointer; 60 - background: #007aff; 61 - color: white; 62 - border-color: #007aff; 63 - } 64 - 65 - .greeting { 66 - margin-top: 1rem; 67 - font-size: 1.25rem; 143 + align-items: center; 144 + justify-content: center; 145 + height: 100%; 146 + gap: 1rem; 147 + text-align: center; 148 + padding: 2rem; 68 149 } 69 150 </style>