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 DID ceremony UI screens and IPC wrapper

authored by

Malpercio and committed by
Tangled
fcf3bcf4 8d9c8fdc

+242 -4
+100
apps/identity-wallet/src/lib/components/onboarding/DIDCeremonyScreen.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import LoadingScreen from './LoadingScreen.svelte'; 4 + import { performDIDCeremony, type DIDCeremonyError } from '$lib/ipc'; 5 + 6 + let { 7 + handle, 8 + onsuccess, 9 + }: { 10 + handle: string; 11 + onsuccess: (did: string) => void; 12 + } = $props(); 13 + 14 + let loading = $state(true); 15 + let error = $state<DIDCeremonyError | null>(null); 16 + 17 + async function runCeremony() { 18 + loading = true; 19 + error = null; 20 + try { 21 + const result = await performDIDCeremony(handle); 22 + loading = false; 23 + onsuccess(result.did); 24 + } catch (raw: unknown) { 25 + loading = false; 26 + if ( 27 + typeof raw === 'object' && 28 + raw !== null && 29 + 'code' in raw && 30 + typeof (raw as DIDCeremonyError).code === 'string' 31 + ) { 32 + error = raw as DIDCeremonyError; 33 + } else { 34 + error = { code: 'NETWORK_ERROR', message: 'An unexpected error occurred.' }; 35 + } 36 + } 37 + } 38 + 39 + function errorMessage(err: DIDCeremonyError): string { 40 + switch (err.code) { 41 + case 'NO_RELAY_SIGNING_KEY': 42 + return "The relay hasn't been configured yet. Please try again later."; 43 + case 'RELAY_KEY_FETCH_FAILED': 44 + case 'NETWORK_ERROR': 45 + return "Couldn't reach the server. Check your connection."; 46 + case 'SIGNING_FAILED': 47 + return 'Device signing failed. Please try again.'; 48 + case 'DID_CREATION_FAILED': 49 + return "Couldn't create your identity. Please try again."; 50 + case 'KEYCHAIN_ERROR': 51 + return "Couldn't save to your device. Please try again."; 52 + case 'KEY_NOT_FOUND': 53 + default: 54 + return 'Something went wrong. Please try again.'; 55 + } 56 + } 57 + 58 + onMount(() => runCeremony()); 59 + </script> 60 + 61 + {#if loading} 62 + <LoadingScreen statusText="Setting up your identity…" /> 63 + {:else if error} 64 + <div class="screen"> 65 + <p class="error-text">{errorMessage(error)}</p> 66 + <button class="retry" onclick={() => runCeremony()}>Retry</button> 67 + </div> 68 + {/if} 69 + 70 + <style> 71 + .screen { 72 + display: flex; 73 + flex-direction: column; 74 + align-items: center; 75 + justify-content: center; 76 + height: 100%; 77 + padding: 2rem; 78 + gap: 1.5rem; 79 + text-align: center; 80 + } 81 + 82 + .error-text { 83 + font-size: 1rem; 84 + color: #ef4444; 85 + margin: 0; 86 + } 87 + 88 + .retry { 89 + width: 100%; 90 + max-width: 320px; 91 + padding: 1rem; 92 + background: #007aff; 93 + color: #fff; 94 + border: none; 95 + border-radius: 12px; 96 + font-size: 1.1rem; 97 + font-weight: 600; 98 + cursor: pointer; 99 + } 100 + </style>
+85
apps/identity-wallet/src/lib/components/onboarding/DIDSuccessScreen.svelte
··· 1 + <script lang="ts"> 2 + let { 3 + did, 4 + oncontinue, 5 + }: { 6 + did: string; 7 + oncontinue: () => void; 8 + } = $props(); 9 + 10 + // Truncate the DID suffix for display on a narrow mobile screen. 11 + // "did:plc:abcdefghijklmnopqrstuvwx" → "did:plc:abcde…uvwx" 12 + let displayDid = $derived( 13 + did.startsWith('did:plc:') && did.length > 20 14 + ? `did:plc:${did.slice(8, 13)}…${did.slice(-4)}` 15 + : did 16 + ); 17 + </script> 18 + 19 + <div class="screen"> 20 + <div class="success-icon" aria-hidden="true">✓</div> 21 + <h2>Identity Created!</h2> 22 + <p class="label">Your decentralized identifier</p> 23 + <code class="did">{displayDid}</code> 24 + <button class="cta" onclick={oncontinue}>Continue</button> 25 + </div> 26 + 27 + <style> 28 + .screen { 29 + display: flex; 30 + flex-direction: column; 31 + align-items: center; 32 + justify-content: center; 33 + height: 100%; 34 + padding: 2rem; 35 + gap: 1.25rem; 36 + text-align: center; 37 + } 38 + 39 + .success-icon { 40 + width: 64px; 41 + height: 64px; 42 + background: #007aff; 43 + color: #fff; 44 + border-radius: 50%; 45 + display: flex; 46 + align-items: center; 47 + justify-content: center; 48 + font-size: 2rem; 49 + font-weight: 700; 50 + } 51 + 52 + h2 { 53 + font-size: 1.5rem; 54 + font-weight: 700; 55 + margin: 0; 56 + } 57 + 58 + .label { 59 + font-size: 0.875rem; 60 + color: #6b7280; 61 + margin: 0; 62 + } 63 + 64 + .did { 65 + font-family: monospace; 66 + font-size: 0.9rem; 67 + background: #f3f4f6; 68 + padding: 0.5rem 1rem; 69 + border-radius: 8px; 70 + word-break: break-all; 71 + } 72 + 73 + .cta { 74 + width: 100%; 75 + max-width: 320px; 76 + padding: 1rem; 77 + background: #007aff; 78 + color: #fff; 79 + border: none; 80 + border-radius: 12px; 81 + font-size: 1.1rem; 82 + font-weight: 600; 83 + cursor: pointer; 84 + } 85 + </style>
+39
apps/identity-wallet/src/lib/ipc.ts
··· 104 104 (invoke('sign_with_device_key', { data: Array.from(data) }) as Promise<number[]>).then( 105 105 (bytes) => new Uint8Array(bytes), 106 106 ); 107 + 108 + // ── perform_did_ceremony ───────────────────────────────────────────────────── 109 + 110 + /** 111 + * Successful result from the `perform_did_ceremony` Rust command. 112 + * This is a pure data shape returned on success. 113 + */ 114 + export type DIDCeremonyResult = { 115 + did: string; 116 + }; 117 + 118 + /** 119 + * Error returned by the `perform_did_ceremony` Rust command. 120 + * 121 + * Serialized as `{ code: "NO_RELAY_SIGNING_KEY" }` etc. by the Rust backend. 122 + * The `message` field is present only on the NETWORK_ERROR variant. 123 + * This is a pure data shape used for error handling. 124 + */ 125 + export type DIDCeremonyError = { 126 + code: 127 + | 'KEY_NOT_FOUND' 128 + | 'RELAY_KEY_FETCH_FAILED' 129 + | 'NO_RELAY_SIGNING_KEY' 130 + | 'SIGNING_FAILED' 131 + | 'DID_CREATION_FAILED' 132 + | 'KEYCHAIN_ERROR' 133 + | 'NETWORK_ERROR'; 134 + message?: string; 135 + }; 136 + 137 + /** 138 + * Perform the DID ceremony: fetch relay key, build signed genesis op, post to relay, 139 + * persist DID and upgraded session token in Keychain. 140 + * 141 + * On success, the DID and new session token are stored in Keychain by the Rust backend. 142 + * On failure, the Promise rejects with a `DIDCeremonyError`. 143 + */ 144 + export const performDIDCeremony = (handle: string): Promise<DIDCeremonyResult> => 145 + invoke('perform_did_ceremony', { handle });
+18 -4
apps/identity-wallet/src/routes/+page.svelte
··· 4 4 import EmailScreen from '$lib/components/onboarding/EmailScreen.svelte'; 5 5 import HandleScreen from '$lib/components/onboarding/HandleScreen.svelte'; 6 6 import LoadingScreen from '$lib/components/onboarding/LoadingScreen.svelte'; 7 + import DIDCeremonyScreen from '$lib/components/onboarding/DIDCeremonyScreen.svelte'; 8 + import DIDSuccessScreen from '$lib/components/onboarding/DIDSuccessScreen.svelte'; 7 9 import { createAccount, type CreateAccountError } from '$lib/ipc'; 8 10 9 11 // ── Onboarding step type ───────────────────────────────────────────────── ··· 22 24 | 'email' 23 25 | 'handle' 24 26 | 'loading' 25 - | 'did_ceremony'; 27 + | 'did_ceremony' 28 + | 'did_success' 29 + | 'shamir_backup'; 26 30 27 31 // ── State ──────────────────────────────────────────────────────────────── 28 32 29 33 let step = $state<OnboardingStep>('welcome'); 30 - let form = $state({ claimCode: '', email: '', handle: '' }); 34 + let form = $state({ claimCode: '', email: '', handle: '', did: '' }); 31 35 32 36 /** 33 37 * Per-field error messages displayed by each screen. ··· 135 139 {:else if step === 'loading'} 136 140 <LoadingScreen statusText="Creating your account…" /> 137 141 {:else if step === 'did_ceremony'} 142 + <DIDCeremonyScreen 143 + handle={form.handle} 144 + onsuccess={(did) => { form.did = did; step = 'did_success'; }} 145 + /> 146 + {:else if step === 'did_success'} 147 + <DIDSuccessScreen 148 + did={form.did} 149 + oncontinue={() => { step = 'shamir_backup'; }} 150 + /> 151 + {:else if step === 'shamir_backup'} 138 152 <div class="placeholder"> 139 - <h2>Account Created!</h2> 140 - <p>DID ceremony coming soon…</p> 153 + <h2>Backup</h2> 154 + <p>Shamir backup coming soon…</p> 141 155 </div> 142 156 {/if} 143 157 </div>