@block-auth.io/angular 0.1.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 (148) hide show
  1. package/.turbo/turbo-build.log +37 -0
  2. package/.turbo/turbo-lint.log +4 -0
  3. package/.turbo/turbo-test.log +82 -0
  4. package/.turbo/turbo-typecheck.log +4 -0
  5. package/CHANGELOG.md +10 -0
  6. package/ng-package.json +6 -0
  7. package/package.json +50 -0
  8. package/scripts/css-to-ts.mjs +14 -0
  9. package/src/lib/__tests__/integration.test.ts +45 -0
  10. package/src/lib/block-auth.service.test.ts +69 -0
  11. package/src/lib/block-auth.service.ts +81 -0
  12. package/src/lib/components/btns/btn-base-provider.component.test.ts +22 -0
  13. package/src/lib/components/btns/btn-base-provider.component.ts +55 -0
  14. package/src/lib/components/btns/btn-block-auth.component.test.ts +79 -0
  15. package/src/lib/components/btns/btn-block-auth.component.ts +104 -0
  16. package/src/lib/components/btns/btn-gmail.component.test.ts +16 -0
  17. package/src/lib/components/btns/btn-gmail.component.ts +41 -0
  18. package/src/lib/components/btns/btn-google-base.component.test.ts +16 -0
  19. package/src/lib/components/btns/btn-google-base.component.ts +73 -0
  20. package/src/lib/components/btns/btn-metamask.component.test.ts +20 -0
  21. package/src/lib/components/btns/btn-metamask.component.ts +98 -0
  22. package/src/lib/components/btns/btn-microsoft.component.test.ts +16 -0
  23. package/src/lib/components/btns/btn-microsoft.component.ts +77 -0
  24. package/src/lib/components/btns/btn-phantom.component.test.ts +20 -0
  25. package/src/lib/components/btns/btn-phantom.component.ts +93 -0
  26. package/src/lib/components/btns/btn-signin.component.test.ts +55 -0
  27. package/src/lib/components/btns/btn-signin.component.ts +108 -0
  28. package/src/lib/components/btns/btn-wallet-connect.component.test.ts +20 -0
  29. package/src/lib/components/btns/btn-wallet-connect.component.ts +109 -0
  30. package/src/lib/components/btns/index.ts +11 -0
  31. package/src/lib/components/contents/content-signin.component.test.ts +33 -0
  32. package/src/lib/components/contents/content-signin.component.ts +68 -0
  33. package/src/lib/components/contents/content-signup.component.test.ts +78 -0
  34. package/src/lib/components/contents/content-signup.component.ts +225 -0
  35. package/src/lib/components/dds/change-account.component.test.ts +66 -0
  36. package/src/lib/components/dds/change-account.component.ts +112 -0
  37. package/src/lib/components/dds/dropdown-profile-btn.component.test.ts +66 -0
  38. package/src/lib/components/dds/dropdown-profile-btn.component.ts +185 -0
  39. package/src/lib/components/dds/dropdown-profile-item.component.test.ts +75 -0
  40. package/src/lib/components/dds/dropdown-profile-item.component.ts +74 -0
  41. package/src/lib/components/dds/dropdown-profile.component.test.ts +98 -0
  42. package/src/lib/components/dds/dropdown-profile.component.ts +258 -0
  43. package/src/lib/components/flow-block-auth.component.test.ts +93 -0
  44. package/src/lib/components/flow-block-auth.component.ts +226 -0
  45. package/src/lib/components/modals/modal-overlay.component.test.ts +81 -0
  46. package/src/lib/components/modals/modal-overlay.component.ts +43 -0
  47. package/src/lib/components/modals/modal-restoration-expired.component.test.ts +71 -0
  48. package/src/lib/components/modals/modal-restoration-expired.component.ts +83 -0
  49. package/src/lib/components/modals/modal-restore.component.test.ts +74 -0
  50. package/src/lib/components/modals/modal-restore.component.ts +104 -0
  51. package/src/lib/components/modals/modal-signin.component.test.ts +66 -0
  52. package/src/lib/components/modals/modal-signin.component.ts +44 -0
  53. package/src/lib/components/modals/modal-signup.component.test.ts +56 -0
  54. package/src/lib/components/modals/modal-signup.component.ts +40 -0
  55. package/src/lib/components/sections/footer-section.component.test.ts +19 -0
  56. package/src/lib/components/sections/footer-section.component.ts +49 -0
  57. package/src/lib/components/sections/internals/providers-buttons.component.test.ts +27 -0
  58. package/src/lib/components/sections/internals/providers-buttons.component.ts +290 -0
  59. package/src/lib/components/sections/internals/providers-filters.component.test.ts +25 -0
  60. package/src/lib/components/sections/internals/providers-filters.component.ts +139 -0
  61. package/src/lib/components/sections/internals/selected-button.component.test.ts +36 -0
  62. package/src/lib/components/sections/internals/selected-button.component.ts +92 -0
  63. package/src/lib/components/sections/login-section.component.test.ts +49 -0
  64. package/src/lib/components/sections/login-section.component.ts +120 -0
  65. package/src/lib/components/sections/providers-section.component.test.ts +22 -0
  66. package/src/lib/components/sections/providers-section.component.ts +187 -0
  67. package/src/lib/components/txns/internals/permissions-required-modal.component.test.ts +14 -0
  68. package/src/lib/components/txns/internals/permissions-required-modal.component.ts +35 -0
  69. package/src/lib/components/txns/internals/solicited-data-modal.component.test.ts +72 -0
  70. package/src/lib/components/txns/internals/solicited-data-modal.component.ts +187 -0
  71. package/src/lib/components/txns/internals/txn-error.component.test.ts +100 -0
  72. package/src/lib/components/txns/internals/txn-error.component.ts +121 -0
  73. package/src/lib/components/txns/internals/txn-type.component.test.ts +52 -0
  74. package/src/lib/components/txns/internals/txn-type.component.ts +51 -0
  75. package/src/lib/components/txns/internals/txn.component.test.ts +188 -0
  76. package/src/lib/components/txns/internals/txn.component.ts +298 -0
  77. package/src/lib/components/txns/txns-overview.component.test.ts +107 -0
  78. package/src/lib/components/txns/txns-overview.component.ts +229 -0
  79. package/src/lib/icons/ai-outline-disconnect.component.ts +21 -0
  80. package/src/lib/icons/bi-logo-gmail.component.ts +21 -0
  81. package/src/lib/icons/cg-profile.component.ts +31 -0
  82. package/src/lib/icons/check-icon.component.ts +27 -0
  83. package/src/lib/icons/ci-clock2.component.ts +31 -0
  84. package/src/lib/icons/fa-arrow-right.component.ts +21 -0
  85. package/src/lib/icons/fa-times.component.ts +21 -0
  86. package/src/lib/icons/fa-undo.component.ts +21 -0
  87. package/src/lib/icons/icons.test.ts +25 -0
  88. package/src/lib/icons/index.ts +22 -0
  89. package/src/lib/icons/io-ios-arrow-back.component.ts +21 -0
  90. package/src/lib/icons/io-ios-arrow-forward.component.ts +21 -0
  91. package/src/lib/icons/io-md-arrow-back.component.ts +19 -0
  92. package/src/lib/icons/io-md-arrow-forward.component.ts +19 -0
  93. package/src/lib/icons/pi-warning-fill.component.ts +21 -0
  94. package/src/lib/icons/tb-arrows-exchange2.component.ts +30 -0
  95. package/src/lib/icons/tb-copy-check-filled.component.ts +21 -0
  96. package/src/lib/icons/tb-copy.component.ts +34 -0
  97. package/src/lib/icons/ti-vendor-microsoft.component.ts +21 -0
  98. package/src/lib/inject-store.test.ts +26 -0
  99. package/src/lib/inject-store.ts +16 -0
  100. package/src/lib/primitives/arrow-icon.component.test.ts +12 -0
  101. package/src/lib/primitives/arrow-icon.component.ts +38 -0
  102. package/src/lib/primitives/btn-arrow-dir.component.test.ts +22 -0
  103. package/src/lib/primitives/btn-arrow-dir.component.ts +65 -0
  104. package/src/lib/primitives/btn-base.component.test.ts +38 -0
  105. package/src/lib/primitives/btn-base.component.ts +140 -0
  106. package/src/lib/primitives/divider-base.component.test.ts +12 -0
  107. package/src/lib/primitives/divider-base.component.ts +74 -0
  108. package/src/lib/primitives/dropdown-arrow.component.test.ts +12 -0
  109. package/src/lib/primitives/dropdown-arrow.component.ts +46 -0
  110. package/src/lib/primitives/dropdown-base.component.test.ts +49 -0
  111. package/src/lib/primitives/dropdown-base.component.ts +329 -0
  112. package/src/lib/primitives/helpers.ts +151 -0
  113. package/src/lib/primitives/horizontal-loader.component.test.ts +19 -0
  114. package/src/lib/primitives/horizontal-loader.component.ts +70 -0
  115. package/src/lib/primitives/icon-base.component.test.ts +19 -0
  116. package/src/lib/primitives/icon-base.component.ts +58 -0
  117. package/src/lib/primitives/img-loader.component.test.ts +12 -0
  118. package/src/lib/primitives/img-loader.component.ts +118 -0
  119. package/src/lib/primitives/index.ts +29 -0
  120. package/src/lib/primitives/input-base.component.test.ts +20 -0
  121. package/src/lib/primitives/input-base.component.ts +210 -0
  122. package/src/lib/primitives/lifetime-slider-base.component.test.ts +88 -0
  123. package/src/lib/primitives/lifetime-slider-base.component.ts +75 -0
  124. package/src/lib/primitives/oval-spinner.component.test.ts +9 -0
  125. package/src/lib/primitives/oval-spinner.component.ts +158 -0
  126. package/src/lib/primitives/pickers/dropdown-picker-animated-base.component.ts +220 -0
  127. package/src/lib/primitives/pickers/picker-button.component.ts +74 -0
  128. package/src/lib/primitives/pickers/time-picker-dropdown-selector.component.ts +188 -0
  129. package/src/lib/primitives/pickers/time-picker.component.ts +124 -0
  130. package/src/lib/primitives/pickers/time-unit-selector.component.ts +82 -0
  131. package/src/lib/primitives/progressbar.component.test.ts +18 -0
  132. package/src/lib/primitives/progressbar.component.ts +93 -0
  133. package/src/lib/primitives/providersStyles.ts +268 -0
  134. package/src/lib/primitives/slider-base.component.ts +56 -0
  135. package/src/lib/provide-block-auth.test.ts +43 -0
  136. package/src/lib/provide-block-auth.ts +43 -0
  137. package/src/lib/runtime/block-auth-runtime.directive.test.ts +89 -0
  138. package/src/lib/runtime/block-auth-runtime.directive.ts +68 -0
  139. package/src/lib/teleport-body.directive.test.ts +29 -0
  140. package/src/lib/teleport-body.directive.ts +21 -0
  141. package/src/lib/testing.ts +15 -0
  142. package/src/lib/transitions.css +23 -0
  143. package/src/public-api.ts +85 -0
  144. package/src/vitest.setup.ts +8 -0
  145. package/tailwind.config.cjs +6 -0
  146. package/tsconfig.json +15 -0
  147. package/tsconfig.lib.json +9 -0
  148. package/vite.config.mts +44 -0
