@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.
- package/README.md +36 -3
- package/package.json +5 -1
- package/src/component/abstract/base_async.ts +9 -11
- package/src/component/abstract/base_form.ts +6 -5
- package/src/component/abstract/base_table.ts +131 -58
- package/src/component/abstract/base_view.ts +43 -27
- package/src/component/account/profile_editor.ts +2 -2
- package/src/component/billing/payment_methods.ts +1 -2
- package/src/component/billing/price_selector.html +2 -1
- package/src/component/billing/price_selector.ts +28 -13
- package/src/component/billing/trial_banner.ts +7 -1
- package/src/component/billing/usage_meter.ts +2 -2
- package/src/component/dashboard/data_card.html +1 -1
- package/src/component/dashboard/entity_selector.ts +21 -23
- package/src/component/form/form_field.html +6 -5
- package/src/component/form/form_field.ts +39 -21
- package/src/component/form/form_record.html +3 -3
- package/src/component/form/form_record.ts +23 -8
- package/src/component/form/form_table.html +2 -2
- package/src/component/form/form_table.ts +6 -5
- package/src/component/login/account_deletion.ts +6 -6
- package/src/component/login/chpass_component.html +4 -4
- package/src/component/login/chpass_component.ts +0 -1
- package/src/component/login/confirm_chpass_component.html +4 -4
- package/src/component/login/confirm_chpass_component.ts +1 -4
- package/src/component/login/consent_gate.html +6 -6
- package/src/component/login/consent_gate.ts +17 -24
- package/src/component/login/login_component.html +7 -4
- package/src/component/login/login_component.ts +9 -5
- package/src/component/login/otp_input.html +13 -5
- package/src/component/login/otp_input.ts +34 -27
- package/src/component/login/register_component.html +31 -14
- package/src/component/login/register_component.ts +80 -89
- package/src/component/login/social_login.html +2 -2
- package/src/component/login/social_login.ts +20 -13
- package/src/component/navigation/navigation.html +2 -2
- package/src/component/navigation/navigation.ts +2 -0
- package/src/component/oauth_connection/oauth_connection_panel.html +68 -0
- package/src/component/oauth_connection/oauth_connection_panel.ts +252 -0
- package/src/component/payment/user_payment_methods.html +2 -2
- package/src/component/payment/user_payment_methods.ts +12 -23
- package/src/component/payout/bank_info_form.html +4 -4
- package/src/component/payout/bank_info_form.ts +22 -26
- package/src/component/payout/provider_onboarding.html +5 -5
- package/src/component/payout/provider_onboarding.ts +28 -48
- package/src/component/security/trusted_devices.html +1 -1
- package/src/component/security/trusted_devices.ts +0 -1
- package/src/component/security/twofactor_setup.html +8 -4
- package/src/component/security/twofactor_setup.ts +21 -3
- package/src/component/security/twofactor_verify.html +2 -1
- package/src/component/security/twofactor_verify.ts +2 -8
- package/src/component/table/table_detail.html +15 -14
- package/src/component/table/table_detail.ts +62 -41
- package/src/component/table/table_edit.html +15 -11
- package/src/component/table/table_edit.ts +94 -67
- package/src/component/table/table_list.html +45 -12
- package/src/component/table/table_list.ts +98 -26
- package/src/component/table/table_lookup.html +11 -8
- package/src/component/table/table_lookup.ts +45 -37
- package/src/component/table/table_report.html +2 -2
- package/src/component/table/table_report.ts +37 -31
- package/src/component/table/table_search.html +4 -4
- package/src/component/table/table_search.ts +36 -45
- package/src/config.ts +1 -1
- package/src/index.ts +4 -0
- package/src/model/appdata.ts +5 -9
- package/src/model/common.ts +27 -27
- package/src/model/decorator.ts +25 -21
- package/src/model/oauth_connection.ts +50 -0
- package/src/service/api-response.interceptor.ts +4 -1
- package/src/service/auth.interceptor.ts +27 -7
- package/src/service/auth.service.ts +188 -134
- package/src/service/label.service.ts +6 -5
- package/src/service/oauth_connection.service.ts +56 -0
- package/src/service/rest_service.ts +57 -13
- package/src/service/rest_url.ts +23 -7
- package/src/service/script_loader.ts +12 -2
- package/src/util/errors.ts +20 -0
- 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.
|
|
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
|
-
<
|
|
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
|
-
</
|
|
8
|
+
</div>
|
|
9
9
|
|
|
10
|
-
<
|
|
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
|
-
</
|
|
15
|
+
</div>
|
|
16
16
|
|
|
17
17
|
@for (opt of optionalConsents(); track opt.id) {
|
|
18
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
<
|
|
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,
|
|
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 —
|
|
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
|
|
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`
|
|
29
|
-
//
|
|
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
|
-
|
|
60
|
-
this.
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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(
|
|
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>
|
|
33
|
-
<input matInput type="
|
|
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="
|
|
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
|
|