@nauticana/sail 0.6.1 → 0.8.0

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 (43) hide show
  1. package/README.md +300 -1
  2. package/package.json +2 -2
  3. package/src/component/abstract/base_table.ts +59 -17
  4. package/src/component/abstract/base_view.ts +9 -9
  5. package/src/component/billing/checkout_button.html +1 -1
  6. package/src/component/billing/plan_selector.html +1 -1
  7. package/src/component/form/form_field.html +1 -1
  8. package/src/component/form/form_field.ts +7 -3
  9. package/src/component/form/form_table.html +4 -4
  10. package/src/component/form/form_table.ts +1 -1
  11. package/src/component/login/account_deletion.html +1 -1
  12. package/src/component/login/account_deletion.ts +4 -1
  13. package/src/component/login/chpass_component.html +1 -1
  14. package/src/component/login/confirm_chpass_component.html +1 -1
  15. package/src/component/login/confirm_register_component.html +1 -1
  16. package/src/component/login/login_component.html +1 -1
  17. package/src/component/login/otp_input.html +3 -3
  18. package/src/component/login/register_component.html +1 -1
  19. package/src/component/navigation/navigation.html +3 -3
  20. package/src/component/payment/user_payment_methods.html +46 -0
  21. package/src/component/payment/user_payment_methods.ts +10 -58
  22. package/src/component/payout/bank_info_form.html +59 -0
  23. package/src/component/payout/bank_info_form.ts +21 -77
  24. package/src/component/payout/provider_onboarding.html +70 -0
  25. package/src/component/payout/provider_onboarding.ts +9 -80
  26. package/src/component/security/trusted_devices.html +4 -4
  27. package/src/component/security/twofactor_setup.html +4 -4
  28. package/src/component/security/twofactor_verify.html +1 -1
  29. package/src/component/table/table_detail.html +28 -7
  30. package/src/component/table/table_detail.ts +40 -3
  31. package/src/component/table/table_edit.html +8 -8
  32. package/src/component/table/table_edit.ts +25 -14
  33. package/src/component/table/table_list.html +26 -5
  34. package/src/component/table/table_list.ts +41 -7
  35. package/src/component/table/table_lookup.html +4 -4
  36. package/src/component/table/table_lookup.ts +10 -9
  37. package/src/component/table/table_report.html +1 -1
  38. package/src/component/table/table_search.html +14 -4
  39. package/src/component/table/table_search.ts +55 -17
  40. package/src/model/appdata.ts +20 -6
  41. package/src/service/auth.service.ts +9 -0
  42. package/src/service/rest_service.ts +12 -0
  43. package/src/service/rest_url.ts +4 -3
@@ -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>
@@ -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>
@@ -18,7 +18,7 @@
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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -1,4 +1,5 @@
1
- import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, inject, signal } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal } from '@angular/core';
2
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2
3
  import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3
4
  import { MatButtonModule } from '@angular/material/button';
4
5
  import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -40,67 +41,7 @@ export const DEFAULT_COUNTRY_PROFILES: CountryProfile[] = [
40
41
  ReactiveFormsModule, MatButtonModule, MatCheckboxModule,
41
42
  MatFormFieldModule, MatInputModule, MatSelectModule,
42
43
  ],
