@nymphjs/tilmeld-components 1.0.0-beta.13 → 1.0.0-beta.130

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.
@@ -1,11 +1,13 @@
1
- {#if clientConfig != null && user != null}
1
+ <svelte:options runes />
2
+
3
+ {#if $clientConfig != null && $user != null}
2
4
  <Dialog
3
- use={usePass}
5
+ {use}
4
6
  bind:open
5
7
  aria-labelledby="tilmeld-account-title"
6
8
  aria-describedby="tilmeld-account-content"
7
9
  surface$class="tilmeld-account-dialog-surface"
8
- {...exclude($$restProps, [
10
+ {...exclude(restProps, [
9
11
  'username$',
10
12
  'email$',
11
13
  'nameFirst$',
@@ -13,19 +15,22 @@
13
15
  'nameLast$',
14
16
  'phone$',
15
17
  'changePasswordLink$',
18
+ 'revokeTokensLink$',
16
19
  'closeButton$',
17
20
  'saveButton$',
18
21
  'changePassword$',
22
+ 'revokeTokens$',
23
+ 'twoFactor$',
19
24
  'progress$',
20
25
  ])}
21
26
  >
22
27
  <!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
23
28
  <Title id="tilmeld-account-title">{title}</Title>
24
29
  <Content id="tilmeld-account-content">
25
- {#if !clientConfig.emailUsernames && clientConfig.allowUsernameChange}
30
+ {#if !$clientConfig.emailUsernames && $clientConfig.allowUsernameChange}
26
31
  <div>
27
32
  <Textfield
28
- bind:value={user.username}
33
+ bind:value={$user.username}
29
34
  label="Username"
30
35
  type="text"
31
36
  style="width: 100%;"
@@ -35,19 +40,21 @@
35
40
  input$autocapitalize="off"
36
41
  input$spellcheck="false"
37
42
  input$emptyValueUndefined
38
- {...prefixFilter($$restProps, 'username$')}
43
+ {...prefixFilter(restProps, 'username$')}
39
44
  >
40
- <HelperText persistent slot="helper">
41
- {usernameVerifiedMessage || ''}
42
- </HelperText>
45
+ {#snippet helper()}
46
+ <HelperText persistent>
47
+ {usernameVerifiedMessage || ''}
48
+ </HelperText>
49
+ {/snippet}
43
50
  </Textfield>
44
51
  </div>
45
52
  {/if}
46
53
 
47
- {#if clientConfig.emailUsernames || clientConfig.userFields.indexOf('email') !== -1}
54
+ {#if $clientConfig.emailUsernames || $clientConfig.userFields.includes('email')}
48
55
  <div>
49
56
  <Textfield
50
- bind:value={user.email}
57
+ bind:value={$user.email}
51
58
  label="Email"
52
59
  type="email"
53
60
  style="width: 100%;"
@@ -57,203 +64,366 @@
57
64
  input$autocapitalize="off"
58
65
  input$spellcheck="false"
59
66
  input$emptyValueUndefined
60
- {...prefixFilter($$restProps, 'email$')}
67
+ {...prefixFilter(restProps, 'email$')}
61
68
  >
62
- <HelperText persistent slot="helper">
63
- {emailVerifiedMessage || ''}
64
- </HelperText>
69
+ {#snippet helper()}
70
+ <HelperText persistent>
71
+ {emailVerifiedMessage || ''}
72
+ </HelperText>
73
+ {/snippet}
65
74
  </Textfield>
66
75
  </div>
67
76
  {/if}
68
77
 
69
- {#if clientConfig.userFields.indexOf('name') !== -1}
78
+ {#if $clientConfig.userFields.includes('name')}
70
79
  <div>
71
80
  <Textfield
72
- bind:value={user.nameFirst}
81
+ bind:value={$user.nameFirst}
73
82
  label="First Name"
74
83
  type="text"
75
84
  style="width: 100%;"
76
85
  input$autocomplete="given-name"
77
86
  input$emptyValueUndefined
78
- {...prefixFilter($$restProps, 'nameFirst$')}
87
+ {...prefixFilter(restProps, 'nameFirst$')}
79
88
  />
80
89
  </div>
81
90
 
82
91
  <div>
83
92
  <Textfield
84
- bind:value={user.nameMiddle}
93
+ bind:value={$user.nameMiddle}
85
94
  label="Middle Name"
86
95
  type="text"
87
96
  style="width: 100%;"
88
97
  input$autocomplete="additional-name"
89
98
  input$emptyValueUndefined
90
- {...prefixFilter($$restProps, 'nameMiddle$')}
99
+ {...prefixFilter(restProps, 'nameMiddle$')}
91
100
  />
92
101
  </div>
93
102
 
94
103
  <div>
95
104
  <Textfield
96
- bind:value={user.nameLast}
105
+ bind:value={$user.nameLast}
97
106
  label="Last Name"
98
107
  type="text"
99
108
  style="width: 100%;"
100
109
  input$autocomplete="family-name"
101
110
  input$emptyValueUndefined
102
- {...prefixFilter($$restProps, 'nameLast$')}
111
+ {...prefixFilter(restProps, 'nameLast$')}
103
112
  />
104
113
  </div>
105
114
  {/if}
106
115
 
107
- {#if clientConfig.userFields.indexOf('phone') !== -1}
116
+ {#if $clientConfig.userFields.includes('phone')}
108
117
  <div>
109
118
  <Textfield
110
- bind:value={user.phone}
119
+ bind:value={$user.phone}
111
120
  label="Phone Number"
112
121
  type="tel"
113
122
  style="width: 100%;"
114
123
  input$autocomplete="tel"
115
124
  input$name="phone"
116
125
  input$emptyValueUndefined
117
- {...prefixFilter($$restProps, 'phone$')}
126
+ {...prefixFilter(restProps, 'phone$')}
118
127
  />
119
128
  </div>
120
129
  {/if}
121
130
 
122
- <slot name="additional" />
131
+ {@render additional?.()}
123
132
 
124
133
  <div class="tilmeld-account-action">
125
134
  <a
126
- href="javascript:void(0);"
127
- on:click={() => {
135
+ href={'javascript:void(0);'}
136
+ onclick={() => {
128
137
  open = false;
129
138
  changePasswordOpen = true;
130
139
  }}
131
- {...prefixFilter($$restProps, 'changePasswordLink$')}
140
+ {...prefixFilter(restProps, 'changePasswordLink$')}
132
141
  >
133
142
  Change your password.
134
143
  </a>
135
144
  </div>
136
145
 
137
- {#if failureMessage}
138
- <div class="tilmeld-account-failure">
139
- {failureMessage}
140
- </div>
141
- {/if}
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>
142
157
 
143
- {#if saving}
144
- <div class="tilmeld-account-loading">
145
- <CircularProgress
146
- style="height: 24px; width: 24px;"
147
- indeterminate
148
- {...prefixFilter($$restProps, 'progress$')}
149
- />
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>
150
173
  </div>
151
- {/if}
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>
152
191
  </Content>
153
192
  <Actions>
154
- <Button disabled={saving} {...prefixFilter($$restProps, 'closeButton$')}>
193
+ <Button disabled={loading} {...prefixFilter(restProps, 'closeButton$')}>
155
194
  <Label>Close</Label>
156
195
  </Button>
157
196
  <Button
158
- on:click$preventDefault$stopPropagation={save}
159
- disabled={saving}
160
- {...prefixFilter($$restProps, 'saveButton$')}
197
+ onclick={preventDefault(stopPropagation(save))}
198
+ disabled={loading}
199
+ {...prefixFilter(restProps, 'saveButton$')}
161
200
  >
162
201
  <Label>Save Changes</Label>
163
202
  </Button>
164
203
  </Actions>
165
204
  </Dialog>
205
+
166
206
  <ChangePassword
167
207
  {User}
168
208
  bind:open={changePasswordOpen}
169
209
  bind:user
170
- {...prefixFilter($$restProps, 'changePassword$')}
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$')}
171
226
  />
172
227
  {/if}
173
228
 
174
229
  <script lang="ts">
230
+ import type { ComponentProps, Snippet } from 'svelte';
175
231
  import { onMount, onDestroy } from 'svelte';
176
- import { get_current_component } from 'svelte/internal';
232
+ import type { Writable } from 'svelte/store';
233
+ import { writable } from 'svelte/store';
177
234
  import CircularProgress from '@smui/circular-progress';
178
235
  import Dialog, { Title, Content, Actions } from '@smui/dialog';
179
236
  import Textfield from '@smui/textfield';
180
237
  import HelperText from '@smui/textfield/helper-text';
181
238
  import Button, { Label } from '@smui/button';
182
239
  import type { ActionArray } from '@smui/common/internal';
183
- import {
184
- forwardEventsBuilder,
185
- exclude,
186
- prefixFilter,
187
- } 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';
188
243
  import type { ClientConfig, CurrentUserData } from '@nymphjs/tilmeld-client';
189
244
  import type { User as UserClass } from '@nymphjs/tilmeld-client';
190
245
  import ChangePassword from './ChangePassword.svelte';
246
+ import RevokeTokens from './RevokeTokens.svelte';
247
+ import TwoFactor from './TwoFactor.svelte';
191
248
 
192
- const forwardEvents = forwardEventsBuilder(get_current_component());
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>;
193
278
 
194
- export let use: ActionArray = [];
195
- $: usePass = [forwardEvents, ...use] as ActionArray;
196
- export let open = false;
197
- export let title = 'Your Account';
198
- export let clientConfig: ClientConfig | undefined = undefined;
199
- export let User: typeof UserClass;
200
- export let user: (UserClass & CurrentUserData) | undefined = undefined;
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
+ [
295
+ k in keyof ComponentProps<typeof Textfield> as `username\$${k}`
296
+ ]?: ComponentProps<typeof Textfield>[k];
297
+ } & {
298
+ [
299
+ k in keyof ComponentProps<typeof Textfield> as `email\$${k}`
300
+ ]?: ComponentProps<typeof Textfield>[k];
301
+ } & {
302
+ [
303
+ k in keyof ComponentProps<typeof Textfield> as `nameFirst\$${k}`
304
+ ]?: ComponentProps<typeof Textfield>[k];
305
+ } & {
306
+ [
307
+ k in keyof ComponentProps<typeof Textfield> as `nameMiddle\$${k}`
308
+ ]?: ComponentProps<typeof Textfield>[k];
309
+ } & {
310
+ [
311
+ k in keyof ComponentProps<typeof Textfield> as `nameLast\$${k}`
312
+ ]?: ComponentProps<typeof Textfield>[k];
313
+ } & {
314
+ [
315
+ k in keyof ComponentProps<typeof Textfield> as `phone\$${k}`
316
+ ]?: ComponentProps<typeof Textfield>[k];
317
+ } & {
318
+ [
319
+ k in keyof SmuiElementPropMap['a'] as `changePasswordLink\$${k}`
320
+ ]?: SmuiElementPropMap['a'][k];
321
+ } & {
322
+ [
323
+ k in keyof SmuiElementPropMap['a'] as `revokeTokensLink\$${k}`
324
+ ]?: SmuiElementPropMap['a'][k];
325
+ } & {
326
+ [
327
+ k in keyof ComponentProps<typeof CircularProgress> as `progress\$${k}`
328
+ ]?: ComponentProps<typeof CircularProgress>[k];
329
+ } & {
330
+ [
331
+ k in keyof ComponentProps<
332
+ typeof Button<undefined, 'button'>
333
+ > as `closeButton\$${k}`
334
+ ]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
335
+ } & {
336
+ [
337
+ k in keyof ComponentProps<
338
+ typeof Button<undefined, 'button'>
339
+ > as `saveButton\$${k}`
340
+ ]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
341
+ } & {
342
+ [
343
+ k in keyof ComponentProps<typeof ChangePassword> as `changePassword\$${k}`
344
+ ]?: ComponentProps<typeof ChangePassword>[k];
345
+ } & {
346
+ [
347
+ k in keyof ComponentProps<typeof RevokeTokens> as `revokeTokens\$${k}`
348
+ ]?: ComponentProps<typeof RevokeTokens>[k];
349
+ } & {
350
+ [
351
+ k in keyof ComponentProps<typeof TwoFactor> as `twoFactor\$${k}`
352
+ ]?: ComponentProps<typeof TwoFactor>[k];
353
+ } = $props();
201
354
 
202
- let saving = false;
203
- let originalUsername: string | undefined = undefined;
204
- let originalEmail: string | undefined = undefined;
205
- let failureMessage: string | undefined = undefined;
206
- let usernameTimer: NodeJS.Timeout | undefined = undefined;
207
- let usernameVerified: boolean | undefined = undefined;
208
- let usernameVerifiedMessage: string | undefined = undefined;
209
- let emailTimer: NodeJS.Timeout | undefined = undefined;
210
- let emailVerified: boolean | undefined = undefined;
211
- let emailVerifiedMessage: string | undefined = undefined;
212
- let changePasswordOpen = false;
355
+ let loading = $state(false);
356
+ let originalUsername: string | undefined = $user?.username;
357
+ let originalEmail: string | undefined = $user?.email;
358
+ let failureMessage: string | undefined = $state();
359
+ let usernameTimer: number | undefined = undefined;
360
+ let usernameVerified: boolean | undefined = $state();
361
+ let usernameVerifiedMessage: string | undefined = $state();
362
+ let emailTimer: number | undefined = undefined;
363
+ let emailVerified: boolean | undefined = $state();
364
+ let emailVerifiedMessage: string | undefined = $state();
365
+ let hasTOTPSecret: boolean | null = $state(null);
366
+ let changePasswordOpen = $state(false);
367
+ let revokeTokensOpen = $state(false);
368
+ let twoFactorOpen = $state(false);
213
369
 
214
370
  const onLogin = (currentUser: UserClass & CurrentUserData) => {
215
- user = currentUser;
216
- originalUsername = user?.username;
217
- originalEmail = user?.email;
371
+ $user = currentUser;
372
+ originalUsername = $user?.username;
373
+ originalEmail = $user?.email;
218
374
  };
219
375
  const onLogout = () => {
220
- user = undefined;
376
+ $user = null;
221
377
  originalUsername = undefined;
222
378
  originalEmail = undefined;
223
379
  };
224
380
 
225
- $: if (user && user.username !== originalUsername) {
226
- checkUsername();
227
- } else {
228
- if (usernameTimer) {
229
- clearTimeout(usernameTimer);
381
+ $effect(() => {
382
+ if ($user && $user.username !== originalUsername) {
383
+ if ($user.$isDirty('username')) {
384
+ checkUsername();
385
+ } else {
386
+ originalUsername = $user.username;
387
+ }
388
+ } else {
389
+ if (usernameTimer) {
390
+ clearTimeout(usernameTimer);
391
+ }
392
+ usernameVerified = true;
393
+ usernameVerifiedMessage = undefined;
230
394
  }
231
- usernameVerified = true;
232
- usernameVerifiedMessage = undefined;
233
- }
395
+ });
234
396
 
235
- $: if (user && user.email !== originalEmail) {
236
- checkEmail();
237
- } else {
238
- if (emailTimer) {
239
- clearTimeout(emailTimer);
397
+ $effect(() => {
398
+ if ($user && $user.email !== originalEmail) {
399
+ if ($user.$isDirty('email')) {
400
+ checkEmail();
401
+ } else {
402
+ originalEmail = $user.email;
403
+ }
404
+ } else {
405
+ if (emailTimer) {
406
+ clearTimeout(emailTimer);
407
+ }
408
+ emailVerified = true;
409
+ emailVerifiedMessage = undefined;
240
410
  }
241
- emailVerified = true;
242
- emailVerifiedMessage = undefined;
243
- }
411
+ });
244
412
 
245
413
  onMount(async () => {
246
414
  User.on('login', onLogin);
247
415
  User.on('logout', onLogout);
248
- if (user === undefined) {
249
- user = (await User.current()) ?? undefined;
416
+ if ($user === (false as unknown as undefined)) {
417
+ $user = undefined;
418
+ $user = await User.current();
250
419
  }
251
- originalUsername = user?.username;
252
- originalEmail = user?.email;
420
+ originalUsername = $user?.username;
421
+ originalEmail = $user?.email;
253
422
  });
254
423
  onMount(async () => {
255
- if (clientConfig === undefined) {
256
- clientConfig = await User.getClientConfig();
424
+ if ($clientConfig === (false as unknown as undefined)) {
425
+ $clientConfig = undefined;
426
+ $clientConfig = await User.getClientConfig();
257
427
  }
258
428
  });
259
429
 
@@ -263,30 +433,27 @@
263
433
  });
264
434
 
265
435
  async function save() {
266
- if (user == null) {
436
+ if ($user == null) {
267
437
  return;
268
438
  }
269
439
 
270
440
  failureMessage = undefined;
271
- saving = true;
441
+ loading = true;
272
442
 
273
- if (clientConfig?.emailUsernames) {
274
- user.username = user.email;
443
+ if ($clientConfig?.emailUsernames) {
444
+ $user.username = $user.email;
275
445
  }
276
446
 
277
447
  try {
278
- if (await user.$save()) {
279
- originalEmail = user.email;
280
- open = false;
281
- usernameVerifiedMessage = undefined;
282
- emailVerifiedMessage = undefined;
283
- } else {
284
- failureMessage = 'Error saving account changes.';
285
- }
448
+ await $user.$save();
449
+ originalEmail = $user.email;
450
+ open = false;
451
+ usernameVerifiedMessage = undefined;
452
+ emailVerifiedMessage = undefined;
286
453
  } catch (e: any) {
287
454
  failureMessage = e?.message;
288
455
  }
289
- saving = false;
456
+ loading = false;
290
457
  }
291
458
 
292
459
  function checkUsername() {
@@ -298,7 +465,7 @@
298
465
  }
299
466
  usernameTimer = setTimeout(async () => {
300
467
  try {
301
- const data = await user?.$checkUsername();
468
+ const data = await $user?.$checkUsername();
302
469
  usernameVerified = data?.result ?? false;
303
470
  usernameVerifiedMessage =
304
471
  data?.message ?? 'Error getting verification.';
@@ -318,7 +485,7 @@
318
485
  }
319
486
  emailTimer = setTimeout(async () => {
320
487
  try {
321
- const data = await user?.$checkEmail();
488
+ const data = await $user?.$checkEmail();
322
489
  emailVerified = data?.result ?? false;
323
490
  emailVerifiedMessage = data?.message ?? 'Error getting verification.';
324
491
  } catch (e: any) {