@@ -0,0 +1,187 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ input,
7
+ output,
8
+ signal,
9
+ } from '@angular/core';
10
+
11
+ // `handleParamChange`/`onValidationChange` (callback props en react) -> outputs
12
+ // `paramChange`/`validationChange`: notificaciones puras de "algo cambió en la
13
+ // UI de este componente" (a diferencia de `closeTxns`/`addError` en Txn, que
14
+ // son funciones orquestadas imperativamente por el padre). `paramChange` emite
15
+ // `{ slug, value }` (un output Angular emite un único valor; se agrupa el par).
16
+ //
17
+ // El `useEffect(() => onValidationChange(allRequiredFilled), [allRequiredFilled])`
18
+ // de react -> `effect()` de Angular: emite el valor inicial tras el primer CD
19
+ // (igual que el efecto de react corre tras el primer render) y en cada cambio.
20
+ export interface ParamChange {
21
+ slug: string;
22
+ value: string;
23
+ }
24
+
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',
32
+ };
33
+
34
+ const PARAM_DISPLAY_ORDER = [
35
+ 'users-firstname',
36
+ 'users-lastname',
37
+ 'users-secondlastname',
38
+ 'users-email',
39
+ 'users-phone',
40
+ 'modules-kyc',
41
+ ];
42
+
43
+ const PARAM_INPUT_TYPE: Record<string, string> = {
44
+ 'users-firstname': 'text',
45
+ 'users-lastname': 'text',
46
+ 'users-secondlastname': 'text',
47
+ 'users-email': 'email',
48
+ 'users-phone': 'tel',
49
+ };
50
+
51
+ const PARAM_VALIDATORS: Record<string, (v: string) => boolean> = {
52
+ 'users-firstname': (v) => /^[A-Za-zÀ-ÖØ-öø-ÿ\s'-]{1,50}$/.test(v.trim()),
53
+ 'users-lastname': (v) => /^[A-Za-zÀ-ÖØ-öø-ÿ\s'-]{1,50}$/.test(v.trim()),
54
+ 'users-secondlastname': (v) => /^[A-Za-zÀ-ÖØ-öø-ÿ\s'-]{1,50}$/.test(v.trim()),
55
+ 'users-email': (v) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v.trim()),
56
+ 'users-phone': (v) => /^\+?[\d\s\-().]{6,20}$/.test(v.trim()),
57
+ };
58
+
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',
65
+ };
66
+
67
+ @Component({
68
+ selector: 'ba-solicited-data-modal',
69
+ standalone: true,
70
+ changeDetection: ChangeDetectionStrategy.OnPush,
71
+ template: `
72
+ <div
73
+ 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
+ @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>
115
+ @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>
119
+ }
120
+ </div>
121
+ }
122
+ @if (!allRequiredFilled()) {
123
+ <p class="bas-m-0 bas-text-center bas-text-xs bas-text-amber-600">{{ fillRequiredTxt() }}</p>
124
+ }
125
+ </div>
126
+ `,
127
+ })
128
+ export class SolicitedDataModalComponent {
129
+ requiredParams = input<string[]>([]);
130
+ optionalParams = input<string[]>([]);
131
+ userParamValues = input<Record<string, string>>({});
132
+ requiredLabelTxt = input('Required');
133
+ optionalLabelTxt = input('Optional');
134
+ fillRequiredTxt = input('Fill required fields to continue');
135
+
136
+ paramChange = output<ParamChange>();
137
+ validationChange = output<boolean>();
138
+
139
+ private readonly touched = signal<Record<string, boolean>>({});
140
+
141
+ protected readonly allRequiredFilled = computed(() =>
142
+ this.requiredParams().every((slug) => {
143
+ if (slug === 'modules-kyc') return true;
144
+ const value = this.userParamValues()[slug]?.trim();
145
+ if (!value) return false;
146
+ const validate = PARAM_VALIDATORS[slug];
147
+ return !validate || validate(value);
148
+ }),
149
+ );
150
+
151
+ protected readonly visibleParams = computed(() =>
152
+ PARAM_DISPLAY_ORDER.filter(
153
+ (slug) =>
154
+ slug !== 'modules-kyc' &&
155
+ (this.requiredParams().includes(slug) || this.optionalParams().includes(slug)),
156
+ ),
157
+ );
158
+
159
+ constructor() {
160
+ effect(() => {
161
+ this.validationChange.emit(this.allRequiredFilled());
162
+ });
163
+ }
164
+
165
+ isRequired = (slug: string): boolean => this.requiredParams().includes(slug);
166
+ label = (slug: string): string => PARAM_LABELS[slug] ?? slug;
167
+ inputType = (slug: string): string => PARAM_INPUT_TYPE[slug] || 'text';
168
+ valueOf = (slug: string): string => this.userParamValues()[slug] || '';
169
+
170
+ private getError(slug: string, value: string): string | null {
171
+ if (!value) return null;
172
+ const validate = PARAM_VALIDATORS[slug];
173
+ if (validate && !validate(value)) return PARAM_ERROR_MSG[slug];
174
+ return null;
175
+ }
176
+
177
+ errorOf = (slug: string): string | null =>
178
+ this.touched()[slug] ? this.getError(slug, this.valueOf(slug)) : null;
179
+
180
+ onBlur(slug: string): void {
181
+ this.touched.update((prev) => ({ ...prev, [slug]: true }));
182
+ }
183
+
184
+ onInput(slug: string, event: Event): void {
185
+ this.paramChange.emit({ slug, value: (event.target as HTMLInputElement).value });
186
+ }
187
+ }
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen, fireEvent } from '@testing-library/angular';
4
+ import { TxnErrorComponent } from './txn-error.component';
5
+ import { provideBlockAuthClientForTest } from '../../../testing';
6
+
7
+ vi.mock('@wagmi/core', () => ({
8
+ createConfig: vi.fn(() => ({ connectors: [] })),
9
+ connect: vi.fn(), disconnect: vi.fn(), getAccount: vi.fn(() => ({})),
10
+ signMessage: vi.fn(), watchAccount: vi.fn(() => () => undefined), http: vi.fn(),
11
+ }));
12
+ vi.mock('@wagmi/connectors', () => ({
13
+ injected: vi.fn(() => ({ id: 'injected' })), walletConnect: vi.fn(() => ({ id: 'walletConnect' })),
14
+ }));
15
+
16
+ // Port de packages/react/src/components/txns/internals/TxnError.test.tsx +
17
+ // TxnActions.behavior.test.tsx (REPORT -> client.txns.report). Mapa: Vue.
18
+ describe('TxnError', () => {
19
+ function makeClient(report = vi.fn()) {
20
+ return { txns: { report } } as any;
21
+ }
22
+
23
+ it('renderiza el panel de error de txn', async () => {
24
+ await render(TxnErrorComponent, {
25
+ inputs: { errorMessageTxt: 'No transactions found' },
26
+ providers: [provideBlockAuthClientForTest(makeClient())],
27
+ });
28
+ expect(screen.getByText('No transactions found')).toBeDefined();
29
+ expect(screen.getByText('REPORT')).toBeDefined();
30
+ });
31
+
32
+ it('REPORT llama a client.txns.report(slug)', async () => {
33
+ const report = vi.fn();
34
+ await render(TxnErrorComponent, {
35
+ inputs: { errorMessageTxt: 'No transactions found', errorMessageSlug: 'no_transactions_found' },
36
+ providers: [provideBlockAuthClientForTest(makeClient(report))],
37
+ });
38
+ await fireEvent.click(screen.getByText('REPORT'));
39
+ expect(report).toHaveBeenCalledWith('no_transactions_found');
40
+ });
41
+
42
+ describe('countdown tras REPORT', () => {
43
+ beforeEach(() => {
44
+ vi.useFakeTimers();
45
+ });
46
+ afterEach(() => {
47
+ vi.useRealTimers();
48
+ });
49
+
50
+ it('muestra "Thanks for reporting" y cuenta hasta cerrar via closeTxns', async () => {
51
+ const closeTxns = vi.fn();
52
+ @Component({
53
+ standalone: true,
54
+ imports: [TxnErrorComponent],
55
+ template: `<ba-txn-error errorMessageTxt="No transactions found" [closeTxns]="c" />`,
56
+ })
57
+ class Host {
58
+ c = closeTxns;
59
+ }
60
+ const { fixture } = await render(Host, {
61
+ providers: [provideBlockAuthClientForTest(makeClient())],
62
+ });
63
+
64
+ await fireEvent.click(screen.getByText('REPORT'));
65
+ await vi.advanceTimersByTimeAsync(1500);
66
+ fixture.detectChanges();
67
+ expect(screen.getByText('Thanks for reporting the error!')).toBeDefined();
68
+
69
+ // 10s countdown, ticking every second; closeTxns fires when it hits 0.
70
+ await vi.advanceTimersByTimeAsync(9000);
71
+ expect(closeTxns).not.toHaveBeenCalled();
72
+ await vi.advanceTimersByTimeAsync(1000);
73
+ expect(closeTxns).toHaveBeenCalledOnce();
74
+ });
75
+
76
+ it('con isCountdown=false no arranca el countdown ni cierra solo', async () => {
77
+ const closeTxns = vi.fn();
78
+ @Component({
79
+ standalone: true,
80
+ imports: [TxnErrorComponent],
81
+ template: `<ba-txn-error errorMessageTxt="No transactions found" [closeTxns]="c" [isCountdown]="false" />`,
82
+ })
83
+ class Host {
84
+ c = closeTxns;
85
+ }
86
+ const { fixture } = await render(Host, {
87
+ providers: [provideBlockAuthClientForTest(makeClient())],
88
+ });
89
+
90
+ await fireEvent.click(screen.getByText('REPORT'));
91
+ await vi.advanceTimersByTimeAsync(1500);
92
+ await vi.advanceTimersByTimeAsync(20000);
93
+ fixture.detectChanges();
94
+ expect(closeTxns).not.toHaveBeenCalled();
95
+
96
+ await fireEvent.click(screen.getByText('Close'));
97
+ expect(closeTxns).toHaveBeenCalledOnce();
98
+ });
99
+ });
100
+ });
@@ -0,0 +1,121 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ DestroyRef,
5
+ inject,
6
+ input,
7
+ signal,
8
+ } from '@angular/core';
9
+ import { injectBlockAuthClient } from '../../../provide-block-auth';
10
+
11
+ // `closeTxns` se mantiene como INPUT DE FUNCIÓN (no output): react lo invoca
12
+ // imperativamente tanto desde el botón "Close" como desde dentro del
13
+ // setInterval del countdown — no es un evento de UI disparado por una
14
+ // interacción directa de este componente, sino una función que este orquesta
15
+ // a su propio ritmo (misma convención que Txn/TxnsOverview para este prop).
16
+ //
17
+ // Deviación deliberada (consistente con el port Vue): se limpian el
18
+ // timeout/interval pendientes vía `DestroyRef` para no mutar signals de un
19
+ // componente ya destruido en tests con temporizadores reales/fake.
20
+ @Component({
21
+ selector: 'ba-txn-error',
22
+ standalone: true,
23
+ changeDetection: ChangeDetectionStrategy.OnPush,
24
+ template: `
25
+ <div
26
+ class="bas-box-border bas-flex bas-h-full bas-max-w-full bas-flex-col bas-items-center bas-gap-8 bas-px-4 bas-pt-6 bas-text-center bas-text-xl bas-font-medium bas-text-[var(--bal-primary-800)]"
27
+ >
28
+ <div class="bas-h-20 bas-w-20 bas-text-red-700" aria-hidden="true">⚠</div>
29
+ <div
30
+ class="bal-ba-transition-2 bal-ba-shadow-base bal-ba-hover-shadow-lg bas-box-border bas-flex bas-w-full bas-cursor-default bas-items-center bas-justify-center bas-rounded-md bas-border-2 bas-border-solid bas-border-red-600 bas-bg-red-50 bas-px-2 bas-py-4 bas-text-2xl bas-text-[var(--bal-primary-600)] hover:bas-border-red-800 hover:bas-bg-red-100 hover:bas-text-[var(--bal-primary-900)]"
31
+ >
32
+ <p class="bas-m-0">{{ errorMessageTxt() }}</p>
33
+ </div>
34
+ <div class="bas-box-border bas-flex bas-w-full bas-flex-col bas-items-center bas-justify-center bas-gap-1 bas-px-1">
35
+ <div class="bas-h-16 bas-w-full">
36
+ @if (!reportSent()) {
37
+ <p class="bas-m-0">{{ helpUsTxt() }}</p>
38
+ } @else {
39
+ <p class="bas-m-0">{{ thanksForReportTxt() }}</p>
40
+ }
41
+ </div>
42
+ @if (!reportSent()) {
43
+ <button
44
+ type="button"
45
+ id="btn-txn-report-error"
46
+ [disabled]="sendingReport()"
47
+ class="bal-ba-hover-ring-red-500 bal-ba-focus-ring-red-500 bas-mt-2 bas-w-[220px] bas-justify-center bas-bg-red-500 hover:bas-bg-red-600 bas-px-4 bas-text-center bas-text-xl bas-font-medium bas-text-[var(--bal-primary-10)]"
48
+ (click)="onReportErrorClicked()"
49
+ >
50
+ {{ sendingReport() ? '…' : reportTxt() }}
51
+ </button>
52
+ } @else {
53
+ <button
54
+ type="button"
55
+ id="btn-txn-error-report"
56
+ class="bal-ba-hover-ring-primary-300 bal-ba-focus-ring-primary-300 bas-mt-2 bas-w-[220px] bas-justify-center bas-rounded-md bas-border-2 bas-border-solid bas-border-[var(--bal-primary-300)] bas-bg-transparent bas-px-4 bas-text-center bas-text-xl bas-font-medium bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]"
57
+ (click)="close()"
58
+ >
59
+ {{ textCloseTxt() }}
60
+ </button>
61
+ }
62
+ </div>
63
+ @if (reportSent() && isCountdown()) {
64
+ <div class="bas-flex bas-flex-row bas-items-end bas-gap-2">
65
+ <p class="bas-m-0">{{ modalWillCloseInTxt() }}</p>
66
+ <p class="bas-m-0">{{ countDown() }}</p>
67
+ </div>
68
+ }
69
+ </div>
70
+ `,
71
+ })
72
+ export class TxnErrorComponent {
73
+ errorMessageSlug = input('unknown');
74
+ errorMessageTxt = input('unknown');
75
+ closeTxns = input<() => void>(() => {});
76
+ helpUsTxt = input('Help us get better and report the error here');
77
+ thanksForReportTxt = input('Thanks for reporting the error!');
78
+ reportTxt = input('REPORT');
79
+ textCloseTxt = input('Close');
80
+ modalWillCloseInTxt = input('This modal will close in...');
81
+ isCountdown = input(true);
82
+
83
+ private readonly client = injectBlockAuthClient();
84
+
85
+ protected readonly sendingReport = signal(false);
86
+ protected readonly reportSent = signal(false);
87
+ protected readonly countDown = signal(10);
88
+
89
+ private reportTimeout: ReturnType<typeof setTimeout> | undefined;
90
+ private countdownInterval: ReturnType<typeof setInterval> | undefined;
91
+
92
+ constructor() {
93
+ inject(DestroyRef).onDestroy(() => {
94
+ if (this.reportTimeout) clearTimeout(this.reportTimeout);
95
+ if (this.countdownInterval) clearInterval(this.countdownInterval);
96
+ });
97
+ }
98
+
99
+ close(): void {
100
+ this.closeTxns()();
101
+ }
102
+
103
+ onReportErrorClicked(): void {
104
+ this.sendingReport.set(true);
105
+ this.client.txns.report(this.errorMessageSlug());
106
+ this.reportTimeout = setTimeout(() => {
107
+ this.sendingReport.set(false);
108
+ this.reportSent.set(true);
109
+ if (!this.isCountdown()) return;
110
+ this.countdownInterval = setInterval(() => {
111
+ if (this.countDown() === 1) {
112
+ this.closeTxns()();
113
+ clearInterval(this.countdownInterval);
114
+ this.countDown.set(0);
115
+ return;
116
+ }
117
+ this.countDown.update((v) => v - 1);
118
+ }, 1000);
119
+ }, 1500);
120
+ }
121
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen, fireEvent } from '@testing-library/angular';
4
+ import { TxnTypeComponent } from './txn-type.component';
5
+
6
+ // Port de packages/react/src/components/txns/internals/TxnType.test.tsx (mapa:
7
+ // packages/vue/.../TxnType.test.ts). Componente presentacional, sin client.
8
+ describe('TxnType', () => {
9
+ it('renderiza el tipo de txn con navegacion', async () => {
10
+ @Component({
11
+ standalone: true,
12
+ imports: [TxnTypeComponent],
13
+ template: `<ba-txn-type txnType="Log In" [txnsLength]="2" />`,
14
+ })
15
+ class Host {}
16
+ await render(Host);
17
+ expect(screen.getByText('Log In')).toBeDefined();
18
+ expect(screen.getByLabelText('next transaction')).toBeDefined();
19
+ expect(screen.getByLabelText('previous transaction')).toBeDefined();
20
+ });
21
+
22
+ it('no muestra flechas de navegacion cuando solo hay una txn', async () => {
23
+ @Component({
24
+ standalone: true,
25
+ imports: [TxnTypeComponent],
26
+ template: `<ba-txn-type txnType="Log In" [txnsLength]="1" />`,
27
+ })
28
+ class Host {}
29
+ await render(Host);
30
+ expect(screen.queryByLabelText('next transaction')).toBeNull();
31
+ expect(screen.queryByLabelText('previous transaction')).toBeNull();
32
+ });
33
+
34
+ it('emite prevTxnClicked/nextTxnClicked al hacer click en las flechas', async () => {
35
+ const onPrev = vi.fn();
36
+ const onNext = vi.fn();
37
+ @Component({
38
+ standalone: true,
39
+ imports: [TxnTypeComponent],
40
+ template: `<ba-txn-type txnType="Log In" [txnsLength]="2" (prevTxnClicked)="p()" (nextTxnClicked)="n()" />`,
41
+ })
42
+ class Host {
43
+ p = onPrev;
44
+ n = onNext;
45
+ }
46
+ await render(Host);
47
+ await fireEvent.click(screen.getByLabelText('previous transaction'));
48
+ await fireEvent.click(screen.getByLabelText('next transaction'));
49
+ expect(onPrev).toHaveBeenCalledOnce();
50
+ expect(onNext).toHaveBeenCalledOnce();
51
+ });
52
+ });
@@ -0,0 +1,51 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+
3
+ // Puramente presentacional: sin
4
+ // lógica de core. `onPrevTxnClicked`/`onNextTxnClicked` (callback props en
5
+ // react) -> outputs `prevTxnClicked`/`nextTxnClicked` (eventos de click, no
6
+ // funciones invocadas imperativamente por el padre), igual que el port Vue.
7
+ // Las flechas `&#8249;`/`&#8250;` (‹ ›) se escriben como caracteres literales.
8
+ @Component({
9
+ selector: 'ba-txn-type',
10
+ standalone: true,
11
+ changeDetection: ChangeDetectionStrategy.OnPush,
12
+ template: `
13
+ <div
14
+ [class]="
15
+ (txnsLength() > 1 ? 'bas-grid bas-grid-cols-[28px_1fr_28px]' : 'bas-flex') +
16
+ ' bas-group bas-z-0 bas-w-full bas-items-center bas-justify-between bas-gap-1'
17
+ "
18
+ >
19
+ @if (txnsLength() > 1) {
20
+ <span
21
+ aria-label="previous transaction"
22
+ class="bal-ba-transition-2 bas-flex bas-h-7 bas-w-7 bas-cursor-pointer bas-items-center bas-justify-center bas-rounded-full hover:bas-bg-[#e7ecef] bas-text-[var(--bal-primary-500)] group-hover:bas-text-[var(--bal-primary-700)]"
23
+ (click)="prevTxnClicked.emit()"
24
+ >‹</span
25
+ >
26
+ }
27
+ <div class="bas-flex bas-w-full bas-items-center bas-justify-center bas-gap-3">
28
+ <span class="bal-ba-transition-2 bas-h-px bas-flex-1 bas-bg-[var(--bal-primary-300)] group-hover:bas-bg-[var(--bal-primary-500)]"></span>
29
+ <p class="bal-ba-transition-2 bas-m-0 bas-cursor-default bas-text-center bas-text-lg bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]">
30
+ {{ txnType() }}
31
+ </p>
32
+ <span class="bal-ba-transition-2 bas-h-px bas-flex-1 bas-bg-[var(--bal-primary-300)] group-hover:bas-bg-[var(--bal-primary-500)]"></span>
33
+ </div>
34
+ @if (txnsLength() > 1) {
35
+ <span
36
+ aria-label="next transaction"
37
+ class="bal-ba-transition-2 bas-flex bas-h-7 bas-w-7 bas-cursor-pointer bas-items-center bas-justify-center bas-rounded-full hover:bas-bg-[#e7ecef] bas-text-[var(--bal-primary-500)] group-hover:bas-text-[var(--bal-primary-700)]"
38
+ (click)="nextTxnClicked.emit()"
39
+ >›</span
40
+ >
41
+ }
42
+ </div>
43
+ `,
44
+ })
45
+ export class TxnTypeComponent {
46
+ txnType = input('');
47
+ txnsLength = input(1);
48
+
49
+ prevTxnClicked = output<void>();
50
+ nextTxnClicked = output<void>();
51
+ }