+11
-5
packages/danaus/src/web/account/forms.ts
+11
-5
packages/danaus/src/web/account/forms.ts
···
1
-
import { signOperation, type UnsignedOperation } from '@atcute/did-plc';
1
+
import { PlcClientError, signOperation, type UnsignedOperation } from '@atcute/did-plc';
2
2
import type { Did, Handle } from '@atcute/lexicons';
3
3
import { isHandle } from '@atcute/lexicons/syntax';
4
4
import { XRPCError } from '@atcute/xrpc-server';
···
193
193
194
194
// update PLC document for did:plc accounts
195
195
if (did.startsWith('did:plc:')) {
196
-
await updatePlcHandle(ctx, did as Did<'plc'>, handle);
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
+
}
197
205
}
198
206
199
207
// update local database and emit identity event
···
271
279
services: state.services,
272
280
};
273
281
274
-
// sign with PDS rotation key
282
+
// sign with PDS rotation key and submit to PLC directory
275
283
const signedOp = await signOperation(unsignedOp, config.secrets.plcRotationKey);
276
-
277
-
// submit to PLC directory
278
284
await plcClient.submitOperation(did, signedOp);
279
285
}
+219
-98
packages/danaus/src/web/account/index.tsx
+219
-98
packages/danaus/src/web/account/index.tsx
···
12
12
import AsideItem from '../admin/components/aside-item.tsx';
13
13
import { IdProvider } from '../components/id.tsx';
14
14
import { registerForms } from '../forms/index.ts';
15
+
import AtOutlined from '../icons/central/at-outlined.tsx';
15
16
import DotGrid1x3HorizontalOutlined from '../icons/central/dot-grid-1x3-horizontal-outlined.tsx';
16
17
import Key2Outlined from '../icons/central/key-2-outlined.tsx';
17
18
import PasskeysOutlined from '../icons/central/passkeys-outlined.tsx';
···
21
22
import PlusLargeOutlined from '../icons/central/plus-large-outlined.tsx';
22
23
import ShieldOutlined from '../icons/central/shield-outlined.tsx';
23
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';
24
29
import Button from '../primitives/button.tsx';
25
30
import DialogActions from '../primitives/dialog-actions.tsx';
26
31
import DialogBody from '../primitives/dialog-body.tsx';
···
151
156
: 'custom';
152
157
const currentLocalPart = isServiceHandle ? currentHandle.slice(0, -currentDomain.length) : currentHandle;
153
158
159
+
const updateHandleError = updateHandleForm.fields.allIssues().at(0);
160
+
const refreshHandleError = refreshHandleForm.fields.allIssues().at(0);
161
+
154
162
return c.render(
155
163
<AccountLayout>
156
164
<title>My account - Danaus</title>
···
160
168
<h3 class="text-base-400 font-medium">Account overview</h3>
161
169
</div>
162
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
+
163
183
<div class="flex flex-col gap-8">
164
184
<div class="flex flex-col gap-2">
165
185
<h4 class="text-base-300 font-medium text-neutral-foreground-2">Your identity</h4>
···
180
200
181
201
<MenuPopover>
182
202
<MenuList>
183
-
<Dialog>
184
-
<DialogTrigger>
185
-
<MenuItem>Change handle</MenuItem>
186
-
</DialogTrigger>
187
-
188
-
<DialogSurface>
189
-
<DialogBody>
190
-
<DialogTitle>Change handle</DialogTitle>
203
+
<MenuItem command="show-modal" commandfor="change-service-handle-dialog">
204
+
Change handle
205
+
</MenuItem>
191
206
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>
207
+
<MenuItem command="show-modal" commandfor="refresh-handle-dialog">
208
+
Request refresh
209
+
</MenuItem>
282
210
</MenuList>
283
211
</MenuPopover>
284
212
</Menu>
···
320
248
</div>
321
249
</div>
322
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>
323
444
</AccountLayout>,
324
445
);
325
446
});
+18
packages/danaus/src/web/icons/central/at-outlined.tsx
+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
+47
-17
packages/danaus/src/web/styles/main.out.css
···
322
322
.-mx-1\.25 {
323
323
margin-inline: calc(var(--spacing) * -1.25);
324
324
}
325
+
.-mx-3 {
326
+
margin-inline: calc(var(--spacing) * -3);
327
+
}
325
328
.-my-0\.5 {
326
329
margin-block: calc(var(--spacing) * -0.5);
327
330
}
···
416
419
.min-h-9 {
417
420
min-height: calc(var(--spacing) * 9);
418
421
}
422
+
.min-h-11 {
423
+
min-height: calc(var(--spacing) * 11);
424
+
}
419
425
.min-h-dvh {
420
426
min-height: 100dvh;
421
427
}
···
479
485
.flex-1 {
480
486
flex: 1;
481
487
}
488
+
.shrink {
489
+
flex-shrink: 1;
490
+
}
482
491
.shrink-0 {
483
492
flex-shrink: 0;
484
493
}
485
494
.grow {
486
495
flex-grow: 1;
496
+
}
497
+
.basis-0 {
498
+
flex-basis: calc(var(--spacing) * 0);
487
499
}
488
500
.cursor-pointer {
489
501
cursor: pointer;
502
+
}
503
+
.list-none {
504
+
list-style-type: none;
490
505
}
491
506
.appearance-none {
492
507
appearance: none;
···
503
518
.flex-col {
504
519
flex-direction: column;
505
520
}
521
+
.flex-row-reverse {
522
+
flex-direction: row-reverse;
523
+
}
506
524
.flex-nowrap {
507
525
flex-wrap: nowrap;
508
526
}
···
610
628
.border {
611
629
border-style: var(--tw-border-style);
612
630
border-width: 1px;
631
+
}
632
+
.border-0 {
633
+
border-style: var(--tw-border-style);
634
+
border-width: 0px;
613
635
}
614
636
.border-b {
615
637
border-bottom-style: var(--tw-border-style);
···
754
776
.pb-2 {
755
777
padding-bottom: calc(var(--spacing) * 2);
756
778
}
779
+
.pb-3 {
780
+
padding-bottom: calc(var(--spacing) * 3);
781
+
}
757
782
.pl-1 {
758
783
padding-left: calc(var(--spacing) * 1);
759
784
}
···
807
832
.font-semibold {
808
833
--tw-font-weight: var(--font-weight-semibold);
809
834
font-weight: var(--font-weight-semibold);
835
+
}
836
+
.wrap-anywhere {
837
+
overflow-wrap: anywhere;
810
838
}
811
839
.wrap-break-word {
812
840
overflow-wrap: break-word;
···
880
908
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
881
909
transition-duration: var(--tw-duration, var(--default-transition-duration));
882
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
+
}
883
916
.duration-100 {
884
917
--tw-duration: 100ms;
885
918
transition-duration: 100ms;
···
891
924
.outline-none {
892
925
--tw-outline-style: none;
893
926
outline-style: none;
927
+
}
928
+
.select-all {
929
+
-webkit-user-select: all;
930
+
user-select: all;
894
931
}
895
932
.select-none {
896
933
-webkit-user-select: none;
···
913
950
}
914
951
.try-flip-y {
915
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
+
}
916
958
}
917
959
.group-hover\/checkbox\:border-compound-brand-background-hover {
918
960
&:is(:where(.group\/checkbox):hover *) {
···
1107
1149
display: flex;
1108
1150
}
1109
1151
}
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
1152
.hover\:border-neutral-stroke-1-hover {
1121
1153
&:hover {
1122
1154
@media (hover: hover) {
···
1375
1407
padding-top: calc(var(--spacing) * 24);
1376
1408
}
1377
1409
}
1378
-
.open\:sm\:items-center {
1379
-
&:is([open], :popover-open, :open) {
1380
-
@media (width >= 40rem) {
1381
-
align-items: center;
1382
-
}
1383
-
}
1384
-
}
1385
1410
.lg\:grid {
1386
1411
@media (width >= 64rem) {
1387
1412
display: grid;
···
1420
1445
.\@sm\/dialog-body\:justify-start {
1421
1446
@container dialog-body (width >= 24rem) {
1422
1447
justify-content: flex-start;
1448
+
}
1449
+
}
1450
+
.\[\&\:\:-webkit-details-marker\]\:hidden {
1451
+
&::-webkit-details-marker {
1452
+
display: none;
1423
1453
}
1424
1454
}
1425
1455
}