@nauticana/sail 1.1.8 → 1.1.10

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.
Files changed (79) hide show
  1. package/README.md +36 -3
  2. package/package.json +5 -1
  3. package/src/component/abstract/base_async.ts +9 -11
  4. package/src/component/abstract/base_form.ts +6 -5
  5. package/src/component/abstract/base_table.ts +131 -58
  6. package/src/component/abstract/base_view.ts +43 -27
  7. package/src/component/account/profile_editor.ts +2 -2
  8. package/src/component/billing/payment_methods.ts +1 -2
  9. package/src/component/billing/price_selector.html +2 -1
  10. package/src/component/billing/price_selector.ts +28 -13
  11. package/src/component/billing/trial_banner.ts +7 -1
  12. package/src/component/billing/usage_meter.ts +2 -2
  13. package/src/component/dashboard/data_card.html +1 -1
  14. package/src/component/dashboard/entity_selector.ts +21 -23
  15. package/src/component/form/form_field.html +6 -5
  16. package/src/component/form/form_field.ts +39 -21
  17. package/src/component/form/form_record.html +3 -3
  18. package/src/component/form/form_record.ts +23 -8
  19. package/src/component/form/form_table.html +2 -2
  20. package/src/component/form/form_table.ts +6 -5
  21. package/src/component/login/account_deletion.ts +6 -6
  22. package/src/component/login/chpass_component.html +4 -4
  23. package/src/component/login/chpass_component.ts +0 -1
  24. package/src/component/login/confirm_chpass_component.html +4 -4
  25. package/src/component/login/confirm_chpass_component.ts +1 -4
  26. package/src/component/login/consent_gate.html +6 -6
  27. package/src/component/login/consent_gate.ts +17 -24
  28. package/src/component/login/login_component.html +7 -4
  29. package/src/component/login/login_component.ts +9 -5
  30. package/src/component/login/otp_input.html +13 -5
  31. package/src/component/login/otp_input.ts +34 -27
  32. package/src/component/login/register_component.html +31 -14
  33. package/src/component/login/register_component.ts +80 -89
  34. package/src/component/login/social_login.html +2 -2
  35. package/src/component/login/social_login.ts +20 -13
  36. package/src/component/navigation/navigation.html +2 -2
  37. package/src/component/navigation/navigation.ts +2 -0
  38. package/src/component/oauth_connection/oauth_connection_panel.html +68 -0
  39. package/src/component/oauth_connection/oauth_connection_panel.ts +252 -0
  40. package/src/component/payment/user_payment_methods.html +2 -2
  41. package/src/component/payment/user_payment_methods.ts +12 -23
  42. package/src/component/payout/bank_info_form.html +4 -4
  43. package/src/component/payout/bank_info_form.ts +22 -26
  44. package/src/component/payout/provider_onboarding.html +5 -5
  45. package/src/component/payout/provider_onboarding.ts +28 -48
  46. package/src/component/security/trusted_devices.html +1 -1
  47. package/src/component/security/trusted_devices.ts +0 -1
  48. package/src/component/security/twofactor_setup.html +8 -4
  49. package/src/component/security/twofactor_setup.ts +21 -3
  50. package/src/component/security/twofactor_verify.html +2 -1
  51. package/src/component/security/twofactor_verify.ts +2 -8
  52. package/src/component/table/table_detail.html +15 -14
  53. package/src/component/table/table_detail.ts +62 -41
  54. package/src/component/table/table_edit.html +15 -11
  55. package/src/component/table/table_edit.ts +94 -67
  56. package/src/component/table/table_list.html +45 -12
  57. package/src/component/table/table_list.ts +98 -26
  58. package/src/component/table/table_lookup.html +11 -8
  59. package/src/component/table/table_lookup.ts +45 -37
  60. package/src/component/table/table_report.html +2 -2
  61. package/src/component/table/table_report.ts +37 -31
  62. package/src/component/table/table_search.html +4 -4
  63. package/src/component/table/table_search.ts +36 -45
  64. package/src/config.ts +1 -1
  65. package/src/index.ts +4 -0
  66. package/src/model/appdata.ts +5 -9
  67. package/src/model/common.ts +27 -27
  68. package/src/model/decorator.ts +25 -21
  69. package/src/model/oauth_connection.ts +50 -0
  70. package/src/service/api-response.interceptor.ts +4 -1
  71. package/src/service/auth.interceptor.ts +27 -7
  72. package/src/service/auth.service.ts +188 -134
  73. package/src/service/label.service.ts +6 -5
  74. package/src/service/oauth_connection.service.ts +56 -0
  75. package/src/service/rest_service.ts +57 -13
  76. package/src/service/rest_url.ts +23 -7
  77. package/src/service/script_loader.ts +12 -2
  78. package/src/util/errors.ts +20 -0
  79. package/src/util/money.ts +19 -7
