@nauticana/sail 0.7.0 → 0.8.1

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 (68) hide show
  1. package/README.md +419 -15
  2. package/package.json +2 -2
  3. package/src/component/abstract/base_async.ts +8 -2
  4. package/src/component/abstract/base_table.ts +108 -49
  5. package/src/component/abstract/base_view.ts +32 -45
  6. package/src/component/billing/checkout_button.html +1 -1
  7. package/src/component/billing/checkout_button.ts +1 -1
  8. package/src/component/billing/payment_methods.ts +1 -1
  9. package/src/component/billing/plan_selector.html +1 -1
  10. package/src/component/billing/plan_selector.ts +1 -1
  11. package/src/component/form/form_field.html +1 -1
  12. package/src/component/form/form_field.ts +8 -7
  13. package/src/component/form/form_record.html +2 -2
  14. package/src/component/form/form_record.ts +1 -1
  15. package/src/component/form/form_table.html +4 -4
  16. package/src/component/form/form_table.ts +2 -2
  17. package/src/component/login/account_deletion.html +1 -1
  18. package/src/component/login/account_deletion.ts +5 -2
  19. package/src/component/login/chpass_component.html +1 -1
  20. package/src/component/login/chpass_component.ts +11 -16
  21. package/src/component/login/confirm_chpass_component.html +1 -1
  22. package/src/component/login/confirm_chpass_component.ts +9 -15
  23. package/src/component/login/confirm_register_component.html +1 -1
  24. package/src/component/login/confirm_register_component.ts +9 -15
  25. package/src/component/login/consent_gate.ts +1 -1
  26. package/src/component/login/login_component.html +2 -2
  27. package/src/component/login/login_component.ts +1 -1
  28. package/src/component/login/otp_input.html +3 -3
  29. package/src/component/login/otp_input.ts +1 -1
  30. package/src/component/login/register_component.html +1 -1
  31. package/src/component/login/register_component.ts +1 -1
  32. package/src/component/login/social_login.ts +1 -1
  33. package/src/component/navigation/navigation.html +3 -3
  34. package/src/component/navigation/navigation.ts +10 -11
  35. package/src/component/payment/user_payment_methods.html +46 -0
  36. package/src/component/payment/user_payment_methods.ts +10 -58
  37. package/src/component/payout/bank_info_form.html +59 -0
  38. package/src/component/payout/bank_info_form.ts +21 -77
  39. package/src/component/payout/provider_onboarding.html +70 -0
  40. package/src/component/payout/provider_onboarding.ts +9 -80
  41. package/src/component/security/trusted_devices.html +4 -4
  42. package/src/component/security/trusted_devices.ts +1 -1
  43. package/src/component/security/twofactor_setup.html +4 -4
  44. package/src/component/security/twofactor_setup.ts +18 -26
  45. package/src/component/security/twofactor_verify.html +1 -1
  46. package/src/component/security/twofactor_verify.ts +10 -12
  47. package/src/component/table/table_detail.html +12 -12
  48. package/src/component/table/table_detail.ts +28 -33
  49. package/src/component/table/table_edit.html +20 -10
  50. package/src/component/table/table_edit.ts +29 -18
  51. package/src/component/table/table_list.html +5 -5
  52. package/src/component/table/table_list.ts +24 -48
  53. package/src/component/table/table_lookup.html +4 -4
  54. package/src/component/table/table_lookup.ts +11 -12
  55. package/src/component/table/table_report.html +1 -1
  56. package/src/component/table/table_report.ts +11 -14
  57. package/src/component/table/table_search.html +5 -5
  58. package/src/component/table/table_search.ts +31 -42
  59. package/src/index.ts +2 -0
  60. package/src/model/appdata.ts +2 -2
  61. package/src/model/common.ts +39 -1
  62. package/src/service/auth.service.ts +26 -26
  63. package/src/service/base_rest.service.ts +20 -0
  64. package/src/service/billing.service.ts +3 -9
  65. package/src/service/payout.service.ts +3 -9
  66. package/src/service/rest_service.ts +5 -5
  67. package/src/service/user_payment_method.service.ts +4 -8
  68. package/src/util/text.ts +19 -0
@@ -7,9 +7,10 @@ import { MatFormFieldModule } from '@angular/material/form-field';
7
7
  import { MatInputModule } from '@angular/material/input';
8
8
  import { MatCheckboxModule } from '@angular/material/checkbox';
