@block-auth.io/angular 0.1.1 → 0.2.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.
@@ -1,4 +1,19 @@
1
-
2
- > @block-auth.io/angular@0.1.0 typecheck /home/wenqi/Documents/blockauthio/blockauth-sdk/packages/angular
3
- > tsc -p tsconfig.lib.json --noEmit
4
-
1
+
2
+ > @block-auth.io/angular@0.1.2 pretypecheck /home/darius_jvc/projects/blockauth-sdk/packages/angular
3
+ > pnpm build:css
4
+
5
+
6
+ > @block-auth.io/angular@0.1.2 build:css /home/darius_jvc/projects/blockauth-sdk/packages/angular
7
+ > tailwindcss -i ../ui-styles/src/input.css -c ./tailwind.config.cjs -o ./src/styles.generated.css --minify && node scripts/css-to-ts.mjs
8
+
9
+ Browserslist: caniuse-lite is outdated. Please run:
10
+ npx update-browserslist-db@latest
11
+ Why you should do it regularly: https://github.com/browserslist/update-db#readme
12
+
13
+ Rebuilding...
14
+
15
+ Done in 770ms.
16
+
17
+ > @block-auth.io/angular@0.1.2 typecheck /home/darius_jvc/projects/blockauth-sdk/packages/angular
18
+ > tsc -p tsconfig.lib.json --noEmit
19
+
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @block-auth.io/angular
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Conditionally render DropdownProfile actions based on authentication state
8
+
9
+ ## 0.1.2
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+ - @block-auth.io/core@0.4.1
15
+
3
16
  ## 0.1.1
4
17
 
5
18
  ### Patch Changes
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@block-auth.io/angular",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "dependencies": {
5
5
  "nanostores": "^0.10.0",
6
6
  "tslib": "^2.6.0",
7
- "@block-auth.io/core": "0.4.0",
7
+ "@block-auth.io/core": "0.4.1",
8
8
  "@block-auth.io/ui-styles": "0.4.0"
9
9
  },
10
10
  "peerDependencies": {
@@ -62,30 +62,22 @@ import { TbCopyComponent, TbCopyCheckFilledComponent } from '../../icons';
62
62
  <ba-tb-copy class="bal-ba-transition-2 bas-h-[18px] bas-w-[18px] bas-shrink-0" />
63
63
  }
64
64
  </div>
65
+ <!-- Solo texto, no copiable (la fila DID de arriba es la única acción de copiar) -->
65
66
  @if (showAccountRow()) {
66
- <div
67
- [class]="
68
- 'bal-ba-transition-2 bas-flex bas-max-w-full bas-flex-row bas-items-center bas-justify-center bas-gap-1 bas-cursor-pointer ' +
69
- (isAccountCopied() ? 'bas-text-[#4d849b]' : 'hover:bas-opacity-50')
70
- "
71
- (click)="onCopyAccount($event)"
72
- >
67
+ <div class="bal-ba-transition-2 bas-flex bas-max-w-full bas-flex-row bas-items-center bas-justify-center bas-gap-1">
73
68
  <p class="bas-m-0 bas-truncate bas-text-xs">{{ accountSubtitle() }}</p>
74
- @if (isAccountCopied()) {
75
- <ba-tb-copy-check-filled class="bal-ba-transition-2 bas-h-4 bas-w-4 bas-shrink-0" />
76
- } @else {
77
- <ba-tb-copy class="bal-ba-transition-2 bas-h-4 bas-w-4 bas-shrink-0" />
78
- }
79
69
  </div>
80
70
  }
81
71
  </div>
82
72
  <div [class]="arrowContainerClassNames()">
83
- <ba-dropdown-arrow
84
- id="ba-ddp-btn-arrow"
85
- [isOpen]="isProfileOpen()"
86
- [color]="style().colorBase"
87
- [sizeClassNames]="arrowSizesClassNames()"
88
- />
73
+ @if (isSigned()) {
74
+ <ba-dropdown-arrow
75
+ id="ba-ddp-btn-arrow"
76
+ [isOpen]="isProfileOpen()"
77
+ [color]="style().colorBase"
78
+ [sizeClassNames]="arrowSizesClassNames()"
79
+ />
80
+ }
89
81
  </div>