@@ -52,9 +52,10 @@ export class AccountDeletionComponent extends BaseAsync {
52
52
  });
53
53
 
54
54
  readonly typedValue = signal('');
55
+ private readonly passwordValue = signal('');
55
56
  readonly canSubmit = computed(() =>
56
57
  this.typedValue() === this.confirmationText() &&
57
- !!this.form.controls.password.value &&
58
+ !!this.passwordValue() &&
58
59
  !this.loading(),
59
60
  );
60
61
 
@@ -63,6 +64,9 @@ export class AccountDeletionComponent extends BaseAsync {
63
64
  this.form.controls.confirm.valueChanges
64
65
  .pipe(takeUntilDestroyed())
65
66
  .subscribe((v) => this.typedValue.set(v ?? ''));
67
+ this.form.controls.password.valueChanges
68
+ .pipe(takeUntilDestroyed())
69
+ .subscribe((v) => this.passwordValue.set(v ?? ''));
66
70
  }
67
71
 
68
72
  onDelete() {
@@ -71,11 +75,7 @@ export class AccountDeletionComponent extends BaseAsync {
71
75
  const reason = this.form.controls.reason.value?.trim() || undefined;
72
76
  this.run(
73
77
  this.auth.deleteAccount({ password }, reason),
74
- () => {
75
- this.successMessage.set('Account deleted.');
76
- const route = this.guiConfig.accountDeletedRoute ?? '/login/local';
77
- this.router.navigateByUrl(route);
78
- },
78
+ () => this.router.navigateByUrl(this.guiConfig.accountDeletedRoute ?? '/login/local'),
79
79
  'Failed to delete account. Please try again.',
80
80
  );
81
81
  }
@@ -9,23 +9,23 @@
9
9
  <form [formGroup]="chpassForm" (ngSubmit)="changePassword()" class="auth-form">
10
10
  <mat-form-field appearance="outline">
11
11
  <mat-label>Username</mat-label>
12
- <input matInput type="text" formControlName="username" required>
12
+ <input matInput type="text" formControlName="username" autocomplete="username" required>
13
13
  </mat-form-field>
14
14
 
15
15
  @if (!forgotPassword()) {
16
16
  <mat-form-field appearance="outline">
17
17
  <mat-label>Current Password</mat-label>
18
- <input matInput type="password" formControlName="old_password" required>
18
+ <input matInput type="password" formControlName="old_password" autocomplete="current-password" required>
19
19
  </mat-form-field>
20
20
 
21
21
  <mat-form-field appearance="outline">
22
22
  <mat-label>New Password</mat-label>
23
- <input matInput type="password" formControlName="new_password" required>
23
+ <input matInput type="password" formControlName="new_password" autocomplete="new-password" required>
24
24
  </mat-form-field>
25
25
 
26
26
  <mat-form-field appearance="outline">
27
27
  <mat-label>Confirm New Password</mat-label>
28
- <input matInput type="password" formControlName="confirm_password" required>
28
+ <input matInput type="password" formControlName="confirm_password" autocomplete="new-password" required>
29
29
  </mat-form-field>
30
30
  }
31
31
 
@@ -79,7 +79,6 @@ export class ChpassComponent extends BaseAsync {
79
79
  request,
80
80
  () => {
81
81
  if (isForgot) {
82
- this.successMessage.set('A confirmation code has been sent to your email.');
83
82
  this.router.navigate(['/confirm/password'], { queryParams: { username } });
84
83
  } else {
85
84
  this.successMessage.set('Password changed successfully.');
@@ -10,22 +10,22 @@
10
10
  <form [formGroup]="confirmForm" (ngSubmit)="confirm()" class="auth-form">
11
11
  <mat-form-field appearance="outline">
12
12
  <mat-label>Username</mat-label>
13
- <input matInput type="text" formControlName="username" required>
13
+ <input matInput type="text" formControlName="username" autocomplete="username" required>
14
14
  </mat-form-field>
15
15
 
16
16
  <mat-form-field appearance="outline">
17
17
  <mat-label>Confirmation Code</mat-label>
18
- <input matInput type="text" formControlName="code" required>
18
+ <input matInput type="text" formControlName="code" autocomplete="one-time-code" required>
19
19
  </mat-form-field>
20
20
 
21
21
  <mat-form-field appearance="outline">
22
22
  <mat-label>New Password</mat-label>
23
- <input matInput type="password" formControlName="new_password" required>
23
+ <input matInput type="password" formControlName="new_password" autocomplete="new-password" required>
24
24
  </mat-form-field>
25
25
 
26
26
  <mat-form-field appearance="outline">
27
27
  <mat-label>Confirm New Password</mat-label>
28
- <input matInput type="password" formControlName="confirm_password" required>
28
+ <input matInput type="password" formControlName="confirm_password" autocomplete="new-password" required>
29
29
  </mat-form-field>
30
30
 
31
31
  @if (errorMessage()) {
@@ -54,10 +54,7 @@ export class ConfirmChpassComponent extends BaseAsync implements OnInit {
54
54
 
55
55
  this.run(
56
56
  this.auth.confirmChpass(username!, code!, new_password!),
57
- () => {
58
- this.successMessage.set('Password changed successfully. Redirecting to login...');
59
- this.router.navigate(['/login/local']);
60
- },
57
+ () => this.router.navigate(['/login/local']),
61
58
  'Confirmation failed. Please try again.',
62
59
  );
63
60
  }
@@ -1,21 +1,21 @@
1
1
  <form [formGroup]="form" class="consent-form" role="group" aria-label="Consent required to continue">
2
- <label class="consent-row">
2
+ <div class="consent-row">
3
3
  <mat-checkbox [formControlName]="ConsentType.PRIVACY_POLICY" required>
4
4
  I have read and agree to the
5
5
  <a [attr.href]="effectivePrivacyUrl()" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
6
6
  and Terms of Service.
7
7
  </mat-checkbox>
8
- </label>
8
+ </div>
9
9
 
10
- <label class="consent-row">
10
+ <div class="consent-row">
11
11
  <mat-checkbox [formControlName]="ConsentType.CROSS_BORDER" required>
12
12
  I consent to my personal information being processed outside my region
13
13
  as described in the Privacy Policy.
14
14
  </mat-checkbox>
15
- </label>
15
+ </div>
16
16
 
17
17
  @for (opt of optionalConsents(); track opt.id) {
18
- <label class="consent-row">
18
+ <div class="consent-row">
19
19
  <mat-checkbox [formControlName]="opt.id" [required]="opt.required ?? false">
20
20
  {{ opt.label }}
21
21
  @if (opt.linkUrl && opt.linkText) {
@@ -25,6 +25,6 @@
25
25
  <small class="consent-hint">{{ opt.hint }}</small>
26
26
  }
27
27
  </mat-checkbox>
28
- </label>
28
+ </div>
29
29
  }
30
30
  </form>
@@ -1,5 +1,5 @@
1
1
  import {
2
- ChangeDetectionStrategy, Component, DestroyRef, ViewEncapsulation,
2
+ ChangeDetectionStrategy, Component, DestroyRef, OnInit, ViewEncapsulation,
3
3
  computed, effect, inject, input, output, signal,
4
4
  } from '@angular/core';
5
5
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -28,7 +28,7 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
28
28
  encapsulation: ViewEncapsulation.None,
29
29
  imports: [ReactiveFormsModule, MatCheckboxModule],
30
30
  })
31
- export class ConsentGateComponent {
31
+ export class ConsentGateComponent implements OnInit {
32
32
  private readonly destroyRef = inject(DestroyRef);
33
33
  protected readonly guiConfig: SailGuiConfig =
34
34
  inject(SAIL_GUI_CONFIG, { optional: true }) ?? DEFAULT_CONFIG;
@@ -74,7 +74,9 @@ export class ConsentGateComponent {
74
74
  }));
75
75
 
76
76
  constructor() {
77
- // Register optional consent controls dynamically when the input changes.
77
+ // Register optional consent controls when the input changes. add/removeControl
78
+ // fire valueChanges, so the subscriptions below emit — never emit from an
79
+ // effect: its microtask scheduling is unreliable for outputs in AOT builds.
78
80
  effect(() => {
79
81
  const options = this.optionalConsents();
80
82
  for (const opt of options) {
@@ -85,33 +87,24 @@ export class ConsentGateComponent {
85
87
  );
86
88
  }
87
89
  }
88
- // Drop controls for options that were removed.
89
90
  for (const key of Object.keys(this.form.controls)) {
90
91
  if (key === ConsentType.PRIVACY_POLICY || key === ConsentType.CROSS_BORDER) continue;
91
92
  if (!options.some((o) => o.id === key)) this.form.removeControl(key);
92
93
  }
93
- this.formValue.set(this.form.getRawValue());
94
- this.formValid.set(this.form.valid);
95
- this.consentStateChange.emit(this.state());
96
94
  });
97
95
 
98
- // Emit on every form change. We emit explicitly here rather than via a
99
- // standalone `effect(() => emit(state()))` because output emission
100
- // from inside an effect is unreliable in production AOT builds: the
101
- // effect's scheduler runs in a microtask outside the user-event CD
102
- // tick, and parent OnPush views can miss the propagated signal write.
103
- // Emitting inside the RxJS subscription puts the emission on the
104
- // synchronous click-handler path, where Angular's zone-aware CD
105
- // picks up the parent's `consents` signal write deterministically.
106
- this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
107
- this.formValue.set(this.form.getRawValue());
108
- this.formValid.set(this.form.valid);
109
- this.consentStateChange.emit(this.state());
110
- });
111
- this.form.statusChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
112
- this.formValid.set(this.form.valid);
113
- this.consentStateChange.emit(this.state());
114
- });
96
+ this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.syncAndEmit());
97
+ this.form.statusChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.syncAndEmit());
98
+ }
99
+
100
+ ngOnInit() {
101
+ this.syncAndEmit();
102
+ }
103
+
104
+ private syncAndEmit() {
105
+ this.formValue.set(this.form.getRawValue());
106
+ this.formValid.set(this.form.valid);
107
+ this.consentStateChange.emit(this.state());
115
108
  }