43
- template: `
44
- <div class="bank-info-form">
45
- <h2 class="bank-info-form__title">{{ title }}</h2>
46
-
47
- @if (intro) {
48
- <p class="bank-info-form__intro">{{ intro }}</p>
49
- }
50
-
51
- <form [formGroup]="form" (ngSubmit)="submit()">
52
- <mat-form-field appearance="outline" class="full-width">
53
- <mat-label>Country *</mat-label>
54
- <mat-select formControlName="countryCode">
55
- @for (c of countryProfiles; track c.code) {
56
- <mat-option [value]="c.code">{{ c.label }} ({{ c.currency }})</mat-option>
57
- }
58
- </mat-select>
59
- </mat-form-field>
60
- <p class="hint">Sets your tax ID format + payout currency</p>
61
-
62
- <mat-form-field appearance="outline" class="full-width">
63
- <mat-label>Account holder name *</mat-label>
64
- <input matInput formControlName="accountHolderName">
65
- </mat-form-field>
66
- <p class="hint">Legal name on the bank account (must match government ID)</p>
67
-
68
- <mat-form-field appearance="outline" class="full-width">
69
- <mat-label>{{ taxIdLabel() }} *</mat-label>
70
- <input matInput formControlName="taxId" [placeholder]="taxIdPlaceholder()">
71
- </mat-form-field>
72
- <p class="hint">{{ taxIdHint() }}</p>
73
-
74
- <mat-form-field appearance="outline" class="full-width">
75
- <mat-label>Billing address *</mat-label>
76
- <input matInput formControlName="billingAddress">
77
- </mat-form-field>
78
- <p class="hint">Business or home address — used for tax correspondence</p>
79
-
80
- <p class="provider-note">
81
- Bank routing details (account number + institution / ABA /
82
- IBAN) are collected by the payout provider in the next step;
83
- this application does not store them.
84
- </p>
85
-
86
- <mat-checkbox formControlName="providerAgreement" class="agreement">
87
- {{ agreementLabel }}
88
- </mat-checkbox>
89
-
90
- <div class="nav-buttons">
91
- @if (showBack) {
92
- <button mat-stroked-button class="pill-btn" type="button" (click)="back.emit()">
93
- &lt; Back
94
- </button>
95
- }
96
- <button mat-flat-button class="pill-btn primary-btn" type="submit"
97
- [disabled]="form.invalid">
98
- {{ submitLabel }}
99
- </button>
100
- </div>
101
- </form>
102
- </div>
103
- `,
44
+ templateUrl: './bank_info_form.html',
104
45
  styles: `
105
46
  .bank-info-form { display: flex; flex-direction: column; gap: 4px; padding: 0 24px 32px; }
106
47
  .bank-info-form__title { margin: 0 0 16px; }
@@ -116,24 +57,24 @@ export const DEFAULT_COUNTRY_PROFILES: CountryProfile[] = [
116
57
  })
117
58
  export class PayoutBankInfoFormComponent {
118
59
  /** Step title rendered above the form. */
119
- @Input() title = 'Payment and Tax';
60
+ readonly title = input('Payment and Tax');
120
61
  /** Optional explainer rendered below the title. */
121
- @Input() intro = 'Tax + payout details. Bank account itself is set up with the payout provider in a separate step — only tax-reporting and dispute-correspondence fields are collected here.';
62
+ readonly intro = input('Tax + payout details. Bank account itself is set up with the payout provider in a separate step — only tax-reporting and dispute-correspondence fields are collected here.');
122
63
  /** Override the list of supported countries — keep the default for most apps. */
123
- @Input() countryProfiles: CountryProfile[] = DEFAULT_COUNTRY_PROFILES;
64
+ readonly countryProfiles = input<CountryProfile[]>(DEFAULT_COUNTRY_PROFILES);
124
65
  /** Provider code persisted on user_bank_info.provider. Default AW (Airwallex) matches keel's default --payout_provider. */
125
- @Input() provider = 'AW';
66
+ readonly provider = input('AW');
126
67
  /** Wording on the provider-agreement checkbox. Override to name the live provider. */
127
- @Input() agreementLabel = 'I agree to the payout provider account agreement';
68
+ readonly agreementLabel = input('I agree to the payout provider account agreement');
128
69
  /** Submit-button label — wizards usually use "Next >". */
129
- @Input() submitLabel = 'Next >';
70
+ readonly submitLabel = input('Next >');
130
71
  /** When true, the back button renders alongside submit. */
131
- @Input() showBack = true;
72
+ readonly showBack = input(true);
132
73
 
133
74
  /** Emitted on submit; payload maps 1:1 to basis user_bank_info columns. */
134
- @Output() submitted = new EventEmitter<BankInfoFormValue>();
75
+ readonly submitted = output<BankInfoFormValue>();
135
76
  /** Emitted when the user clicks Back. */
136
- @Output() back = new EventEmitter<void>();
77
+ readonly back = output<void>();
137
78
 
138
79
  private readonly fb = inject(FormBuilder);
139
80
 
@@ -145,17 +86,20 @@ export class PayoutBankInfoFormComponent {
145
86
  providerAgreement: [false, Validators.requiredTrue],
146
87
  });
147
88
 
148
- private readonly selectedCountry = signal<CountryProfile>(this.countryProfiles[0]);
89
+ private readonly selectedCountry = signal<CountryProfile>(this.countryProfiles()[0]);
149
90
 
150
91
  readonly taxIdLabel = computed(() => this.selectedCountry().taxIdLabel);
151
92
  readonly taxIdPlaceholder = computed(() => this.selectedCountry().taxIdPlaceholder);
152
93
  readonly taxIdHint = computed(() => this.selectedCountry().taxIdHint);
153
94
 
154
95
  constructor() {
155
- this.form.controls.countryCode.valueChanges.subscribe((code) => {
156
- const profile = this.countryProfiles.find((c) => c.code === code) ?? this.countryProfiles[0];
157
- this.selectedCountry.set(profile);
158
- });
96
+ this.form.controls.countryCode.valueChanges
97
+ .pipe(takeUntilDestroyed())
98
+ .subscribe((code) => {
99
+ const profiles = this.countryProfiles();
100
+ const profile = profiles.find((c) => c.code === code) ?? profiles[0];
101
+ this.selectedCountry.set(profile);
102
+ });
159
103
  }
160
104
 
161
105
  submit() {
@@ -169,7 +113,7 @@ export class PayoutBankInfoFormComponent {
169
113
  billingAddress: v.billingAddress!,
170
114
  taxIdType: country.taxIdType,
171
115
  taxId: v.taxId!,
172
- provider: this.provider,
116
+ provider: this.provider(),
173
117
  providerAgreement: v.providerAgreement!,
174
118
  });
175
119
  }
@@ -0,0 +1,70 @@
1
+ <div class="payout-onboarding">
2
+ <h2 class="payout-onboarding__title">{{ title() }}</h2>
3
+
4
+ <p class="payout-onboarding__intro">
5
+ Bank routing details are collected by the payout provider, not
6
+ this application. After this step the provider will run KYC and
7
+ notify us when the account is ready for payouts.
8
+ </p>
9
+
10
+ @if (reusable().length > 0) {
11
+ <mat-card class="reuse-card">
12
+ <mat-card-header>
13
+ <mat-card-title>Reuse an existing account</mat-card-title>
14
+ </mat-card-header>
15
+ <mat-card-content>
16
+ <p class="hint">
17
+ You already have a payout account on
18
+ {{ reusable().length === 1 ? 'another partner' : 'other partners' }}.
19
+ You can reuse it here (no KYC redo) if the currency matches.
20
+ </p>
21
+ <mat-radio-group [value]="selectedAccountId()" (change)="selectAccount($any($event.value))">
22
+ @for (acc of reusable(); track acc.providerAccountId) {
23
+ <mat-radio-button [value]="acc.providerAccountId" class="reuse-option">
24
+ <strong>{{ acc.partnerCaption }}</strong>
25
+ — {{ acc.countryCode }} / {{ acc.currency }} ({{ acc.provider }})
26
+ </mat-radio-button>
27
+ }
28
+ </mat-radio-group>
29
+ <button matButton="filled" class="payout-onboarding__btn payout-onboarding__btn--primary"
30
+ [disabled]="!selectedAccountId() || busy()"
31
+ (click)="linkExisting()">
32
+ Use this account
33
+ </button>
34
+ </mat-card-content>
35
+ </mat-card>
36
+ }
37
+
38
+ <mat-card>
39
+ <mat-card-header>
40
+ <mat-card-title>Set up a new account</mat-card-title>
41
+ </mat-card-header>
42
+ <mat-card-content>
43
+ <p class="hint">
44
+ Opens the provider's secure onboarding page. You'll be asked
45
+ for bank routing details and government ID. The process
46
+ usually takes a few minutes.
47
+ </p>
48
+ @if (errorMsg()) {
49
+ <p class="payout-onboarding__error">{{ errorMsg() }}</p>
50
+ }
51
+ <button matButton="filled" class="payout-onboarding__btn payout-onboarding__btn--primary"
52
+ [disabled]="busy()"
53
+ (click)="startProviderKyc()">
54
+ <mat-icon>open_in_new</mat-icon>
55
+ Start onboarding
56
+ </button>
57
+ </mat-card-content>
58
+ </mat-card>
59
+
60
+ <div class="payout-onboarding__nav">
61
+ @if (showBack()) {
62
+ <button matButton="outlined" class="payout-onboarding__btn" type="button" (click)="back.emit()">
63
+ &lt; Back
64
+ </button>
65
+ }
66
+ <button matButton="outlined" class="payout-onboarding__btn" type="button" (click)="skipped.emit()">
67
+ {{ skipLabel() }}
68
+ </button>
69
+ </div>
70
+ </div>
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, inject, signal } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, OnInit, inject, input, output, signal } from '@angular/core';
2
2
  import { MatButtonModule } from '@angular/material/button';
3
3
  import { MatCardModule } from '@angular/material/card';
4
4
  import { MatIconModule } from '@angular/material/icon';
@@ -26,78 +26,7 @@ import { ReusableAccount } from '../../model/appdata';
26
26
  selector: 'sail-payout-provider-onboarding',
27
27
  changeDetection: ChangeDetectionStrategy.OnPush,
28
28
  imports: [MatButtonModule, MatCardModule, MatIconModule, MatRadioModule],
29
- template: `
30
- <div class="payout-onboarding">
31
- <h2 class="payout-onboarding__title">{{ title }}</h2>
32
-
33
- <p class="payout-onboarding__intro">
34
- Bank routing details are collected by the payout provider, not
35
- this application. After this step the provider will run KYC and
36
- notify us when the account is ready for payouts.
37
- </p>
38
-
39
- @if (reusable().length > 0) {
40
- <mat-card class="reuse-card">
41
- <mat-card-header>
42
- <mat-card-title>Reuse an existing account</mat-card-title>
43
- </mat-card-header>
44
- <mat-card-content>
45
- <p class="hint">
46
- You already have a payout account on
47
- {{ reusable().length === 1 ? 'another partner' : 'other partners' }}.
48
- You can reuse it here (no KYC redo) if the currency matches.
49
- </p>
50
- <mat-radio-group [value]="selectedAccountId()" (change)="selectAccount($any($event.value))">
51
- @for (acc of reusable(); track acc.providerAccountId) {
52
- <mat-radio-button [value]="acc.providerAccountId" class="reuse-option">
53
- <strong>{{ acc.partnerCaption }}</strong>
54
- — {{ acc.countryCode }} / {{ acc.currency }} ({{ acc.provider }})
55
- </mat-radio-button>
56
- }
57
- </mat-radio-group>
58
- <button mat-flat-button class="payout-onboarding__btn payout-onboarding__btn--primary"
59
- [disabled]="!selectedAccountId() || busy()"
60
- (click)="linkExisting()">
61
- Use this account
62
- </button>
63
- </mat-card-content>
64
- </mat-card>
65
- }
66
-
67
- <mat-card>
68
- <mat-card-header>
69
- <mat-card-title>Set up a new account</mat-card-title>
70
- </mat-card-header>
71
- <mat-card-content>
72
- <p class="hint">
73
- Opens the provider's secure onboarding page. You'll be asked
74
- for bank routing details and government ID. The process
75
- usually takes a few minutes.
76
- </p>
77
- @if (errorMsg()) {
78
- <p class="payout-onboarding__error">{{ errorMsg() }}</p>
79
- }
80
- <button mat-flat-button class="payout-onboarding__btn payout-onboarding__btn--primary"
81
- [disabled]="busy()"
82
- (click)="startProviderKyc()">
83
- <mat-icon>open_in_new</mat-icon>
84
- Start onboarding
85
- </button>
86
- </mat-card-content>
87
- </mat-card>
88
-
89
- <div class="payout-onboarding__nav">
90
- @if (showBack) {
91
- <button mat-stroked-button class="payout-onboarding__btn" type="button" (click)="back.emit()">
92
- &lt; Back
93
- </button>
94
- }
95
- <button mat-stroked-button class="payout-onboarding__btn" type="button" (click)="skipped.emit()">
96
- {{ skipLabel }}
97
- </button>
98
- </div>
99
- </div>
100
- `,
29
+ templateUrl: './provider_onboarding.html',
101
30
  styles: `
