work-in-progress atproto PDS
typescript atproto pds atcute

refactor: improve on handle change dialog

mary.my.id 81105dc5 5481d484

verified
Changed files
+295 -120
packages
danaus
src
web
account
icons
central
styles
+11 -5
packages/danaus/src/web/account/forms.ts
··· 1 - import { signOperation, type UnsignedOperation } from '@atcute/did-plc'; 2 import type { Did, Handle } from '@atcute/lexicons'; 3 import { isHandle } from '@atcute/lexicons/syntax'; 4 import { XRPCError } from '@atcute/xrpc-server'; ··· 193 194 // update PLC document for did:plc accounts 195 if (did.startsWith('did:plc:')) { 196 - await updatePlcHandle(ctx, did as Did<'plc'>, handle); 197 } 198 199 // update local database and emit identity event ··· 271 services: state.services, 272 }; 273 274 - // sign with PDS rotation key 275 const signedOp = await signOperation(unsignedOp, config.secrets.plcRotationKey); 276 - 277 - // submit to PLC directory 278 await plcClient.submitOperation(did, signedOp); 279 }
··· 1 + import { PlcClientError, signOperation, type UnsignedOperation } from '@atcute/did-plc'; 2 import type { Did, Handle } from '@atcute/lexicons'; 3 import { isHandle } from '@atcute/lexicons/syntax'; 4 import { XRPCError } from '@atcute/xrpc-server'; ··· 193 194 // update PLC document for did:plc accounts 195 if (did.startsWith('did:plc:')) { 196 + try { 197 + await updatePlcHandle(ctx, did as Did<'plc'>, handle); 198 + } catch (err) { 199 + if (err instanceof PlcClientError) { 200 + invalid(`Unable to update DID document, please try again later`); 201 + } 202 + 203 + throw err; 204 + } 205 } 206 207 // update local database and emit identity event ··· 279 services: state.services, 280 }; 281 282 + // sign with PDS rotation key and submit to PLC directory 283 const signedOp = await signOperation(unsignedOp, config.secrets.plcRotationKey); 284 await plcClient.submitOperation(did, signedOp); 285 }
+219 -98
packages/danaus/src/web/account/index.tsx
··· 12 import AsideItem from '../admin/components/aside-item.tsx'; 13 import { IdProvider } from '../components/id.tsx'; 14 import { registerForms } from '../forms/index.ts'; 15 import DotGrid1x3HorizontalOutlined from '../icons/central/dot-grid-1x3-horizontal-outlined.tsx'; 16 import Key2Outlined from '../icons/central/key-2-outlined.tsx'; 17 import PasskeysOutlined from '../icons/central/passkeys-outlined.tsx'; ··· 21 import PlusLargeOutlined from '../icons/central/plus-large-outlined.tsx'; 22 import ShieldOutlined from '../icons/central/shield-outlined.tsx'; 23 import UsbOutlined from '../icons/central/usb-outlined.tsx'; 24 import Button from '../primitives/button.tsx'; 25 import DialogActions from '../primitives/dialog-actions.tsx'; 26 import DialogBody from '../primitives/dialog-body.tsx'; ··· 151 : 'custom'; 152 const currentLocalPart = isServiceHandle ? currentHandle.slice(0, -currentDomain.length) : currentHandle; 153 154 return c.render( 155 <AccountLayout> 156 <title>My account - Danaus</title> ··· 160 <h3 class="text-base-400 font-medium">Account overview</h3> 161 </div> 162 163 <div class="flex flex-col gap-8"> 164 <div class="flex flex-col gap-2"> 165 <h4 class="text-base-300 font-medium text-neutral-foreground-2">Your identity</h4> ··· 180 181 <MenuPopover> 182 <MenuList> 183 - <Dialog> 184 - <DialogTrigger> 185 - <MenuItem>Change handle</MenuItem> 186 - </DialogTrigger> 187 - 188 - <DialogSurface> 189 - <DialogBody> 190 - <DialogTitle>Change handle</DialogTitle> 191 192 - <form {...updateHandleForm} class="contents"> 193 - <DialogContent class="flex flex-col gap-4"> 194 - <p class="text-base-300 text-neutral-foreground-3"> 195 - Your handle is your unique identity on the AT Protocol network. 196 - </p> 197 - 198 - <Field 199 - label="Domain" 200 - validationMessageText={ 201 - updateHandleForm.fields.domain.issues()[0]?.message 202 - } 203 - > 204 - <Select 205 - {...updateHandleForm.fields.domain.as('select')} 206 - value={updateHandleForm.fields.domain.value() || currentDomain} 207 - options={[ 208 - ...ctx.config.identity.serviceHandleDomains.map((d) => ({ 209 - value: d, 210 - label: d, 211 - })), 212 - { value: 'custom', label: 'I have my own domain' }, 213 - ]} 214 - /> 215 - </Field> 216 - 217 - <Field 218 - label="Handle" 219 - required 220 - validationMessageText={ 221 - updateHandleForm.fields.handle.issues()[0]?.message 222 - } 223 - > 224 - <Input 225 - {...updateHandleForm.fields.handle.as('text')} 226 - value={updateHandleForm.fields.handle.value() || currentLocalPart} 227 - placeholder="alice" 228 - required 229 - /> 230 - </Field> 231 - 232 - <p class="text-base-200 text-neutral-foreground-3"> 233 - Custom domains must have a DNS TXT record or .well-known file pointing to 234 - your DID. 235 - </p> 236 - </DialogContent> 237 - 238 - <DialogActions> 239 - <DialogClose> 240 - <Button>Cancel</Button> 241 - </DialogClose> 242 - 243 - <Button type="submit" variant="primary"> 244 - Save 245 - </Button> 246 - </DialogActions> 247 - </form> 248 - </DialogBody> 249 - </DialogSurface> 250 - </Dialog> 251 - 252 - <Dialog> 253 - <DialogTrigger> 254 - <MenuItem>Request refresh</MenuItem> 255 - </DialogTrigger> 256 - 257 - <DialogSurface> 258 - <DialogBody> 259 - <DialogTitle>Request handle refresh</DialogTitle> 260 - 261 - <form {...refreshHandleForm} class="contents"> 262 - <DialogContent> 263 - <p class="text-base-300"> 264 - This will notify the network to re-verify your handle. Use this if apps 265 - are marking your handle as invalid despite being set up correctly. 266 - </p> 267 - </DialogContent> 268 - 269 - <DialogActions> 270 - <DialogClose> 271 - <Button>Cancel</Button> 272 - </DialogClose> 273 - 274 - <Button type="submit" variant="primary"> 275 - Refresh 276 - </Button> 277 - </DialogActions> 278 - </form> 279 - </DialogBody> 280 - </DialogSurface> 281 - </Dialog> 282 </MenuList> 283 </MenuPopover> 284 </Menu> ··· 320 </div> 321 </div> 322 </div> 323 </AccountLayout>, 324 ); 325 });
··· 12 import AsideItem from '../admin/components/aside-item.tsx'; 13 import { IdProvider } from '../components/id.tsx'; 14 import { registerForms } from '../forms/index.ts'; 15 + import AtOutlined from '../icons/central/at-outlined.tsx'; 16 import DotGrid1x3HorizontalOutlined from '../icons/central/dot-grid-1x3-horizontal-outlined.tsx'; 17 import Key2Outlined from '../icons/central/key-2-outlined.tsx'; 18 import PasskeysOutlined from '../icons/central/passkeys-outlined.tsx'; ··· 22 import PlusLargeOutlined from '../icons/central/plus-large-outlined.tsx'; 23 import ShieldOutlined from '../icons/central/shield-outlined.tsx'; 24 import UsbOutlined from '../icons/central/usb-outlined.tsx'; 25 + import AccordionHeader from '../primitives/accordion-header.tsx'; 26 + import AccordionItem from '../primitives/accordion-item.tsx'; 27 + import AccordionPanel from '../primitives/accordion-panel.tsx'; 28 + import Accordion from '../primitives/accordion.tsx'; 29 import Button from '../primitives/button.tsx'; 30 import DialogActions from '../primitives/dialog-actions.tsx'; 31 import DialogBody from '../primitives/dialog-body.tsx'; ··· 156 : 'custom'; 157 const currentLocalPart = isServiceHandle ? currentHandle.slice(0, -currentDomain.length) : currentHandle; 158 159 + const updateHandleError = updateHandleForm.fields.allIssues().at(0); 160 + const refreshHandleError = refreshHandleForm.fields.allIssues().at(0); 161 + 162 return c.render( 163 <AccountLayout> 164 <title>My account - Danaus</title> ··· 168 <h3 class="text-base-400 font-medium">Account overview</h3> 169 </div> 170 171 + {updateHandleError && ( 172 + <MessageBar intent="error" layout="singleline"> 173 + <MessageBarBody>{updateHandleError.message}</MessageBarBody> 174 + </MessageBar> 175 + )} 176 + 177 + {refreshHandleError && ( 178 + <MessageBar intent="error" layout="singleline"> 179 + <MessageBarBody>{refreshHandleError.message}</MessageBarBody> 180 + </MessageBar> 181 + )} 182 + 183 <div class="flex flex-col gap-8"> 184 <div class="flex flex-col gap-2"> 185 <h4 class="text-base-300 font-medium text-neutral-foreground-2">Your identity</h4> ··· 200 201 <MenuPopover> 202 <MenuList> 203 + <MenuItem command="show-modal" commandfor="change-service-handle-dialog"> 204 + Change handle 205 + </MenuItem> 206 207 + <MenuItem command="show-modal" commandfor="refresh-handle-dialog"> 208 + Request refresh 209 + </MenuItem> 210 </MenuList> 211 </MenuPopover> 212 </Menu> ··· 248 </div> 249 </div> 250 </div> 251 + 252 + <Dialog id="change-service-handle-dialog"> 253 + <DialogSurface> 254 + <DialogBody> 255 + <DialogTitle>Change handle</DialogTitle> 256 + 257 + <form {...updateHandleForm} class="contents"> 258 + <DialogContent class="flex flex-col gap-4"> 259 + <p class="text-base-300 text-neutral-foreground-3"> 260 + Your handle is your unique identity on the AT Protocol network. 261 + </p> 262 + 263 + <Field label="Handle" required> 264 + <div class="flex gap-2"> 265 + <Input 266 + {...updateHandleForm.fields.handle.as('text')} 267 + value={updateHandleForm.fields.handle.value() || currentLocalPart} 268 + placeholder="alice" 269 + contentBefore={<AtOutlined size={16} />} 270 + class="grow" 271 + /> 272 + 273 + <Select 274 + {...updateHandleForm.fields.domain.as('select')} 275 + value={updateHandleForm.fields.domain.value() || currentDomain} 276 + options={ctx.config.identity.serviceHandleDomains.map((d) => ({ 277 + value: d, 278 + label: d, 279 + }))} 280 + /> 281 + </div> 282 + </Field> 283 + 284 + <div></div> 285 + </DialogContent> 286 + 287 + <DialogActions> 288 + <Button command="show-modal" commandfor="change-custom-handle-dialog"> 289 + Use my own domain 290 + </Button> 291 + 292 + <div class="grow"></div> 293 + 294 + <DialogClose> 295 + <Button>Cancel</Button> 296 + </DialogClose> 297 + 298 + <Button type="submit" variant="primary"> 299 + Change 300 + </Button> 301 + </DialogActions> 302 + </form> 303 + </DialogBody> 304 + </DialogSurface> 305 + </Dialog> 306 + 307 + <Dialog id="refresh-handle-dialog"> 308 + <DialogSurface> 309 + <DialogBody> 310 + <DialogTitle>Request handle refresh</DialogTitle> 311 + 312 + <form {...refreshHandleForm} class="contents"> 313 + <DialogContent> 314 + <p class="text-base-300"> 315 + This will notify the network to re-verify your handle. Use this if apps are marking your 316 + handle as invalid despite being set up correctly. 317 + </p> 318 + </DialogContent> 319 + 320 + <DialogActions> 321 + <DialogClose> 322 + <Button>Cancel</Button> 323 + </DialogClose> 324 + 325 + <Button type="submit" variant="primary"> 326 + Refresh 327 + </Button> 328 + </DialogActions> 329 + </form> 330 + </DialogBody> 331 + </DialogSurface> 332 + </Dialog> 333 + 334 + <Dialog id="change-custom-handle-dialog"> 335 + <DialogSurface> 336 + <DialogBody> 337 + <DialogTitle>Change handle</DialogTitle> 338 + 339 + <form {...updateHandleForm} class="contents"> 340 + <DialogContent class="flex flex-col gap-4"> 341 + <p class="text-base-300 text-neutral-foreground-3"> 342 + Your handle is your unique identity on the AT Protocol network. 343 + </p> 344 + 345 + <Field label="Handle" required> 346 + <Input 347 + {...updateHandleForm.fields.handle.as('text')} 348 + placeholder="alice.com" 349 + contentBefore={<AtOutlined size={16} />} 350 + /> 351 + </Field> 352 + 353 + <input {...updateHandleForm.fields.domain.as('hidden', 'custom')} /> 354 + 355 + <Accordion class="flex flex-col gap-2"> 356 + <AccordionItem name="handle-method" open> 357 + <AccordionHeader>DNS record</AccordionHeader> 358 + <AccordionPanel> 359 + <div class="flex flex-col gap-3"> 360 + <p class="text-base-300 text-neutral-foreground-3"> 361 + Add the following DNS record to your domain: 362 + </p> 363 + 364 + <div class="flex flex-col gap-2 rounded-md bg-neutral-background-3 p-3"> 365 + <div class="flex flex-col gap-0.5"> 366 + <span class="text-base-200 text-neutral-foreground-3">Host</span> 367 + <input 368 + type="text" 369 + readonly 370 + value="_atproto.<your-domain>" 371 + class="font-mono text-base-300 outline-none" 372 + /> 373 + </div> 374 + <div class="flex flex-col gap-0.5"> 375 + <span class="text-base-200 text-neutral-foreground-3">Type</span> 376 + <input 377 + type="text" 378 + readonly 379 + value="TXT" 380 + class="font-mono text-base-300 outline-none" 381 + /> 382 + </div> 383 + <div class="flex flex-col gap-0.5"> 384 + <span class="text-base-200 text-neutral-foreground-3">Value</span> 385 + <input 386 + type="text" 387 + readonly 388 + value={`did=${session.did}`} 389 + class="font-mono text-base-300 outline-none" 390 + /> 391 + </div> 392 + </div> 393 + </div> 394 + </AccordionPanel> 395 + </AccordionItem> 396 + 397 + <AccordionItem name="handle-method"> 398 + <AccordionHeader>HTTP well-known entry</AccordionHeader> 399 + <AccordionPanel> 400 + <div class="flex flex-col gap-3"> 401 + <p class="text-base-300 text-neutral-foreground-3"> 402 + Upload a text file to the following URL: 403 + </p> 404 + 405 + <div class="flex flex-col gap-2 rounded-md bg-neutral-background-3 p-3"> 406 + <div class="flex flex-col gap-0.5"> 407 + <span class="text-base-200 text-neutral-foreground-3">URL</span> 408 + <input 409 + type="text" 410 + readonly 411 + value="https://<your-domain>/.well-known/atproto-did" 412 + class="font-mono text-base-300 outline-none" 413 + /> 414 + </div> 415 + <div class="flex flex-col gap-0.5"> 416 + <span class="text-base-200 text-neutral-foreground-3">Contents</span> 417 + <input 418 + type="text" 419 + readonly 420 + value={session.did} 421 + class="font-mono text-base-300 outline-none" 422 + /> 423 + </div> 424 + </div> 425 + </div> 426 + </AccordionPanel> 427 + </AccordionItem> 428 + </Accordion> 429 + </DialogContent> 430 + 431 + <DialogActions> 432 + <DialogClose> 433 + <Button>Cancel</Button> 434 + </DialogClose> 435 + 436 + <Button type="submit" variant="primary"> 437 + Change 438 + </Button> 439 + </DialogActions> 440 + </form> 441 + </DialogBody> 442 + </DialogSurface> 443 + </Dialog> 444 </AccountLayout>, 445 ); 446 });
+18
packages/danaus/src/web/icons/central/at-outlined.tsx
···
··· 1 + import type { IconProps } from './_types.ts'; 2 + 3 + const ArrowInboxOutlined = (props: IconProps) => { 4 + const { size = 24, class: className } = props; 5 + 6 + return ( 7 + <svg viewBox="0 0 24 24" width={size} height={size} fill="none" class={className}> 8 + <path 9 + d="M16.7368 19.6541C15.361 20.5073 13.738 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 13.9262 20.0428 15.9154 17.8101 15.7125C15.9733 15.5455 14.6512 13.8737 14.9121 12.0479L15.4274 8.5M14.8581 12.4675C14.559 14.596 12.8066 16.1093 10.9442 15.8476C9.08175 15.5858 7.81444 13.6481 8.11358 11.5196C8.41272 9.39109 10.165 7.87778 12.0275 8.13953C13.8899 8.40128 15.1573 10.339 14.8581 12.4675Z" 10 + stroke="currentColor" 11 + stroke-width="2" 12 + stroke-linecap="round" 13 + /> 14 + </svg> 15 + ); 16 + }; 17 + 18 + export default ArrowInboxOutlined;
+47 -17
packages/danaus/src/web/styles/main.out.css
··· 322 .-mx-1\.25 { 323 margin-inline: calc(var(--spacing) * -1.25); 324 } 325 .-my-0\.5 { 326 margin-block: calc(var(--spacing) * -0.5); 327 } ··· 416 .min-h-9 { 417 min-height: calc(var(--spacing) * 9); 418 } 419 .min-h-dvh { 420 min-height: 100dvh; 421 } ··· 479 .flex-1 { 480 flex: 1; 481 } 482 .shrink-0 { 483 flex-shrink: 0; 484 } 485 .grow { 486 flex-grow: 1; 487 } 488 .cursor-pointer { 489 cursor: pointer; 490 } 491 .appearance-none { 492 appearance: none; ··· 503 .flex-col { 504 flex-direction: column; 505 } 506 .flex-nowrap { 507 flex-wrap: nowrap; 508 } ··· 610 .border { 611 border-style: var(--tw-border-style); 612 border-width: 1px; 613 } 614 .border-b { 615 border-bottom-style: var(--tw-border-style); ··· 754 .pb-2 { 755 padding-bottom: calc(var(--spacing) * 2); 756 } 757 .pl-1 { 758 padding-left: calc(var(--spacing) * 1); 759 } ··· 807 .font-semibold { 808 --tw-font-weight: var(--font-weight-semibold); 809 font-weight: var(--font-weight-semibold); 810 } 811 .wrap-break-word { 812 overflow-wrap: break-word; ··· 880 transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); 881 transition-duration: var(--tw-duration, var(--default-transition-duration)); 882 } 883 .duration-100 { 884 --tw-duration: 100ms; 885 transition-duration: 100ms; ··· 891 .outline-none { 892 --tw-outline-style: none; 893 outline-style: none; 894 } 895 .select-none { 896 -webkit-user-select: none; ··· 913 } 914 .try-flip-y { 915 position-try-fallbacks: flip-block; 916 } 917 .group-hover\/checkbox\:border-compound-brand-background-hover { 918 &:is(:where(.group\/checkbox):hover *) { ··· 1107 display: flex; 1108 } 1109 } 1110 - .open\:items-end { 1111 - &:is([open], :popover-open, :open) { 1112 - align-items: flex-end; 1113 - } 1114 - } 1115 - .open\:justify-center { 1116 - &:is([open], :popover-open, :open) { 1117 - justify-content: center; 1118 - } 1119 - } 1120 .hover\:border-neutral-stroke-1-hover { 1121 &:hover { 1122 @media (hover: hover) { ··· 1375 padding-top: calc(var(--spacing) * 24); 1376 } 1377 } 1378 - .open\:sm\:items-center { 1379 - &:is([open], :popover-open, :open) { 1380 - @media (width >= 40rem) { 1381 - align-items: center; 1382 - } 1383 - } 1384 - } 1385 .lg\:grid { 1386 @media (width >= 64rem) { 1387 display: grid; ··· 1420 .\@sm\/dialog-body\:justify-start { 1421 @container dialog-body (width >= 24rem) { 1422 justify-content: flex-start; 1423 } 1424 } 1425 }
··· 322 .-mx-1\.25 { 323 margin-inline: calc(var(--spacing) * -1.25); 324 } 325 + .-mx-3 { 326 + margin-inline: calc(var(--spacing) * -3); 327 + } 328 .-my-0\.5 { 329 margin-block: calc(var(--spacing) * -0.5); 330 } ··· 419 .min-h-9 { 420 min-height: calc(var(--spacing) * 9); 421 } 422 + .min-h-11 { 423 + min-height: calc(var(--spacing) * 11); 424 + } 425 .min-h-dvh { 426 min-height: 100dvh; 427 } ··· 485 .flex-1 { 486 flex: 1; 487 } 488 + .shrink { 489 + flex-shrink: 1; 490 + } 491 .shrink-0 { 492 flex-shrink: 0; 493 } 494 .grow { 495 flex-grow: 1; 496 + } 497 + .basis-0 { 498 + flex-basis: calc(var(--spacing) * 0); 499 } 500 .cursor-pointer { 501 cursor: pointer; 502 + } 503 + .list-none { 504 + list-style-type: none; 505 } 506 .appearance-none { 507 appearance: none; ··· 518 .flex-col { 519 flex-direction: column; 520 } 521 + .flex-row-reverse { 522 + flex-direction: row-reverse; 523 + } 524 .flex-nowrap { 525 flex-wrap: nowrap; 526 } ··· 628 .border { 629 border-style: var(--tw-border-style); 630 border-width: 1px; 631 + } 632 + .border-0 { 633 + border-style: var(--tw-border-style); 634 + border-width: 0px; 635 } 636 .border-b { 637 border-bottom-style: var(--tw-border-style); ··· 776 .pb-2 { 777 padding-bottom: calc(var(--spacing) * 2); 778 } 779 + .pb-3 { 780 + padding-bottom: calc(var(--spacing) * 3); 781 + } 782 .pl-1 { 783 padding-left: calc(var(--spacing) * 1); 784 } ··· 832 .font-semibold { 833 --tw-font-weight: var(--font-weight-semibold); 834 font-weight: var(--font-weight-semibold); 835 + } 836 + .wrap-anywhere { 837 + overflow-wrap: anywhere; 838 } 839 .wrap-break-word { 840 overflow-wrap: break-word; ··· 908 transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); 909 transition-duration: var(--tw-duration, var(--default-transition-duration)); 910 } 911 + .transition-transform { 912 + transition-property: transform, translate, scale, rotate; 913 + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); 914 + transition-duration: var(--tw-duration, var(--default-transition-duration)); 915 + } 916 .duration-100 { 917 --tw-duration: 100ms; 918 transition-duration: 100ms; ··· 924 .outline-none { 925 --tw-outline-style: none; 926 outline-style: none; 927 + } 928 + .select-all { 929 + -webkit-user-select: all; 930 + user-select: all; 931 } 932 .select-none { 933 -webkit-user-select: none; ··· 950 } 951 .try-flip-y { 952 position-try-fallbacks: flip-block; 953 + } 954 + .group-open\/accordion-item\:rotate-180 { 955 + &:is(:where(.group\/accordion-item):is([open], :popover-open, :open) *) { 956 + rotate: 180deg; 957 + } 958 } 959 .group-hover\/checkbox\:border-compound-brand-background-hover { 960 &:is(:where(.group\/checkbox):hover *) { ··· 1149 display: flex; 1150 } 1151 } 1152 .hover\:border-neutral-stroke-1-hover { 1153 &:hover { 1154 @media (hover: hover) { ··· 1407 padding-top: calc(var(--spacing) * 24); 1408 } 1409 } 1410 .lg\:grid { 1411 @media (width >= 64rem) { 1412 display: grid; ··· 1445 .\@sm\/dialog-body\:justify-start { 1446 @container dialog-body (width >= 24rem) { 1447 justify-content: flex-start; 1448 + } 1449 + } 1450 + .\[\&\:\:-webkit-details-marker\]\:hidden { 1451 + &::-webkit-details-marker { 1452 + display: none; 1453 } 1454 } 1455 }