116
109
 
117
110
  protected readonly ConsentType = ConsentType;
@@ -15,14 +15,17 @@
15
15
  <form [formGroup]="loginForm" (ngSubmit)="login()" class="auth-form">
16
16
  <mat-form-field appearance="outline">
17
17
  <mat-label>Username</mat-label>
18
- <input matInput type="text" formControlName="username" required>
18
+ <input matInput type="text" formControlName="username" autocomplete="username" required>
19
19
  </mat-form-field>
20
20
  <mat-form-field appearance="outline">
21
21
  <mat-label>Password</mat-label>
22
- <input matInput type="password" formControlName="password" required>
22
+ <input matInput type="password" formControlName="password" autocomplete="current-password" required>
23
23
  </mat-form-field>
24
- <button matButton="elevated" class="primary" type="submit" [disabled]="!loginForm.valid">
25
- Login
24
+ @if (errorMessage()) {
25
+ <p class="auth-error" role="alert">{{ errorMessage() }}</p>
26
+ }
27
+ <button matButton="elevated" class="primary" type="submit" [disabled]="!loginForm.valid || loading()">
28
+ {{ loading() ? 'Signing in…' : 'Login' }}
26
29
  </button>
27
30
  </form>
28
31
  @if (showSocialLogin()) {
@@ -1,4 +1,5 @@
1
1
  import { ChangeDetectionStrategy, Component, computed, inject, ViewEncapsulation } from '@angular/core';
2
+ import { BaseAsync } from '../abstract/base_async';
2
3
  import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3
4
  import { BaseAuthService } from '../../service/auth.service';
4
5
  import { ActivatedRoute, RouterLink } from '@angular/router';
@@ -23,7 +24,7 @@ import { SocialLoginComponent } from './social_login';
23
24
  SocialLoginComponent,
24
25
  ],