90
82
  </div>
91
83
  `,
@@ -154,7 +146,6 @@ export class DropdownProfileBtnComponent {
154
146
  );
155
147
 
156
148
  protected readonly isDidCopied = signal(false);
157
- protected readonly isAccountCopied = signal(false);
158
149
 
159
150
  protected readonly gridClassNames = computed(() => this.gridClassNamesBySize[this.dropdownBtnSize()]);
160
151
  protected readonly imgSizesClassNames = computed(() => this.imgSizesClassNamesBySize[this.dropdownBtnSize()]);
@@ -168,18 +159,12 @@ export class DropdownProfileBtnComponent {
168
159
  });
169
160
 
170
161
  onCopyDid(e: MouseEvent): void {
171
- this.copy(this.rawDid(), this.isDidCopied, e);
172
- }
173
- onCopyAccount(e: MouseEvent): void {
174
- this.copy(this.rawAccount(), this.isAccountCopied, e);
175
- }
176
-
177
- private copy(value: string, copiedSignal: { set: (v: boolean) => void }, e: MouseEvent): void {
178
162
  // Don't toggle the dropdown open/closed when the user only wants to copy.
179
163
  e.stopPropagation();
164
+ const value = this.rawDid();
180
165
  if (!value) return;
181
166
  void navigator.clipboard?.writeText(value);
182
- copiedSignal.set(true);
183
- setTimeout(() => copiedSignal.set(false), 3000);
167
+ this.isDidCopied.set(true);
168
+ setTimeout(() => this.isDidCopied.set(false), 3000);
184
169
  }
185
170
  }
@@ -18,13 +18,15 @@ vi.mock('@wagmi/connectors', () => ({
18
18
  // packages/vue/.../DropdownProfile.test.ts). Client falso con `logout` spy y
19
19
  // stores nanostores; serviceName 'blockauth' para textos en inglés.
20
20
  describe('DropdownProfile', () => {
21
- function makeClient(logout = vi.fn(async () => {})) {
21
+ // isSigned: true — el item list (My profile/Change account/Disconnect)
22
+ // solo se muestra autenticado (ver dropdown-profile.component.ts).
23
+ function makeClient(logout = vi.fn(async () => {}), uiOverrides: Record<string, unknown> = {}) {
22
24
  return {
23
25
  logout,
24
26
  web3: { signup: vi.fn() },
25
27
  oauth: { signupGoogle: vi.fn(), signupMicrosoft: vi.fn() },
26
28
  connectBlockAuth: vi.fn(),
27
- state: { $ui: map({ serviceName: 'blockauth' }), $ws: map({}) },
29
+ state: { $ui: map({ serviceName: 'blockauth', isSigned: true, ...uiOverrides }), $ws: map({}) },
28
30
  } as any;
29
31
  }
30
32
 
@@ -95,4 +97,14 @@ describe('DropdownProfile', () => {
95
97
  });
96
98
  expect(screen.getByTestId('dropdown-profile').className).toContain('bas-w-fit');
97
99
  });
100
+
101
+ it('sin firmar (conectado pero no autenticado) solo permite copiar el DID', async () => {
102
+ await render(DropdownProfileComponent, {
103
+ providers: [provideBlockAuthClientForTest(makeClient(vi.fn(), { isSigned: false }))],
104
+ });
105
+ await fireEvent.click(screen.getByTestId('dropdown-profile-btn'));
106
+ expect(screen.queryByText('My profile')).toBeNull();
107
+ expect(screen.queryByText('Change account')).toBeNull();
108
+ expect(screen.queryByText('Disconnect')).toBeNull();
109
+ });
98
110
  });
@@ -88,22 +88,23 @@ export interface DropdownProfileAdditionalItem {
88
88
  (clicked)="item.onClick()"
89
89
  />
90
90
  }
91
- <!-- CHANGE ACCOUNT -->
92
- <ba-change-account
93
- [serviceName]="serviceName()"
94
- [text]="localized(btnsTexts().changeAccount, 'Change account')"
95
- [disabled]="disabled()"
96
- [isTopBorder]="isWeb3Service() || itemsToShow().length > 0"
97
- (changeAccountClicked)="onChangeAccountClicked()"
98
- />
99
- <!-- DISCONNECT -->
100
- <ba-dropdown-profile-item
101
- id="ba-dd8-disconnect-0"
102
- [text]="localized(btnsTexts().disconnect, 'Disconnect')"
103
- [firstIcon]="disconnectIcon"
104
- [isTopBorder]="true"
105
- (clicked)="onDisconnectClicked()"
106
- />
91
+ <!-- CHANGE ACCOUNT + DISCONNECT — hidden until the login transaction is signed -->
92
+ @if (isSigned()) {
93
+ <ba-change-account
94
+ [serviceName]="serviceName()"
95
+ [text]="localized(btnsTexts().changeAccount, 'Change account')"
96
+ [disabled]="disabled()"
97
+ [isTopBorder]="isWeb3Service() || itemsToShow().length > 0"
98
+ (changeAccountClicked)="onChangeAccountClicked()"
99
+ />
100
+ <ba-dropdown-profile-item
101
+ id="ba-dd8-disconnect-0"
102
+ [text]="localized(btnsTexts().disconnect, 'Disconnect')"
103
+ [firstIcon]="disconnectIcon"
104
+ [isTopBorder]="true"
105
+ (clicked)="onDisconnectClicked()"
106
+ />
107
+ }
107
108
  </ul>
108
109
  </div>
109
110
  }
@@ -153,6 +154,7 @@ export class DropdownProfileComponent {
153
154
  private animTimeout: ReturnType<typeof setTimeout> | undefined;
154
155
 
155
156
  protected readonly serviceName = computed(() => this.ui().serviceName ?? 'blockauth');
157
+ protected readonly isSigned = computed(() => this.ui().isSigned);
156
158
 
157
159
  private readonly uiConfigContent = computed(
158
160
  () =>
@@ -191,12 +193,15 @@ export class DropdownProfileComponent {
191
193
  },
192
194
  ]);
193
195
 
194
- protected readonly itemsToShow = computed(
195
- () => this.additionalDropdownItems() ?? this.defaultAdditionalDropdownItems(),
196
+ // Not fully authenticated yet (connected but transaction unsigned): only the
197
+ // "Copy DID" row in `DropdownProfileBtnComponent` is a valid action, so the
198
+ // item list (My profile / Change account / Disconnect) stays empty.
199
+ protected readonly itemsToShow = computed(() =>
200
+ this.isSigned() ? this.additionalDropdownItems() ?? this.defaultAdditionalDropdownItems() : [],
196
201
  );
197
202
  private readonly web2Services = ['google', 'microsoft'];
198
203
  protected readonly isWeb3Service = computed(() => !this.web2Services.includes(this.serviceName()));
199
- protected readonly totalDropdownItems = computed(() => 2 + this.itemsToShow().length);
204
+ protected readonly totalDropdownItems = computed(() => (this.isSigned() ? 2 + this.itemsToShow().length : 0));
200
205
 
201
206
  private readonly heightBySize: Record<string, number> = { xs: 46, small: 52, base: 58, large: 64 };
202
207
 
@@ -217,6 +222,7 @@ export class DropdownProfileComponent {
217
222
  }
218
223
 
219
224
  onProfileClicked(): void {
225
+ if (!this.isSigned()) return;
220
226
  this.isAnimationCompleted.set(false);
221
227
  const next = !this.isProfileOpen();
222
228
  this.isProfileOpen.set(next);
@@ -1,14 +1,27 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
2
  import { render, screen } from '@testing-library/angular';
3
+ import { setLocale } from '@block-auth.io/core';
3
4
  import { PermissionsRequiredModalComponent } from './permissions-required-modal.component';
4
5
 
5
6
  // Port de packages/react/src/components/txns/internals/PermissionsRequiredModal.test.tsx
6
7
  // (mapa: packages/vue/.../PermissionsRequiredModal.test.ts).
7
8
  describe('PermissionsRequiredModal', () => {
9
+ afterEach(() => setLocale('en'));
10
+
8
11
  it('renderiza el modal de permisos requeridos', async () => {
9
12
  await render(PermissionsRequiredModalComponent);
10
13
  expect(screen.getByText('Temporal access to identity')).toBeDefined();
11
- expect(screen.getByText('Access to the list of authorised apps')).toBeDefined();
12
- expect(screen.getByText('Only this transaction will be signed')).toBeDefined();
14
+ expect(
15
+ screen.getByText('Access to the list of authorised apps'),
16
+ ).toBeDefined();
17
+ expect(
18
+ screen.getByText('Only this transaction will be signed'),
19
+ ).toBeDefined();
20
+ });
21
+
22
+ it('respeta el locale activo', async () => {
23
+ setLocale('es');
24
+ await render(PermissionsRequiredModalComponent);
25
+ expect(screen.getByText('Acceso temporal a tu identidad')).toBeDefined();
13
26
  });
14
27
  });
@@ -1,14 +1,10 @@
1
- import { ChangeDetectionStrategy, Component } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, computed } from '@angular/core';
2
+ import { $locale, t as translate, type TextKey } from '@block-auth.io/core';
3
+ import { injectStore } from '../../../inject-store';
2
4
 
3
5
  // Port literal de packages/react/src/components/txns/internals/PermissionsRequiredModal.tsx
4
- // (mapa: packages/vue/.../PermissionsRequiredModal.vue). Sin props/outputs, sin
5
- // lógica de core: solo itera un array fijo de permisos.
6
+ // (mapa: packages/vue/.../PermissionsRequiredModal.vue). Sin props/outputs.
6
7
  const iconClassNames = 'bas-w-6 bas-h-6 bas-text-[#009F6B]';
7
- const requiredPermissions = [
8
- { message: 'Temporal access to identity', icon: '👤' },
9
- { message: 'Access to the list of authorised apps', icon: '📝' },
10
- { message: 'Only this transaction will be signed', icon: '🔄' },
11
- ];
12
8
 
13
9
  @Component({
14
10
  selector: 'ba-permissions-required-modal',
@@ -18,18 +14,30 @@ const requiredPermissions = [
18
14
  <div
19
15
  class="bal-ba-transition-2 bas-box-border bas-grid bas-w-[334px] bas-grid-rows-3 bas-gap-3 bas-rounded-md bas-border-2 bas-border-solid bas-border-[var(--bal-primary-100)] bas-bg-[var(--bal-primary-10)] bas-py-4 bas-pl-3 hover:bas-border-[var(--bal-primary-300)]"
20
16
  >
21
- @for (permission of requiredPermissions; track $index) {
22
- <div class="bas-flex bas-flex-row bas-items-center bas-gap-2">
23
- <span [class]="iconClassNames" aria-hidden="true">{{ permission.icon }}</span>
24
- <p class="bas-m-0 bas-text-start bas-text-base bas-text-[var(--bal-primary-800)]">
25
- {{ permission.message }}
26
- </p>
27
- </div>
17
+ @for (permission of requiredPermissions(); track $index) {
18
+ <div class="bas-flex bas-flex-row bas-items-center bas-gap-2">
19
+ <span [class]="iconClassNames" aria-hidden="true">{{
20
+ permission.icon
21
+ }}</span>
22
+ <p
23
+ class="bas-m-0 bas-text-start bas-text-base bas-text-[var(--bal-primary-800)]"
24
+ >
25
+ {{ permission.message }}
26
+ </p>
27
+ </div>
28
28
  }
29
29
  </div>
30
30
  `,