9
9
  import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
10
+ import { BaseAsync } from '../abstract/base_async';
10
11
 
11
12
  @Component({
12
- selector: 'app-chpass',
13
+ selector: 'sail-chpass',
13
14
  templateUrl: './chpass_component.html',
14
15
  changeDetection: ChangeDetectionStrategy.OnPush,
15
16
  encapsulation: ViewEncapsulation.None,
@@ -22,15 +23,13 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
22
23
  RouterLink,
23
24
  ],
24
25
  })
25
- export class ChpassComponent {
26
+ export class ChpassComponent extends BaseAsync {
26
27
  private auth = inject(BaseAuthService);
27
28
  private fb = inject(FormBuilder);
28
29
  private router = inject(Router);
29
30
  protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
30
31
 
31
32
  forgotPassword = signal(false);
32
- errorMessage = signal('');
33
- successMessage = signal('');
34
33
 
35
34
  chpassForm = this.fb.group({
36
35
  username: ['', Validators.required],
@@ -62,9 +61,6 @@ export class ChpassComponent {
62
61
  }
63
62
 
64
63
  changePassword(): void {
65
- this.errorMessage.set('');
66
- this.successMessage.set('');
67
-
68
64
  if (this.chpassForm.invalid) return;
69
65
 
70
66
  const { username, old_password, new_password, confirm_password } = this.chpassForm.value;
@@ -74,23 +70,22 @@ export class ChpassComponent {
74
70
  return;
75
71
  }
76
72
 
77
- const request = this.forgotPassword()
73
+ const isForgot = this.forgotPassword();
74
+ const request = isForgot
78
75
  ? this.auth.forgotPassword(username!)
79
76
  : this.auth.chpass(username!, new_password!, old_password!);
80
77
 
81
- request.subscribe({
82
- next: () => {
83
- if (this.forgotPassword()) {
78
+ this.run(
79
+ request,
80
+ () => {
81
+ if (isForgot) {
84
82
  this.successMessage.set('A confirmation code has been sent to your email.');
85
83
  this.router.navigate(['/confirm/password'], { queryParams: { username } });
86
84
  } else {
87
85
  this.successMessage.set('Password changed successfully.');
88
86
  }
89
87
  },
90
- error: (err) => {
91
- const msg = typeof err.error === 'string' ? err.error : err.error?.message;
92
- this.errorMessage.set(msg || 'Password change failed. Please try again.');
93
- },
94
- });
88
+ 'Password change failed. Please try again.',
89
+ );
95
90
  }
96
91
  }
@@ -36,7 +36,7 @@
36
36
  <div class="auth-success" role="status">{{ successMessage() }}</div>
37
37
  }
38
38
 
39
- <button mat-raised-button class="primary" type="submit" [disabled]="!confirmForm.valid">
39
+ <button matButton="elevated" class="primary" type="submit" [disabled]="!confirmForm.valid">
40
40
  Confirm Password Change
41
41
  </button>
42
42
  </form>
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component, inject, signal, OnInit, ViewEncapsulation } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
2
2
  import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3
3
  import { BaseAuthService } from '../../service/auth.service';
4
4
  import { ActivatedRoute, Router, RouterLink } from '@angular/router';
@@ -6,9 +6,10 @@ import { MatButtonModule } from '@angular/material/button';
6
6
  import { MatFormFieldModule } from '@angular/material/form-field';
7
7
  import { MatInputModule } from '@angular/material/input';
8
8
  import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
9
+ import { BaseAsync } from '../abstract/base_async';
9
10
 
10
11
  @Component({
11
- selector: 'app-confirm-chpass',
12
+ selector: 'sail-confirm-chpass',
12
13
  templateUrl: './confirm_chpass_component.html',
13
14
  changeDetection: ChangeDetectionStrategy.OnPush,
14
15
  encapsulation: ViewEncapsulation.None,
@@ -20,16 +21,13 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
20
21
  RouterLink,
21
22
  ],
22
23
  })
23
- export class ConfirmChpassComponent implements OnInit {
24
+ export class ConfirmChpassComponent extends BaseAsync implements OnInit {
24
25
  private auth = inject(BaseAuthService);
25
26
  private fb = inject(FormBuilder);
26
27
  private router = inject(Router);
27
28
  private route = inject(ActivatedRoute);
28
29
  protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
29
30
 
30
- errorMessage = signal('');
31
- successMessage = signal('');
32
-
33
31
  confirmForm = this.fb.group({
34
32
  username: ['', Validators.required],
35
33
  code: ['', Validators.required],
@@ -45,9 +43,6 @@ export class ConfirmChpassComponent implements OnInit {
45
43
  }
46
44
 
47
45
  confirm(): void {
48
- this.errorMessage.set('');
49
- this.successMessage.set('');
50
-
51
46
  if (this.confirmForm.invalid) return;
52
47
 
53
48
  const { username, code, new_password, confirm_password } = this.confirmForm.value;
@@ -57,14 +52,13 @@ export class ConfirmChpassComponent implements OnInit {
57
52
  return;
58
53
  }
59
54
 
60
- this.auth.confirmChpass(username!, code!, new_password!).subscribe({
61
- next: () => {
55
+ this.run(
56
+ this.auth.confirmChpass(username!, code!, new_password!),
57
+ () => {
62
58
  this.successMessage.set('Password changed successfully. Redirecting to login...');
63
59
  this.router.navigate(['/login/local']);
64
60
  },
65
- error: (err) => {
66
- this.errorMessage.set(err.error?.message ?? 'Confirmation failed. Please try again.');
67
- },
68
- });
61
+ 'Confirmation failed. Please try again.',
62
+ );
69
63
  }
70
64
  }
@@ -26,7 +26,7 @@
26
26
  <div class="auth-success" role="status">{{ successMessage() }}</div>
27
27
  }
28
28
 
29
- <button mat-raised-button class="primary" type="submit" [disabled]="!confirmForm.valid">
29
+ <button matButton="elevated" class="primary" type="submit" [disabled]="!confirmForm.valid">
30
30
  Confirm
31
31
  </button>
32
32
  </form>
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component, inject, signal, OnInit, ViewEncapsulation } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
2
2
  import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3
3
  import { BaseAuthService } from '../../service/auth.service';
4
4
  import { ActivatedRoute, Router, RouterLink } from '@angular/router';
@@ -6,9 +6,10 @@ import { MatButtonModule } from '@angular/material/button';
6
6
  import { MatFormFieldModule } from '@angular/material/form-field';
7
7
  import { MatInputModule } from '@angular/material/input';
8
8
  import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
9
+ import { BaseAsync } from '../abstract/base_async';
9
10
 
10
11
  @Component({
11
- selector: 'app-confirm-register',
12
+ selector: 'sail-confirm-register',
12
13
  templateUrl: './confirm_register_component.html',
13
14
  changeDetection: ChangeDetectionStrategy.OnPush,
14
15
  encapsulation: ViewEncapsulation.None,
@@ -20,16 +21,13 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
20
21
  RouterLink,
21
22
  ],
22
23
  })
23
- export class ConfirmRegisterComponent implements OnInit {
24
+ export class ConfirmRegisterComponent extends BaseAsync implements OnInit {
24
25
  private auth = inject(BaseAuthService);
25
26
  private fb = inject(FormBuilder);
26
27
  private router = inject(Router);
27
28
  private route = inject(ActivatedRoute);
28
29
  protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
29
30
 
30
- errorMessage = signal('');
31
- successMessage = signal('');
32
-
33
31
  confirmForm = this.fb.group({
34
32
  email: ['', [Validators.required, Validators.email]],
35
33
  code: ['', Validators.required],
@@ -43,15 +41,13 @@ export class ConfirmRegisterComponent implements OnInit {
43
41
  }
44
42
 
45
43
  confirm(): void {
46
- this.errorMessage.set('');
47
- this.successMessage.set('');
48
-
49
44
  if (this.confirmForm.invalid) return;
50
45
 
51
46
  const { email, code } = this.confirmForm.value;
52
47
 
53
- this.auth.confirmRegister(email!, code!).subscribe({
54
- next: (resp) => {
48
+ this.run(
49
+ this.auth.confirmRegister(email!, code!),
50
+ (resp) => {
55
51
  this.successMessage.set('Registration confirmed.');
56
52
  if (resp?.paymentRequired && resp.paymentUrl) {
57
53
  // Paid plan: go straight to payment. Subscription is 'P' until paid.
@@ -63,9 +59,7 @@ export class ConfirmRegisterComponent implements OnInit {
63
59
  // Free plan: land on login.
64
60
  this.router.navigate(['/login/local']);
65
61
  },
66
- error: (err) => {
67
- this.errorMessage.set(err.error?.message ?? 'Confirmation failed. Please try again.');
68
- },
69
- });
62
+ 'Confirmation failed. Please try again.',
63
+ );
70
64
  }
71
65
  }
@@ -22,7 +22,7 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
22
22
  * See keel/README.md → "Consent Capture" for the backend-side contract.
23
23
  */
24
24
  @Component({
25
- selector: 'app-consent-gate',
25
+ selector: 'sail-consent-gate',
26
26
  templateUrl: './consent_gate.html',
27
27
  changeDetection: ChangeDetectionStrategy.OnPush,
28
28
  encapsulation: ViewEncapsulation.None,
@@ -18,13 +18,13 @@
18
18
  <mat-label>Password</mat-label>
19
19
  <input matInput type="password" formControlName="password" required>
20
20
  </mat-form-field>
21
- <button mat-raised-button class="primary" type="submit" [disabled]="!loginForm.valid">
21
+ <button matButton="elevated" class="primary" type="submit" [disabled]="!loginForm.valid">
22
22
  Login
23
23
  </button>
24
24
  </form>
25
25
  @if (showSocialLogin()) {
26
26
  <hr class="auth-divider">
27
- <app-social-login
27
+ <sail-social-login
28
28
  [providers]="enabledSocialProviders()"
29
29
  (loginSuccess)="onSocialSuccess($event)"
30
30
  (loginError)="onSocialError($event)" />
@@ -10,7 +10,7 @@ import { LoginResponseSocial, SocialProvider } from '../../model/auth';
10
10
  import { SocialLoginComponent } from './social_login';
11
11
 
12
12
  @Component({
13
- selector: 'app-login',
13
+ selector: 'sail-login',
14
14
  templateUrl: './login_component.html',
15
15
  changeDetection: ChangeDetectionStrategy.OnPush,
16
16
  encapsulation: ViewEncapsulation.None,
@@ -16,7 +16,7 @@
16
16
 
17
17
  <p class="otp-resend">
18
18
  @if (canResend()) {
19
- <button mat-button class="resend-btn" type="button" (click)="onResend()">Resend code</button>
19
+ <button matButton class="resend-btn" type="button" (click)="onResend()">Resend code</button>
20
20
  } @else {
21
21
  Resend code in {{ countdown() }}s
22
22
  }
@@ -27,11 +27,11 @@
27
27
  @if (key === '') {
28
28
  <div class="keypad-spacer"></div>
29
29
  } @else if (key === 'back') {
30
- <button mat-button class="keypad-key" type="button" (click)="onBackspace()" aria-label="Backspace" [disabled]="disabled()">
30
+ <button matButton class="keypad-key" type="button" (click)="onBackspace()" aria-label="Backspace" [disabled]="disabled()">
31
31
  <mat-icon>backspace</mat-icon>
32
32
  </button>
33
33
  } @else {
34
- <button mat-button class="keypad-key" type="button" (click)="onKeyPress(key)" [disabled]="disabled()">{{ key }}</button>
34
+ <button matButton class="keypad-key" type="button" (click)="onKeyPress(key)" [disabled]="disabled()">{{ key }}</button>
35
35
  }
36
36
  }
37
37
  </div>
@@ -13,7 +13,7 @@ import { MatIconModule } from '@angular/material/icon';
13
13
  * call verifyOtp / resendOtp. See README "Phone / email OTP login".
14
14
  */
15
15
  @Component({
16
- selector: 'app-otp-input',
16
+ selector: 'sail-otp-input',
17
17
  templateUrl: './otp_input.html',
18
18
  changeDetection: ChangeDetectionStrategy.OnPush,
19
19
  encapsulation: ViewEncapsulation.None,
@@ -120,7 +120,7 @@
120
120
  <div class="geocode-status geocode-error" role="alert">{{ registerError() }}</div>
121
121
  }
122
122
 
123
- <button mat-raised-button class="primary" type="submit"
123
+ <button matButton="elevated" class="primary" type="submit"
124
124
  [disabled]="!registerForm.valid || geocodeStatus() !== 'success'">
125
125
  Create Account
126
126
  </button>
@@ -15,7 +15,7 @@ import { PartnerRegistration, PublicPlan } from '../../model/appdata';
15
15
  import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
16
16
 
17
17
  @Component({
18
- selector: 'app-register',
18
+ selector: 'sail-register',
19
19
  templateUrl: './register_component.html',
20
20
  changeDetection: ChangeDetectionStrategy.OnPush,
21
21
  encapsulation: ViewEncapsulation.None,
@@ -27,7 +27,7 @@ const APPLE_URL = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/
27
27
  * without config are hidden (with a console.warn for diagnostics).
28
28
  */
29
29
  @Component({
30
- selector: 'app-social-login',
30
+ selector: 'sail-social-login',
31
31
  templateUrl: './social_login.html',
32
32
  changeDetection: ChangeDetectionStrategy.OnPush,
33
33
  encapsulation: ViewEncapsulation.None,
@@ -29,7 +29,7 @@
29
29
  <mat-sidenav-content>
30
30
  <mat-toolbar class="primary">
31
31
  @if (isHandset()) {
32
- <button type="button" aria-label="Toggle sidenav" mat-icon-button (click)="sidenav.toggle()">
32
+ <button type="button" aria-label="Toggle sidenav" matIconButton (click)="sidenav.toggle()">
33
33
  <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
34
34
  </button>
35
35
  }
@@ -37,10 +37,10 @@
37
37
  <span class="toolbar-spacer"></span>
38
38
  <ng-content select="[toolbar-actions]"></ng-content>
39
39
  @if (isLoggedIn()) {
40
- <button mat-button (click)="logout()">Logout</button>
40
+ <button matButton (click)="logout()">Logout</button>
41
41
  } @else if (guiConfig.publicRouteLinks) {
42
42
  @for (link of guiConfig.publicRouteLinks; track link.routerLink) {
43
- <a mat-button [routerLink]="link.routerLink">{{ link.label }}</a>
43
+ <a matButton [routerLink]="link.routerLink">{{ link.label }}</a>
44
44
  }
45
45
  }
46
46
  </mat-toolbar>
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal, ViewEncapsulation } from "@angular/core";
1
+ import { ChangeDetectionStrategy, Component, computed, inject, ViewEncapsulation } from "@angular/core";
2
2
  import { toSignal } from "@angular/core/rxjs-interop";
3
3
  import { RouterLink, RouterOutlet } from "@angular/router";
4
4
  import { MatSidenavModule } from '@angular/material/sidenav';
@@ -14,7 +14,7 @@ import { BaseAuthService } from "../../service/auth.service";
14
14
  import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
15
15
 
16
16
  @Component({
17
- selector: 'app-navigation',
17
+ selector: 'sail-navigation',
18
18
  templateUrl: './navigation.html',
19
19
  changeDetection: ChangeDetectionStrategy.OnPush,
20
20
  encapsulation: ViewEncapsulation.None,
@@ -29,16 +29,20 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
29
29
  MatExpansionModule,
30
30
  ],
31
31
  })
32
- export class Navigation implements OnInit {
32
+ export class Navigation {
33
33
  private readonly authService = inject(BaseAuthService);
34
34
  private readonly breakpointObserver = inject(BreakpointObserver);
35
35
  protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
36
36
 
37
- private readonly allMenuItems = signal<ApplicationMenu[]>([]);
37
+ // Restore the stored JWT before the menu stream fires so `canAccess()` checks
38
+ // resolve against the right user. Done at field-init time (runs in injection
39
+ // context) rather than ngOnInit, so the toSignal below can read from it.
40
+ private readonly _sessionLoaded = (this.authService.loadStoredSession(), true);
41
+
42
+ private readonly allMenuItems = toSignal(this.authService.getMenus(), { initialValue: [] as ApplicationMenu[] });
38
43
 
39
44
  readonly menuItems = computed(() => {
40
- const menus = this.allMenuItems();
41
- return menus
45
+ return this.allMenuItems()
42
46
  .map(menu => ({
43
47
  ...menu,
44
48
  ApplicationMenuItems: (menu.ApplicationMenuItems ?? [])
@@ -54,11 +58,6 @@ export class Navigation implements OnInit {
54
58
 
55
59
  readonly isLoggedIn = this.authService.isLoggedIn;
56
60
 
57
- ngOnInit() {
58
- this.authService.loadStoredSession();
59
- this.authService.getMenus().subscribe((menuItems: ApplicationMenu[]) => { this.allMenuItems.set(menuItems); });
60
- }
61
-
62
61
  logout() {
63
62
  this.authService.logout();
64
63
  }
@@ -0,0 +1,46 @@
1
+ <div class="user-payment-methods">
2
+ <h2 class="user-payment-methods__title">{{ title() }}</h2>
3
+
4
+ @if (showAdd()) {
5
+ <button matButton="elevated"
6
+ class="user-payment-methods__add accent"
7
+ (click)="addClicked.emit()">
8
+ <mat-icon>add</mat-icon> {{ addLabel() }}
9
+ </button>
10
+ }
11
+
12
+ @if (methods().length === 0 && !loading()) {
13
+ <p class="user-payment-methods__empty">{{ emptyLabel() }}</p>
14
+ }
15
+
16
+ @for (m of methods(); track m.Id) {
17
+ <mat-card class="user-payment-methods__item">
18
+ <mat-card-content>
19
+ <div class="user-payment-methods__row">
20
+ <div class="user-payment-methods__info">
21
+ <mat-icon>{{ iconFor(m) }}</mat-icon>
22
+ <span class="user-payment-methods__brand">{{ m.Brand || m.MethodType }}</span>
23
+ @if (m.LastFour) {
24
+ <span class="user-payment-methods__last4">•••• {{ m.LastFour }}</span>
25
+ }
26
+ @if (m.IsDefault) {
27
+ <span class="user-payment-methods__default">Default</span>
28
+ }
29
+ </div>
30
+ <div class="user-payment-methods__actions">
31
+ @if (!m.IsDefault) {
32
+ <button matButton (click)="setDefault(m)">Set default</button>
33
+ }
34
+ <button matIconButton (click)="remove(m)" aria-label="Delete payment method">
35
+ <mat-icon>delete</mat-icon>
36
+ </button>
37
+ </div>
38
+ </div>
39
+ </mat-card-content>
40
+ </mat-card>
41
+ }
42
+
43
+ @if (errorMsg()) {
44
+ <p class="user-payment-methods__error">{{ errorMsg() }}</p>
45
+ }
46
+ </div>
@@ -1,5 +1,4 @@
1
- import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation, inject, signal } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
1
+ import { ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation, inject, input, output, signal } from '@angular/core';
3
2
  import { MatButtonModule } from '@angular/material/button';
4
3
  import { MatCardModule } from '@angular/material/card';
5
4
  import { MatIconModule } from '@angular/material/icon';
@@ -23,55 +22,8 @@ import { UserPaymentMethod } from '../../model/appdata';
23
22
  selector: 'sail-user-payment-methods',
24
23
  changeDetection: ChangeDetectionStrategy.OnPush,
25
24
  encapsulation: ViewEncapsulation.None,
26
- imports: [CommonModule, MatButtonModule, MatCardModule, MatIconModule],
27
- template: `
28
- <div class="user-payment-methods">
29
- <h2 class="user-payment-methods__title">{{ title }}</h2>
30
-
31
- @if (showAdd) {
32
- <button mat-raised-button color="accent"
33
- class="user-payment-methods__add"
34
- (click)="addClicked.emit()">
35
- <mat-icon>add</mat-icon> {{ addLabel }}
36
- </button>
37
- }
38
-
39
- @if (methods().length === 0 && !loading()) {
40
- <p class="user-payment-methods__empty">{{ emptyLabel }}</p>
41
- }
42
-
43
- @for (m of methods(); track m.Id) {
44
- <mat-card class="user-payment-methods__item">
45
- <mat-card-content>
46
- <div class="user-payment-methods__row">
47
- <div class="user-payment-methods__info">
48
- <mat-icon>{{ iconFor(m) }}</mat-icon>
49
- <span class="user-payment-methods__brand">{{ m.Brand || m.MethodType }}</span>
50
- @if (m.LastFour) {
51
- <span class="user-payment-methods__last4">•••• {{ m.LastFour }}</span>
52
- }
53
- @if (m.IsDefault) {
54
- <span class="user-payment-methods__default">Default</span>
55
- }
56
- </div>
57
- <div class="user-payment-methods__actions">
58
- @if (!m.IsDefault) {
59
- <button mat-button (click)="setDefault(m)">Set default</button>
60
- }
61
- <button mat-icon-button (click)="remove(m)" aria-label="Delete payment method">
62
- <mat-icon>delete</mat-icon>
63
- </button>
64
- </div>
65
- </div>
66
- </mat-card-content>
67
- </mat-card>
68
- }
69
-
70
- @if (errorMsg()) {
71
- <p class="user-payment-methods__error">{{ errorMsg() }}</p>
72
- }
73
- </div>
74
- `,
25
+ imports: [MatButtonModule, MatCardModule, MatIconModule],
26
+ templateUrl: './user_payment_methods.html',
75
27
  styles: `
76
28
  .user-payment-methods { display: flex; flex-direction: column; gap: 12px; }
77
29
  .user-payment-methods__title { margin: 0; }
@@ -88,17 +40,17 @@ import { UserPaymentMethod } from '../../model/appdata';
88
40
  `,
89
41
  })
90
42
  export class UserPaymentMethodsComponent extends BaseAsync implements OnInit {
91
- @Input() title = 'Payment Methods';
92
- @Input() addLabel = 'Add Payment Method';
93
- @Input() emptyLabel = 'No saved payment methods yet.';
94
- @Input() showAdd = true;
43
+ readonly title = input('Payment Methods');
44
+ readonly addLabel = input('Add Payment Method');
45
+ readonly emptyLabel = input('No saved payment methods yet.');
46
+ readonly showAdd = input(true);
95
47
 
96
48
  /** Emitted when the "Add" button is tapped. Consumer routes to its SetupIntent flow. */
97
- @Output() addClicked = new EventEmitter<void>();
49
+ readonly addClicked = output<void>();
98
50
  /** Emitted after a successful set-default flip. */
99
- @Output() defaultChanged = new EventEmitter<UserPaymentMethod>();
51
+ readonly defaultChanged = output<UserPaymentMethod>();
100
52
  /** Emitted after a successful delete. */
101
- @Output() deleted = new EventEmitter<UserPaymentMethod>();
53
+ readonly deleted = output<UserPaymentMethod>();
102
54
 
103
55
  private readonly service = inject(UserPaymentMethodService);
104
56
 
@@ -0,0 +1,59 @@
1
+ <div class="bank-info-form">
2
+ <h2 class="bank-info-form__title">{{ title() }}</h2>
3
+
4
+ @if (intro()) {
5
+ <p class="bank-info-form__intro">{{ intro() }}</p>
6
+ }
7
+
8
+ <form [formGroup]="form" (ngSubmit)="submit()">
9
+ <mat-form-field appearance="outline" class="full-width">
10
+ <mat-label>Country *</mat-label>
11
+ <mat-select formControlName="countryCode">
12
+ @for (c of countryProfiles(); track c.code) {
13
+ <mat-option [value]="c.code">{{ c.label }} ({{ c.currency }})</mat-option>
14
+ }
15
+ </mat-select>
16
+ </mat-form-field>
17
+ <p class="hint">Sets your tax ID format + payout currency</p>
18
+
19
+ <mat-form-field appearance="outline" class="full-width">
20
+ <mat-label>Account holder name *</mat-label>
21
+ <input matInput formControlName="accountHolderName">
22
+ </mat-form-field>
23
+ <p class="hint">Legal name on the bank account (must match government ID)</p>
24
+
25
+ <mat-form-field appearance="outline" class="full-width">
26
+ <mat-label>{{ taxIdLabel() }} *</mat-label>
27
+ <input matInput formControlName="taxId" [placeholder]="taxIdPlaceholder()">
28
+ </mat-form-field>
29
+ <p class="hint">{{ taxIdHint() }}</p>
30
+
31
+ <mat-form-field appearance="outline" class="full-width">
32
+ <mat-label>Billing address *</mat-label>
33
+ <input matInput formControlName="billingAddress">
34
+ </mat-form-field>
35
+ <p class="hint">Business or home address — used for tax correspondence</p>
36
+
37
+ <p class="provider-note">
38
+ Bank routing details (account number + institution / ABA /
39
+ IBAN) are collected by the payout provider in the next step;
40
+ this application does not store them.
41
+ </p>
42
+
43
+ <mat-checkbox formControlName="providerAgreement" class="agreement">
44
+ {{ agreementLabel() }}
45
+ </mat-checkbox>
46
+
47
+ <div class="nav-buttons">
48
+ @if (showBack()) {
49
+ <button matButton="outlined" class="pill-btn" type="button" (click)="back.emit()">
50
+ &lt; Back
51
+ </button>
52
+ }
53
+ <button matButton="filled" class="pill-btn primary-btn" type="submit"
54
+ [disabled]="form.invalid">
55
+ {{ submitLabel() }}
56
+ </button>
57
+ </div>
58
+ </form>
59
+ </div>