102
31
  .payout-onboarding { display: flex; flex-direction: column; gap: 16px; padding: 0 24px 32px; }
103
32
  .payout-onboarding__title { margin: 0; }
@@ -113,20 +42,20 @@ import { ReusableAccount } from '../../model/appdata';
113
42
  })
114
43
  export class PayoutProviderOnboardingComponent implements OnInit {
115
44
  /** Step title — defaults match the most common payout-step framing. */
116
- @Input() title = 'Bank Account Setup';
45
+ readonly title = input('Bank Account Setup');
117
46
  /** Skip-button label — covers wizards where skipping returns to the parent flow. */
118
- @Input() skipLabel = "I'll do this later";
47
+ readonly skipLabel = input("I'll do this later");
119
48
  /** When true, the back button renders alongside skip. */
120
- @Input() showBack = true;
49
+ readonly showBack = input(true);
121
50
 
122
51
  /** Emitted after a successful reuse-link operation. */
123
- @Output() linked = new EventEmitter<void>();
52
+ readonly linked = output<void>();
124
53
  /** Emitted when the user dismisses the step (skip). */
125
- @Output() skipped = new EventEmitter<void>();
54
+ readonly skipped = output<void>();
126
55
  /** Emitted when the user clicks Back. */
127
- @Output() back = new EventEmitter<void>();
56
+ readonly back = output<void>();
128
57
  /** Emitted after the hosted page is opened in a new tab. Consumers can route away or show a "waiting on provider" screen. */
129
- @Output() started = new EventEmitter<void>();
58
+ readonly started = output<void>();
130
59
 
131
60
  private readonly payoutService = inject(PayoutService);
132
61
 
@@ -31,7 +31,7 @@
31
31
  <td>{{ formatDate(device.last_used_at) }}</td>
32
32
  <td>{{ formatDate(device.created_at) }}</td>
33
33
  <td>
34
- <button mat-icon-button (click)="revokeDevice(device)" title="Revoke">
34
+ <button matIconButton (click)="revokeDevice(device)" title="Revoke">
35
35
  <mat-icon>delete</mat-icon>
36
36
  </button>
37
37
  </td>
@@ -58,15 +58,15 @@
58
58
  <input matInput type="password" formControlName="password" autocomplete="current-password" required>
59
59
  </mat-form-field>
60
60
  <div class="auth-actions">
61
- <button mat-raised-button class="warn" type="submit" [disabled]="!logoutForm.valid || loading()">
61
+ <button matButton="elevated" class="warn" type="submit" [disabled]="!logoutForm.valid || loading()">
62
62
  Sign out everywhere
63
63
  </button>
64
- <button mat-button type="button" (click)="logoutOpen.set(false)">Cancel</button>
64
+ <button matButton type="button" (click)="logoutOpen.set(false)">Cancel</button>
65
65
  </div>
66
66
  </form>
67
67
  } @else {
68
68
  <div class="auth-actions">
69
- <button mat-button class="warn" type="button" (click)="logoutOpen.set(true)" [disabled]="loading()">
69
+ <button matButton class="warn" type="button" (click)="logoutOpen.set(true)" [disabled]="loading()">
70
70
  Sign out of all devices
71
71
  </button>
72
72
  <a routerLink="/dashboard">Back to Dashboard</a>