31
31
  })
32
32
  export class PermissionsRequiredModalComponent {
33
33
  protected readonly iconClassNames = iconClassNames;
34
- protected readonly requiredPermissions = requiredPermissions;
34
+
35
+ private readonly locale = injectStore($locale);
36
+ protected t = (key: TextKey): string => translate(key, this.locale());
37
+
38
+ protected readonly requiredPermissions = computed(() => [
39
+ { message: this.t('permTemporalIdentity'), icon: '👤' },
40
+ { message: this.t('permAuthorisedApps'), icon: '📝' },
41
+ { message: this.t('permOnlyThisTxn'), icon: '🔄' },
42
+ ]);
35
43
  }
@@ -1,16 +1,25 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
2
  import { Component } from '@angular/core';
3
3
  import { render, screen, fireEvent } from '@testing-library/angular';
4
- import { SolicitedDataModalComponent, type ParamChange } from './solicited-data-modal.component';
4
+ import { setLocale } from '@block-auth.io/core';
5
+ import {
6
+ SolicitedDataModalComponent,
7
+ type ParamChange,
8
+ } from './solicited-data-modal.component';
5
9
 
6
10
  // Port de packages/react/src/components/txns/internals/SolicitedDataModal.test.tsx
7
11
  // (mapa: packages/vue/.../SolicitedDataModal.test.ts). Presentacional, sin client.