25
26
  })
26
- export class LoginComponent {
27
+ export class LoginComponent extends BaseAsync {
27
28
  private auth = inject(BaseAuthService);
28
29
  protected fb = inject(FormBuilder);
29
30
  protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
@@ -52,10 +53,13 @@ export class LoginComponent {
52
53
  });
53
54
 
54
55
  login(): void {
55
- if (this.loginForm.valid) {
56
- const { username, password } = this.loginForm.value;
57
- this.auth.login(username!, password!);
58
- }
56
+ if (!this.loginForm.valid) return;
57
+ const { username, password } = this.loginForm.value;
58
+ this.run(
59
+ this.auth.login(username!, password!),
60
+ () => {},
61
+ 'Invalid username or password.',
62
+ );
59
63
  }
60
64
 
61
65
  /**
@@ -4,17 +4,25 @@
4
4
  <p class="otp-contact">{{ contact() }}</p>
5
5
  }
6
6
 
7
- <div class="otp-digits" role="group" aria-label="Verification code">
7
+ <input class="otp-input-field"
8
+ type="text"
9
+ inputmode="numeric"
10
+ autocomplete="one-time-code"
11
+ [attr.maxlength]="length()"
12
+ [value]="code()"
13
+ (input)="onCodeInput($event)"
14
+ [disabled]="disabled()"
15
+ aria-label="Verification code">
16
+
17
+ <div class="otp-digits" aria-hidden="true">
8
18
  @for (digit of digits(); track $index) {
9
- <div class="otp-digit"
10
- [class.active]="$index === currentIndex()"
11
- [attr.aria-label]="'Digit ' + ($index + 1)">
19
+ <div class="otp-digit" [class.active]="$index === currentIndex()">
12
20
  {{ digit }}
13
21
  </div>
14
22
  }
15
23
  </div>
16
24
 
17
- <p class="otp-resend">
25
+ <p class="otp-resend" aria-live="polite">
18
26
  @if (canResend()) {
19
27
  <button matButton class="resend-btn" type="button" (click)="onResend()">Resend code</button>
20
28
  } @else {
@@ -1,12 +1,14 @@
1
1
  import {
2
- ChangeDetectionStrategy, Component, DestroyRef, OnInit, ViewEncapsulation,
2
+ ChangeDetectionStrategy, Component, DestroyRef, ViewEncapsulation,
3
3
  computed, effect, inject, input, output, signal,
4
4
  } from '@angular/core';
5
5
  import { MatButtonModule } from '@angular/material/button';
6
6
  import { MatIconModule } from '@angular/material/icon';
7
7
 
8
8
  /**
9
- * OtpInput — pure-presentational numeric OTP keypad with resend countdown.
9
+ * OtpInput — numeric OTP entry with resend countdown. A real (styleable)
10
+ * input drives the digit boxes, so typing, paste, and one-time-code
11
+ * autofill all work; the keypad is a touch-friendly supplement.
10
12
  *
11
13
  * Owns no service layer. The parent component sends the OTP (via
12
14
  * BaseAuthService.sendOtp) and subscribes to `codeComplete` / `resend` to
@@ -19,17 +21,14 @@ import { MatIconModule } from '@angular/material/icon';
19
21
  encapsulation: ViewEncapsulation.None,
20
22
  imports: [MatButtonModule, MatIconModule],
21
23
  })
22
- export class OtpInputComponent implements OnInit {
24
+ export class OtpInputComponent {
23
25
  private readonly destroyRef = inject(DestroyRef);
24
26
 
25
27
  readonly length = input(6);
26
28
  readonly contact = input('');
27
29
  // Fallback only. Consumers should pass the value returned by
28
- // /public/otp/send in `resendCountdownSec` on the response — that is
29
- // the authoritative value (sourced from the keel server's
30
- // --otp_token_ttl_seconds flag and matches the lifetime of the OTP
31
- // code itself). 300s is the legacy default, kept so existing callers
32
- // without the new field don't break.
30
+ // /public/otp/send in `resendCountdownSec` — the authoritative TTL from
31
+ // keel's --otp_token_ttl_seconds. The countdown restarts when it changes.
33
32
  readonly resendCountdownSec = input(300);
34
33
  readonly disabled = input(false);
35
34
 
@@ -48,48 +47,56 @@ export class OtpInputComponent implements OnInit {
48
47
  return idx === -1 ? d.length - 1 : idx;
49
48
  });
50
49
 
50
+ readonly code = computed(() => this.digits().join(''));
51
+
51
52
  readonly keypadKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '', '0', 'back'] as const;
52
53
 
53
54
  constructor() {
54
- // Reset the digits array whenever `length` input changes.
55
55
  effect(() => { this.digits.set(Array(this.length()).fill('')); });
56
+ // Restarts when the parent binds the authoritative TTL (arrives async).
57
+ effect(() => {
58
+ const ttl = this.resendCountdownSec();
59
+ this.startCountdown(ttl);
60
+ });
56
61
  this.destroyRef.onDestroy(() => this.clearTimer());
57
62
  }
58
63
 
59
- ngOnInit() {
60
- this.startCountdown();
64
+ onCodeInput(event: Event) {
65
+ if (this.disabled()) return;
66
+ const target = event.target as HTMLInputElement;
67
+ const raw = target.value.replace(/\D/g, '').slice(0, this.length());
68
+ target.value = raw;
69
+ this.setCode(raw);
70
+ }
71
+
72
+ private setCode(raw: string) {
73
+ const d = Array<string>(this.length()).fill('');
74
+ for (let i = 0; i < raw.length; i++) d[i] = raw[i];
75
+ this.digits.set(d);
76
+ if (raw.length === this.length()) this.codeComplete.emit(raw);
61
77
  }
62
78
 
63
79
  onKeyPress(key: string) {
64
80
  if (this.disabled()) return;
65
- const d = [...this.digits()];
66
- const idx = d.findIndex((v) => v === '');
67
- if (idx === -1) return;
68
- d[idx] = key;
69
- this.digits.set(d);
70
- if (idx === this.length() - 1) this.codeComplete.emit(d.join(''));
81
+ const current = this.code();
82
+ if (current.length >= this.length()) return;
83
+ this.setCode(current + key);
71
84
  }
72
85
 
73
86
  onBackspace() {
74
87
  if (this.disabled()) return;
75
- const d = [...this.digits()];
76
- let idx = d.findIndex((v) => v === '');
77
- if (idx === -1) idx = d.length;
78
- if (idx > 0) {
79
- d[idx - 1] = '';
80
- this.digits.set(d);
81
- }
88
+ this.setCode(this.code().slice(0, -1));
82
89
  }
83
90
 
84
91
  onResend() {
85
92
  this.resend.emit();
86
93
  this.digits.set(Array(this.length()).fill(''));
87
- this.startCountdown();
94
+ this.startCountdown(this.resendCountdownSec());
88
95
  }
89
96
 
90
- private startCountdown() {
97
+ private startCountdown(seconds: number) {
91
98
  this.clearTimer();
92
- this.countdown.set(this.resendCountdownSec());
99
+ this.countdown.set(seconds);
93
100
  this.canResend.set(false);
94
101
  this.countdownTimer = setInterval(() => {
95
102
  const next = this.countdown() - 1;
@@ -13,34 +13,51 @@
13
13
  <div class="form-row">
14
14
  <mat-form-field appearance="outline">
15
15
  <mat-label>First Name</mat-label>
16
- <input matInput type="text" formControlName="FirstName" required>
16
+ <input matInput type="text" formControlName="FirstName" autocomplete="given-name" required>
17
17
  </mat-form-field>
18
18
 
19
19
  <mat-form-field appearance="outline">
20
20
  <mat-label>Last Name</mat-label>
21
- <input matInput type="text" formControlName="LastName" required>
21
+ <input matInput type="text" formControlName="LastName" autocomplete="family-name" required>
22
22
  </mat-form-field>
23
23
  </div>
24
24
 
25
25
  <mat-form-field appearance="outline">
26
26
  <mat-label>Email</mat-label>
27
- <input matInput type="email" formControlName="Email" required>
27
+ <input matInput type="email" formControlName="Email" autocomplete="email" required>
28
+ @if (registerForm.controls.Email.hasError('email')) {
29
+ <mat-error>Enter a valid email address.</mat-error>
30
+ }
31
+ </mat-form-field>
32
+
33
+ <mat-form-field appearance="outline">
34
+ <mat-label>Username</mat-label>
35
+ <input matInput type="text" formControlName="UserName" autocomplete="username" required>
28
36
  </mat-form-field>
29
37
 
30
38
  <div class="form-row">
31
39
  <mat-form-field appearance="outline">
32
- <mat-label>Username</mat-label>
33
- <input matInput type="text" formControlName="UserName" required>
40
+ <mat-label>Password</mat-label>
41
+ <input matInput type="password" formControlName="Password" autocomplete="new-password" required>
42
+ @if (registerForm.controls.Password.hasError('minlength')) {
43
+ <mat-error>At least 6 characters.</mat-error>
44
+ }
34
45
  </mat-form-field>
35
46
 
36
47
  <mat-form-field appearance="outline">
37
- <mat-label>Password</mat-label>
38
- <input matInput type="password" formControlName="Password" required>
48
+ <mat-label>Confirm Password</mat-label>
49
+ <input matInput type="password" formControlName="ConfirmPassword" autocomplete="new-password" required>
39
50
  </mat-form-field>
40
51
  </div>
52
+ @if (registerForm.hasError('passwordMismatch') && registerForm.controls.ConfirmPassword.touched) {
53
+ <div class="geocode-status geocode-error" role="alert">Passwords do not match.</div>
54
+ }
41
55
 
42
56
  <div class="auth-section-label">Plan</div>
43
57
 
58
+ @if (plansError()) {
59
+ <div class="geocode-status geocode-error" role="alert">{{ plansError() }}</div>
60
+ }
44
61
  <mat-form-field appearance="outline">
45
62
  <mat-label>Subscription plan</mat-label>
46
63
  <mat-select formControlName="PlanID" required>
@@ -62,12 +79,12 @@
62
79
  <div class="form-row">
63
80
  <mat-form-field appearance="outline">
64
81
  <mat-label>Phone</mat-label>
65
- <input matInput type="tel" formControlName="Phone" required>
82
+ <input matInput type="tel" formControlName="Phone" autocomplete="tel" required>
66
83
  </mat-form-field>
67
84
 
68
85
  <mat-form-field appearance="outline">
69
86
  <mat-label>Website URL</mat-label>
70
- <input matInput type="url" formControlName="DomainURL" required>
87
+ <input matInput type="url" formControlName="DomainURL" autocomplete="url" required>
71
88
  </mat-form-field>
72
89
  </div>
73
90
 
@@ -75,30 +92,30 @@
75
92
 
76
93
  <mat-form-field appearance="outline">
77
94
  <mat-label>Address (Street and Number)</mat-label>
78
- <input matInput type="text" formControlName="Address" required>
95
+ <input matInput type="text" formControlName="Address" autocomplete="address-line1" required>
79
96
  </mat-form-field>
80
97
 
81
98
  <div class="form-row">
82
99
  <mat-form-field appearance="outline">
83
100
  <mat-label>City</mat-label>
84
- <input matInput type="text" formControlName="City" required>
101
+ <input matInput type="text" formControlName="City" autocomplete="address-level2" required>
85
102
  </mat-form-field>
86
103
 
87
104
  <mat-form-field appearance="outline">
88
105
  <mat-label>State</mat-label>
89
- <input matInput type="text" formControlName="State" required>
106
+ <input matInput type="text" formControlName="State" autocomplete="address-level1" required>
90
107
  </mat-form-field>
91
108
  </div>
92
109
 
93
110
  <div class="form-row">
94
111
  <mat-form-field appearance="outline">
95
112
  <mat-label>Zipcode</mat-label>
96
- <input matInput type="text" formControlName="Zipcode" required>
113
+ <input matInput type="text" formControlName="Zipcode" autocomplete="postal-code" required>
97
114
  </mat-form-field>
98
115
 
99
116
  <mat-form-field appearance="outline">
100
117
  <mat-label>Country</mat-label>
101
- <input matInput type="text" formControlName="Country" required>
118
+ <input matInput type="text" formControlName="Country" autocomplete="country-name" required>
102
119
  </mat-form-field>
103
120
  </div>
104
121