@nymphjs/tilmeld-components 1.0.0-beta.80 → 1.0.0-beta.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.0.0-beta.82](https://github.com/sciactive/nymphjs/compare/v1.0.0-beta.81...v1.0.0-beta.82) (2024-12-15)
7
+
8
+ ### Features
9
+
10
+ - migrate to es modules, upgrade all packages, migrate to Svelte 5 ([3f2b9e5](https://github.com/sciactive/nymphjs/commit/3f2b9e517b39934eddce66601d7fc747fbf3f9e6))
11
+
12
+ # [1.0.0-beta.81](https://github.com/sciactive/nymphjs/compare/v1.0.0-beta.80...v1.0.0-beta.81) (2024-09-28)
13
+
14
+ **Note:** Version bump only for package @nymphjs/tilmeld-components
15
+
6
16
  # [1.0.0-beta.80](https://github.com/sciactive/nymphjs/compare/v1.0.0-beta.79...v1.0.0-beta.80) (2024-09-28)
7
17
 
8
18
  **Note:** Version bump only for package @nymphjs/tilmeld-components
package/README.md CHANGED
@@ -16,7 +16,7 @@ You need to have an SMUI theme compiled and installed on your front end app. If
16
16
 
17
17
  # License
18
18
 
19
- Copyright 2021 SciActive Inc
19
+ Copyright 2021-2024 SciActive Inc
20
20
 
21
21
  Licensed under the Apache License, Version 2.0 (the "License");
22
22
  you may not use this file except in compliance with the License.
@@ -0,0 +1,515 @@
1
+ <svelte:options runes />
2
+
3
+ {#if $clientConfig != null && $user != null}
4
+ <Dialog
5
+ {use}
6
+ bind:open
7
+ aria-labelledby="tilmeld-account-title"
8
+ aria-describedby="tilmeld-account-content"
9
+ surface$class="tilmeld-account-dialog-surface"
10
+ {...exclude(restProps, [
11
+ 'username$',
12
+ 'email$',
13
+ 'nameFirst$',
14
+ 'nameMiddle$',
15
+ 'nameLast$',
16
+ 'phone$',
17
+ 'changePasswordLink$',
18
+ 'revokeTokensLink$',
19
+ 'closeButton$',
20
+ 'saveButton$',
21
+ 'changePassword$',
22
+ 'revokeTokens$',
23
+ 'twoFactor$',
24
+ 'progress$',
25
+ ])}
26
+ >
27
+ <!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
28
+ <Title id="tilmeld-account-title">{title}</Title>
29
+ <Content id="tilmeld-account-content">
30
+ {#if !$clientConfig.emailUsernames && $clientConfig.allowUsernameChange}
31
+ <div>
32
+ <Textfield
33
+ bind:value={$user.username}
34
+ label="Username"
35
+ type="text"
36
+ style="width: 100%;"
37
+ helperLine$style="width: 100%;"
38
+ invalid={usernameVerified === false}
39
+ input$autocomplete="username"
40
+ input$autocapitalize="off"
41
+ input$spellcheck="false"
42
+ input$emptyValueUndefined
43
+ {...prefixFilter(restProps, 'username$')}
44
+ >
45
+ {#snippet helper()}
46
+ <HelperText persistent>
47
+ {usernameVerifiedMessage || ''}
48
+ </HelperText>
49
+ {/snippet}
50
+ </Textfield>
51
+ </div>
52
+ {/if}
53
+
54
+ {#if $clientConfig.emailUsernames || $clientConfig.userFields.includes('email')}
55
+ <div>
56
+ <Textfield
57
+ bind:value={$user.email}
58
+ label="Email"
59
+ type="email"
60
+ style="width: 100%;"
61
+ helperLine$style="width: 100%;"
62
+ invalid={emailVerified === false}
63
+ input$autocomplete="email"
64
+ input$autocapitalize="off"
65
+ input$spellcheck="false"
66
+ input$emptyValueUndefined
67
+ {...prefixFilter(restProps, 'email$')}
68
+ >
69
+ {#snippet helper()}
70
+ <HelperText persistent>
71
+ {emailVerifiedMessage || ''}
72
+ </HelperText>
73
+ {/snippet}
74
+ </Textfield>
75
+ </div>
76
+ {/if}
77
+
78
+ {#if $clientConfig.userFields.includes('name')}
79
+ <div>
80
+ <Textfield
81
+ bind:value={$user.nameFirst}
82
+ label="First Name"
83
+ type="text"
84
+ style="width: 100%;"
85
+ input$autocomplete="given-name"
86
+ input$emptyValueUndefined
87
+ {...prefixFilter(restProps, 'nameFirst$')}
88
+ />
89
+ </div>
90
+
91
+ <div>
92
+ <Textfield
93
+ bind:value={$user.nameMiddle}
94
+ label="Middle Name"
95
+ type="text"
96
+ style="width: 100%;"
97
+ input$autocomplete="additional-name"
98
+ input$emptyValueUndefined
99
+ {...prefixFilter(restProps, 'nameMiddle$')}
100
+ />
101
+ </div>
102
+
103
+ <div>
104
+ <Textfield
105
+ bind:value={$user.nameLast}
106
+ label="Last Name"
107
+ type="text"
108
+ style="width: 100%;"
109
+ input$autocomplete="family-name"
110
+ input$emptyValueUndefined
111
+ {...prefixFilter(restProps, 'nameLast$')}
112
+ />
113
+ </div>
114
+ {/if}
115
+
116
+ {#if $clientConfig.userFields.includes('phone')}
117
+ <div>
118
+ <Textfield
119
+ bind:value={$user.phone}
120
+ label="Phone Number"
121
+ type="tel"
122
+ style="width: 100%;"
123
+ input$autocomplete="tel"
124
+ input$name="phone"
125
+ input$emptyValueUndefined
126
+ {...prefixFilter(restProps, 'phone$')}
127
+ />
128
+ </div>
129
+ {/if}
130
+
131
+ {@render additional?.()}
132
+
133
+ <div class="tilmeld-account-action">
134
+ <a
135
+ href={'javascript:void(0);'}
136
+ onclick={() => {
137
+ open = false;
138
+ changePasswordOpen = true;
139
+ }}
140
+ {...prefixFilter(restProps, 'changePasswordLink$')}
141
+ >
142
+ Change your password.
143
+ </a>
144
+ </div>
145
+
146
+ <div class="tilmeld-account-action">
147
+ <a
148
+ href={'javascript:void(0);'}
149
+ onclick={() => {
150
+ open = false;
151
+ revokeTokensOpen = true;
152
+ }}
153
+ {...prefixFilter(restProps, 'revokeTokensLink$')}
154
+ >
155
+ Log out of other sessions.
156
+ </a>
157
+
158
+ <div class="tilmeld-account-action">
159
+ <a
160
+ href={'javascript:void(0);'}
161
+ onclick={() => {
162
+ open = false;
163
+ twoFactorOpen = true;
164
+ }}
165
+ {...prefixFilter(restProps, 'twoFactor$')}
166
+ >
167
+ {#if hasTOTPSecret === false}
168
+ Enable two factor authentication (2FA).
169
+ {:else}
170
+ Manage two factor authentication (2FA).
171
+ {/if}
172
+ </a>
173
+ </div>
174
+
175
+ {#if failureMessage}
176
+ <div class="tilmeld-account-failure">
177
+ {failureMessage}
178
+ </div>
179
+ {/if}
180
+
181
+ {#if loading}
182
+ <div class="tilmeld-account-loading">
183
+ <CircularProgress
184
+ style="height: 24px; width: 24px;"
185
+ indeterminate
186
+ {...prefixFilter(restProps, 'progress$')}
187
+ />
188
+ </div>
189
+ {/if}
190
+ </div>
191
+ </Content>
192
+ <Actions>
193
+ <Button disabled={loading} {...prefixFilter(restProps, 'closeButton$')}>
194
+ <Label>Close</Label>
195
+ </Button>
196
+ <Button
197
+ onclick={preventDefault(stopPropagation(save))}
198
+ disabled={loading}
199
+ {...prefixFilter(restProps, 'saveButton$')}
200
+ >
201
+ <Label>Save Changes</Label>
202
+ </Button>
203
+ </Actions>
204
+ </Dialog>
205
+
206
+ <ChangePassword
207
+ {User}
208
+ bind:open={changePasswordOpen}
209
+ bind:user
210
+ {...prefixFilter(restProps, 'changePassword$')}
211
+ />
212
+
213
+ <RevokeTokens
214
+ {User}
215
+ bind:open={revokeTokensOpen}
216
+ bind:user
217
+ {...prefixFilter(restProps, 'revokeTokens$')}
218
+ />
219
+
220
+ <TwoFactor
221
+ {User}
222
+ bind:open={twoFactorOpen}
223
+ bind:user
224
+ bind:hasTOTPSecret
225
+ {...prefixFilter(restProps, 'twoFactor$')}
226
+ />
227
+ {/if}
228
+
229
+ <script lang="ts">
230
+ import type { ComponentProps, Snippet } from 'svelte';
231
+ import { onMount, onDestroy } from 'svelte';
232
+ import type { Writable } from 'svelte/store';
233
+ import { writable } from 'svelte/store';
234
+ import CircularProgress from '@smui/circular-progress';
235
+ import Dialog, { Title, Content, Actions } from '@smui/dialog';
236
+ import Textfield from '@smui/textfield';
237
+ import HelperText from '@smui/textfield/helper-text';
238
+ import Button, { Label } from '@smui/button';
239
+ import type { ActionArray } from '@smui/common/internal';
240
+ import { exclude, prefixFilter } from '@smui/common/internal';
241
+ import { preventDefault, stopPropagation } from '@smui/common/events';
242
+ import type { SmuiElementPropMap } from '@smui/common';
243
+ import type { ClientConfig, CurrentUserData } from '@nymphjs/tilmeld-client';
244
+ import type { User as UserClass } from '@nymphjs/tilmeld-client';
245
+ import ChangePassword from './ChangePassword.svelte';
246
+ import RevokeTokens from './RevokeTokens.svelte';
247
+ import TwoFactor from './TwoFactor.svelte';
248
+
249
+ type OwnProps = {
250
+ /**
251
+ * An array of Action or [Action, ActionProps] to be applied to the element.
252
+ */
253
+ use?: ActionArray;
254
+ /**
255
+ * Whether the dialog is open.
256
+ */
257
+ open?: boolean;
258
+ /**
259
+ * The title of the dialog.
260
+ */
261
+ title?: string;
262
+ /**
263
+ * A writable store of the Nymph client config.
264
+ *
265
+ * It will be retrieved from the server if not provided.
266
+ */
267
+ clientConfig?: Writable<ClientConfig | undefined>;
268
+ /**
269
+ * The User class from Nymph.
270
+ */
271
+ User: typeof UserClass;
272
+ /**
273
+ * A writable store of the current user.
274
+ *
275
+ * It will be retrieved from the server if not provided.
276
+ */
277
+ user?: Writable<(UserClass & CurrentUserData) | null | undefined>;
278
+
279
+ /**
280
+ * A spot for additional content.
281
+ */
282
+ additional?: Snippet;
283
+ };
284
+ let {
285
+ use = [],
286
+ open = $bindable(false),
287
+ title = 'Your Account',
288
+ clientConfig = $bindable(writable(false as unknown as undefined)),
289
+ User,
290
+ user = $bindable(writable(false as unknown as undefined)),
291
+ additional,
292
+ ...restProps
293
+ }: OwnProps & {
294
+ [k in keyof ComponentProps<
295
+ typeof Textfield
296
+ > as `username\$${k}`]?: ComponentProps<typeof Textfield>[k];
297
+ } & {
298
+ [k in keyof ComponentProps<
299
+ typeof Textfield
300
+ > as `email\$${k}`]?: ComponentProps<typeof Textfield>[k];
301
+ } & {
302
+ [k in keyof ComponentProps<
303
+ typeof Textfield
304
+ > as `nameFirst\$${k}`]?: ComponentProps<typeof Textfield>[k];
305
+ } & {
306
+ [k in keyof ComponentProps<
307
+ typeof Textfield
308
+ > as `nameMiddle\$${k}`]?: ComponentProps<typeof Textfield>[k];
309
+ } & {
310
+ [k in keyof ComponentProps<
311
+ typeof Textfield
312
+ > as `nameLast\$${k}`]?: ComponentProps<typeof Textfield>[k];
313
+ } & {
314
+ [k in keyof ComponentProps<
315
+ typeof Textfield
316
+ > as `phone\$${k}`]?: ComponentProps<typeof Textfield>[k];
317
+ } & {
318
+ [k in keyof SmuiElementPropMap['a'] as `changePasswordLink\$${k}`]?: SmuiElementPropMap['a'][k];
319
+ } & {
320
+ [k in keyof SmuiElementPropMap['a'] as `revokeTokensLink\$${k}`]?: SmuiElementPropMap['a'][k];
321
+ } & {
322
+ [k in keyof ComponentProps<
323
+ typeof CircularProgress
324
+ > as `progress\$${k}`]?: ComponentProps<typeof CircularProgress>[k];
325
+ } & {
326
+ [k in keyof ComponentProps<
327
+ typeof Button<undefined, 'button'>
328
+ > as `closeButton\$${k}`]?: ComponentProps<
329
+ typeof Button<undefined, 'button'>
330
+ >[k];
331
+ } & {
332
+ [k in keyof ComponentProps<
333
+ typeof Button<undefined, 'button'>
334
+ > as `saveButton\$${k}`]?: ComponentProps<
335
+ typeof Button<undefined, 'button'>
336
+ >[k];
337
+ } & {
338
+ [k in keyof ComponentProps<
339
+ typeof ChangePassword
340
+ > as `changePassword\$${k}`]?: ComponentProps<typeof ChangePassword>[k];
341
+ } & {
342
+ [k in keyof ComponentProps<
343
+ typeof RevokeTokens
344
+ > as `revokeTokens\$${k}`]?: ComponentProps<typeof RevokeTokens>[k];
345
+ } & {
346
+ [k in keyof ComponentProps<
347
+ typeof TwoFactor
348
+ > as `twoFactor\$${k}`]?: ComponentProps<typeof TwoFactor>[k];
349
+ } = $props();
350
+
351
+ let loading = $state(false);
352
+ let originalUsername: string | undefined = $user?.username;
353
+ let originalEmail: string | undefined = $user?.email;
354
+ let failureMessage: string | undefined = $state();
355
+ let usernameTimer: NodeJS.Timeout | undefined = undefined;
356
+ let usernameVerified: boolean | undefined = $state();
357
+ let usernameVerifiedMessage: string | undefined = $state();
358
+ let emailTimer: NodeJS.Timeout | undefined = undefined;
359
+ let emailVerified: boolean | undefined = $state();
360
+ let emailVerifiedMessage: string | undefined = $state();
361
+ let hasTOTPSecret: boolean | null = $state(null);
362
+ let changePasswordOpen = $state(false);
363
+ let revokeTokensOpen = $state(false);
364
+ let twoFactorOpen = $state(false);
365
+
366
+ const onLogin = (currentUser: UserClass & CurrentUserData) => {
367
+ $user = currentUser;
368
+ originalUsername = $user?.username;
369
+ originalEmail = $user?.email;
370
+ };
371
+ const onLogout = () => {
372
+ $user = null;
373
+ originalUsername = undefined;
374
+ originalEmail = undefined;
375
+ };
376
+
377
+ $effect(() => {
378
+ if ($user && $user.username !== originalUsername) {
379
+ if ($user.$isDirty('username')) {
380
+ checkUsername();
381
+ } else {
382
+ originalUsername = $user.username;
383
+ }
384
+ } else {
385
+ if (usernameTimer) {
386
+ clearTimeout(usernameTimer);
387
+ }
388
+ usernameVerified = true;
389
+ usernameVerifiedMessage = undefined;
390
+ }
391
+ });
392
+
393
+ $effect(() => {
394
+ if ($user && $user.email !== originalEmail) {
395
+ if ($user.$isDirty('email')) {
396
+ checkEmail();
397
+ } else {
398
+ originalEmail = $user.email;
399
+ }
400
+ } else {
401
+ if (emailTimer) {
402
+ clearTimeout(emailTimer);
403
+ }
404
+ emailVerified = true;
405
+ emailVerifiedMessage = undefined;
406
+ }
407
+ });
408
+
409
+ onMount(async () => {
410
+ User.on('login', onLogin);
411
+ User.on('logout', onLogout);
412
+ if ($user === (false as unknown as undefined)) {
413
+ $user = undefined;
414
+ $user = await User.current();
415
+ }
416
+ originalUsername = $user?.username;
417
+ originalEmail = $user?.email;
418
+ });
419
+ onMount(async () => {
420
+ if ($clientConfig === (false as unknown as undefined)) {
421
+ $clientConfig = undefined;
422
+ $clientConfig = await User.getClientConfig();
423
+ }
424
+ });
425
+
426
+ onDestroy(() => {
427
+ User.off('login', onLogin);
428
+ User.off('logout', onLogout);
429
+ });
430
+
431
+ async function save() {
432
+ if ($user == null) {
433
+ return;
434
+ }
435
+
436
+ failureMessage = undefined;
437
+ loading = true;
438
+
439
+ if ($clientConfig?.emailUsernames) {
440
+ $user.username = $user.email;
441
+ }
442
+
443
+ try {
444
+ if (await $user.$save()) {
445
+ originalEmail = $user.email;
446
+ open = false;
447
+ usernameVerifiedMessage = undefined;
448
+ emailVerifiedMessage = undefined;
449
+ } else {
450
+ failureMessage = 'Error saving account changes.';
451
+ }
452
+ } catch (e: any) {
453
+ failureMessage = e?.message;
454
+ }
455
+ loading = false;
456
+ }
457
+
458
+ function checkUsername() {
459
+ usernameVerified = undefined;
460
+ usernameVerifiedMessage = undefined;
461
+ if (usernameTimer) {
462
+ clearTimeout(usernameTimer);
463
+ usernameTimer = undefined;
464
+ }
465
+ usernameTimer = setTimeout(async () => {
466
+ try {
467
+ const data = await $user?.$checkUsername();
468
+ usernameVerified = data?.result ?? false;
469
+ usernameVerifiedMessage =
470
+ data?.message ?? 'Error getting verification.';
471
+ } catch (e: any) {
472
+ usernameVerified = false;
473
+ usernameVerifiedMessage = e?.message;
474
+ }
475
+ }, 400);
476
+ }
477
+
478
+ function checkEmail() {
479
+ emailVerified = undefined;
480
+ emailVerifiedMessage = undefined;
481
+ if (emailTimer) {
482
+ clearTimeout(emailTimer);
483
+ emailTimer = undefined;
484
+ }
485
+ emailTimer = setTimeout(async () => {
486
+ try {
487
+ const data = await $user?.$checkEmail();
488
+ emailVerified = data?.result ?? false;
489
+ emailVerifiedMessage = data?.message ?? 'Error getting verification.';
490
+ } catch (e: any) {
491
+ emailVerified = false;
492
+ emailVerifiedMessage = e?.message;
493
+ }
494
+ }, 400);
495
+ }
496
+ </script>
497
+
498
+ <style>
499
+ :global(.mdc-dialog .mdc-dialog__surface.tilmeld-account-dialog-surface) {
500
+ width: 360px;
501
+ max-width: calc(100vw - 32px);
502
+ }
503
+ .tilmeld-account-failure {
504
+ margin-top: 1em;
505
+ color: var(--mdc-theme-error, #f00);
506
+ }
507
+ .tilmeld-account-action {
508
+ margin-top: 1em;
509
+ }
510
+ .tilmeld-account-loading {
511
+ display: flex;
512
+ justify-content: center;
513
+ align-items: center;
514
+ }
515
+ </style>