8
12
  describe('SolicitedDataModal', () => {
13
+ afterEach(() => setLocale('en'));
14
+
9
15
  it('renderiza el modal de datos solicitados con campos requeridos y opcionales', async () => {
10
16
  @Component({
11
17
  standalone: true,
12
18
  imports: [SolicitedDataModalComponent],
13
- template: `<ba-solicited-data-modal [requiredParams]="['users-email']" [optionalParams]="['users-phone']" />`,
19
+ template: `<ba-solicited-data-modal
20
+ [requiredParams]="['users-email']"
21
+ [optionalParams]="['users-phone']"
22
+ />`,
14
23
  })
15
24
  class Host {}
16
25
  await render(Host);
@@ -18,6 +27,24 @@ describe('SolicitedDataModal', () => {
18
27
  expect(screen.getByText('Phone')).toBeDefined();
19
28
  });
20
29
 
30
+ it('respeta el locale activo', async () => {
31
+ setLocale('es');
32
+ @Component({
33
+ standalone: true,
34
+ imports: [SolicitedDataModalComponent],
35
+ template: `<ba-solicited-data-modal
36
+ [requiredParams]="['users-email']"
37
+ [optionalParams]="['users-phone']"
38
+ />`,
39
+ })
40
+ class Host {}
41
+ await render(Host);
42
+ expect(screen.getByText('Correo electrónico')).toBeDefined();
43
+ expect(screen.getByText('Teléfono')).toBeDefined();
44
+ expect(screen.getByText('Requerido')).toBeDefined();
45
+ expect(screen.getByText('Opcional')).toBeDefined();
46
+ });
47
+
21
48
  it('emite paramChange al escribir y validationChange según validez', async () => {
22
49
  const paramChanges: ParamChange[] = [];
23
50
  const validationChanges: boolean[] = [];
@@ -44,7 +71,10 @@ describe('SolicitedDataModal', () => {
44
71
 
45
72
  const input = screen.getByRole('textbox');
46
73
  await fireEvent.input(input, { target: { value: 'not-an-email' } });
47
- expect(paramChanges[paramChanges.length - 1]).toEqual({ slug: 'users-email', value: 'not-an-email' });
74
+ expect(paramChanges[paramChanges.length - 1]).toEqual({
75
+ slug: 'users-email',
76
+ value: 'not-an-email',
77
+ });
48
78
 
49
79
  fixture.componentInstance.vals = { 'users-email': 'a@b.com' };
50
80
  fixture.detectChanges();
@@ -67,6 +97,8 @@ describe('SolicitedDataModal', () => {
67
97
  await render(Host);
68
98
  const input = screen.getByRole('textbox');
69
99
  await fireEvent.blur(input);
70
- expect(screen.getByText('Enter a valid email (e.g. name@domain.com)')).toBeDefined();
100
+ expect(
101
+ screen.getByText('Enter a valid email (e.g. name@domain.com)'),
102
+ ).toBeDefined();
71
103
  });
72
104
  });
@@ -7,6 +7,8 @@ import {
7
7
  output,
8
8
  signal,
9
9
  } from '@angular/core';
10
+ import { $locale, t as translate, type TextKey } from '@block-auth.io/core';
11
+ import { injectStore } from '../../../inject-store';
10
12
 
11
13
  // `handleParamChange`/`onValidationChange` (callback props en react) -> outputs
12
14
  // `paramChange`/`validationChange`: notificaciones puras de "algo cambió en la
@@ -22,13 +24,12 @@ export interface ParamChange {
22
24
  value: string;
23
25
  }
24
26
 
25
- const PARAM_LABELS: Record<string, string> = {
26
- 'modules-kyc': 'KYC',
27
- 'users-firstname': 'First Name',
28
- 'users-lastname': 'Last Name',
29
- 'users-secondlastname': 'Second Last Name',
30
- 'users-email': 'Email',
31
- 'users-phone': 'Phone',
27
+ const PARAM_LABEL_KEYS: Record<string, TextKey> = {
28
+ 'users-firstname': 'firstName',
29
+ 'users-lastname': 'lastName',
30
+ 'users-secondlastname': 'secondLastName',
31
+ 'users-email': 'email',
32
+ 'users-phone': 'phone',
32
33
  };
33
34
 
34
35
  const PARAM_DISPLAY_ORDER = [
@@ -56,12 +57,12 @@ const PARAM_VALIDATORS: Record<string, (v: string) => boolean> = {
56
57
  'users-phone': (v) => /^\+?[\d\s\-().]{6,20}$/.test(v.trim()),
57
58
  };
58
59
 
59
- const PARAM_ERROR_MSG: Record<string, string> = {
60
- 'users-firstname': 'Only letters and spaces allowed',
61
- 'users-lastname': 'Only letters and spaces allowed',
62
- 'users-secondlastname': 'Only letters and spaces allowed',
63
- 'users-email': 'Enter a valid email (e.g. name@domain.com)',
64
- 'users-phone': 'Enter a valid phone number',
60
+ const PARAM_ERROR_KEYS: Record<string, TextKey> = {
61
+ 'users-firstname': 'onlyLettersAllowed',
62
+ 'users-lastname': 'onlyLettersAllowed',
63
+ 'users-secondlastname': 'onlyLettersAllowed',
64
+ 'users-email': 'enterValidEmail',
65
+ 'users-phone': 'enterValidPhone',
65
66
  };
66
67
 
67
68
  @Component({
@@ -73,54 +74,66 @@ const PARAM_ERROR_MSG: Record<string, string> = {
73
74
  class="bal-ba-transition-2 bas-box-border bas-flex bas-w-[334px] bas-flex-col bas-gap-3 bas-rounded-md bas-border-2 bas-border-solid bas-border-[var(--bal-primary-100)] bas-bg-[var(--bal-primary-10)] bas-px-4 bas-py-4 hover:bas-border-[var(--bal-primary-300)]"
74
75
  >
75
76
  @for (slug of visibleParams(); track slug) {
76
- <div class="bas-flex bas-flex-col bas-gap-1">
77
- <div class="bas-flex bas-items-center bas-justify-between">
78
- <label class="bas-text-xs bas-font-medium bas-text-slate-600">{{ label(slug) }}</label>
79
- <span
80
- style="font-size: 10px"
81
- [class]="
82
- 'bas-rounded bas-px-1 bas-py-px bas-font-bold bas-uppercase bas-tracking-wide ' +
83
- (isRequired(slug)
84
- ? 'bas-bg-amber-100 bas-text-amber-600'
85
- : 'bas-bg-slate-100 bas-text-slate-400')
86
- "
87
- >
88
- {{ isRequired(slug) ? requiredLabelTxt() : optionalLabelTxt() }}
89
- </span>
90
- </div>
91
- <div class="bas-relative">
92
- <input
93
- [type]="inputType(slug)"
94
- [value]="valueOf(slug)"
95
- [class]="
96
- 'bas-w-full bas-rounded bas-border bas-bg-white bas-px-2 bas-py-1 bas-text-sm bas-text-slate-700 bas-outline-none bas-transition-colors ' +
97
- (errorOf(slug)
98
- ? 'bas-border-red-400 bas-pr-7 focus:bas-border-red-500'
99
- : 'bas-border-slate-300 focus:bas-border-[var(--bal-primary-500)]')
100
- "
101
- (input)="onInput(slug, $event)"
102
- (blur)="onBlur(slug)"
103
- />
104
- @if (errorOf(slug)) {
105
- <svg
106
- class="bas-absolute bas-right-2 bas-top-1/2 bas-h-4 bas-w-4 -bas-translate-y-1/2 bas-text-red-500"
107
- viewBox="0 0 24 24"
108
- fill="currentColor"
109
- aria-hidden="true"
110
- >
111
- <path d="M12 2 1 21h22L12 2Zm0 6c.55 0 1 .45 1 1v5a1 1 0 1 1-2 0V9c0-.55.45-1 1-1Zm0 9.5a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Z" />
112
- </svg>
113
- }
114
- </div>
77
+ <div class="bas-flex bas-flex-col bas-gap-1">
78
+ <div class="bas-flex bas-items-center bas-justify-between">
79
+ <label class="bas-text-xs bas-font-medium bas-text-slate-600">{{
80
+ label(slug)
81
+ }}</label>
82
+ <span
83
+ style="font-size: 10px"
84
+ [class]="
85
+ 'bas-rounded bas-px-1 bas-py-px bas-font-bold bas-uppercase bas-tracking-wide ' +
86
+ (isRequired(slug)
87
+ ? 'bas-bg-amber-100 bas-text-amber-600'
88
+ : 'bas-bg-slate-100 bas-text-slate-400')
89
+ "
90
+ >
91
+ {{
92
+ isRequired(slug)
93
+ ? requiredLabelTxtResolved()
94
+ : optionalLabelTxtResolved()
95
+ }}
96
+ </span>
97
+ </div>
98
+ <div class="bas-relative">
99
+ <input
100
+ [type]="inputType(slug)"
101
+ [value]="valueOf(slug)"
102
+ [class]="
103
+ 'bas-w-full bas-rounded bas-border bas-bg-white bas-px-2 bas-py-1 bas-text-sm bas-text-slate-700 bas-outline-none bas-transition-colors ' +
104
+ (errorOf(slug)
105
+ ? 'bas-border-red-400 bas-pr-7 focus:bas-border-red-500'
106
+ : 'bas-border-slate-300 focus:bas-border-[var(--bal-primary-500)]')
107
+ "
108
+ (input)="onInput(slug, $event)"
109
+ (blur)="onBlur(slug)"
110
+ />
115
111
  @if (errorOf(slug)) {
116
- <p style="font-size: 10px" class="bas-m-0 bas-px-2 bas-font-medium bas-text-red-500">
117
- {{ errorOf(slug) }}
118
- </p>
112
+ <svg
113
+ class="bas-absolute bas-right-2 bas-top-1/2 bas-h-4 bas-w-4 -bas-translate-y-1/2 bas-text-red-500"
114
+ viewBox="0 0 24 24"
115
+ fill="currentColor"
116
+ aria-hidden="true"
117
+ >
118
+ <path
119
+ d="M12 2 1 21h22L12 2Zm0 6c.55 0 1 .45 1 1v5a1 1 0 1 1-2 0V9c0-.55.45-1 1-1Zm0 9.5a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Z"
120
+ />
121
+ </svg>
119
122
  }
120
123
  </div>
121
- }
122
- @if (!allRequiredFilled()) {
123
- <p class="bas-m-0 bas-text-center bas-text-xs bas-text-amber-600">{{ fillRequiredTxt() }}</p>
124
+ @if (errorOf(slug)) {
125
+ <p
126
+ style="font-size: 10px"
127
+ class="bas-m-0 bas-px-2 bas-font-medium bas-text-red-500"
128
+ >
129
+ {{ errorOf(slug) }}
130
+ </p>
131
+ }
132
+ </div>
133
+ } @if (!allRequiredFilled()) {
134
+ <p class="bas-m-0 bas-text-center bas-text-xs bas-text-amber-600">
135
+ {{ fillRequiredTxtResolved() }}
136
+ </p>
124
137
  }
125
138
  </div>
126
139
  `,
@@ -129,13 +142,26 @@ export class SolicitedDataModalComponent {
129
142
  requiredParams = input<string[]>([]);
130
143
  optionalParams = input<string[]>([]);
131
144
  userParamValues = input<Record<string, string>>({});
132
- requiredLabelTxt = input('Required');
133
- optionalLabelTxt = input('Optional');
134
- fillRequiredTxt = input('Fill required fields to continue');
145
+ requiredLabelTxt = input<string>();
146
+ optionalLabelTxt = input<string>();
147
+ fillRequiredTxt = input<string>();
135
148
 
136
149
  paramChange = output<ParamChange>();
137
150
  validationChange = output<boolean>();
138
151
 
152
+ private readonly locale = injectStore($locale);
153
+ protected t = (key: TextKey): string => translate(key, this.locale());
154
+
155
+ protected readonly requiredLabelTxtResolved = computed(
156
+ () => this.requiredLabelTxt() ?? this.t('required'),
157
+ );
158
+ protected readonly optionalLabelTxtResolved = computed(
159
+ () => this.optionalLabelTxt() ?? this.t('optional'),
160
+ );
161
+ protected readonly fillRequiredTxtResolved = computed(
162
+ () => this.fillRequiredTxt() ?? this.t('fillRequiredFields'),
163
+ );
164
+
139
165
  private readonly touched = signal<Record<string, boolean>>({});
140
166
 
141
167
  protected readonly allRequiredFilled = computed(() =>
@@ -152,7 +178,8 @@ export class SolicitedDataModalComponent {
152
178
  PARAM_DISPLAY_ORDER.filter(
153
179
  (slug) =>
154
180
  slug !== 'modules-kyc' &&
155
- (this.requiredParams().includes(slug) || this.optionalParams().includes(slug)),
181
+ (this.requiredParams().includes(slug) ||
182
+ this.optionalParams().includes(slug)),
156
183
  ),
157
184
  );
158
185
 
@@ -163,14 +190,17 @@ export class SolicitedDataModalComponent {
163
190
  }
164
191
 
165
192
  isRequired = (slug: string): boolean => this.requiredParams().includes(slug);
166
- label = (slug: string): string => PARAM_LABELS[slug] ?? slug;
193
+ label = (slug: string): string => {
194
+ const key = PARAM_LABEL_KEYS[slug];
195
+ return key ? this.t(key) : slug;
196
+ };
167
197
  inputType = (slug: string): string => PARAM_INPUT_TYPE[slug] || 'text';
168
198
  valueOf = (slug: string): string => this.userParamValues()[slug] || '';
169
199
 
170
200
  private getError(slug: string, value: string): string | null {
171
201
  if (!value) return null;
172
202
  const validate = PARAM_VALIDATORS[slug];
173
- if (validate && !validate(value)) return PARAM_ERROR_MSG[slug];
203
+ if (validate && !validate(value)) return this.t(PARAM_ERROR_KEYS[slug]);
174
204
  return null;
175
205
  }
176
206
 
@@ -182,6 +212,9 @@ export class SolicitedDataModalComponent {
182
212
  }
183
213
 
184
214
  onInput(slug: string, event: Event): void {
185
- this.paramChange.emit({ slug, value: (event.target as HTMLInputElement).value });
215
+ this.paramChange.emit({
216
+ slug,
217
+ value: (event.target as HTMLInputElement).value,
218
+ });
186
219
  }
187
220
  }