@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,81 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Component, input } from '@angular/core';
3
+ import { render, screen, fireEvent } from '@testing-library/angular';
4
+ import { ModalOverlayComponent } from './modal-overlay.component';
5
+
6
+ // Port de packages/react/src/components/modals/ModalOverlay.tsx (mapa:
7
+ // packages/vue/src/components/modals/ModalOverlay.test.ts): verifica el
8
+ // contrato de portal + backdrop-click + slot que comparten los 4 modales.
9
+ // Los signal inputs no se pueden fijar por `componentInputs` de forma fiable,
10
+ // así que se usa un host component con binding de template.
11
+ @Component({
12
+ selector: 'ba-overlay-host',
13
+ standalone: true,
14
+ imports: [ModalOverlayComponent],
15
+ template: `
16
+ <ba-modal-overlay [open]="open()" testId="modal-x" (closed)="onClose()">
17
+ <div data-testid="panel-content">hola</div>
18
+ </ba-modal-overlay>
19
+ `,
20
+ })
21
+ class OverlayHostComponent {
22
+ open = input(false);
23
+ onClose = (): void => undefined;
24
+ }
25
+
26
+ describe('ModalOverlay', () => {
27
+ it('no renderiza nada cuando open es false', async () => {
28
+ await render(OverlayHostComponent);
29
+ expect(screen.queryByTestId('modal-x')).toBeNull();
30
+ });
31
+
32
+ it('se monta en document.body (portal) y renderiza el slot cuando open es true', async () => {
33
+ @Component({
34
+ selector: 'ba-overlay-host-open',
35
+ standalone: true,
36
+ imports: [ModalOverlayComponent],
37
+ template: `
38
+ <ba-modal-overlay [open]="true" testId="modal-x">
39
+ <div data-testid="panel-content">hola</div>
40
+ </ba-modal-overlay>
41
+ `,
42
+ })
43
+ class HostOpen {}
44
+ await render(HostOpen);
45
+ const overlay = screen.getByTestId('modal-x');
46
+ expect(overlay).toBeDefined();
47
+ expect(overlay.parentElement).toBe(document.body);
48
+ expect(screen.getByTestId('panel-content').textContent).toBe('hola');
49
+ });
50
+
51
+ it('el panel tiene role="dialog" y aria-modal="true"', async () => {
52
+ @Component({
53
+ selector: 'ba-overlay-host-dialog',
54
+ standalone: true,
55
+ imports: [ModalOverlayComponent],
56
+ template: `<ba-modal-overlay [open]="true" testId="modal-x" />`,
57
+ })
58
+ class HostDialog {}
59
+ await render(HostDialog);
60
+ const dialog = screen.getByRole('dialog');
61
+ expect(dialog.getAttribute('aria-modal')).toBe('true');
62
+ });
63
+
64
+ it('al hacer click en el backdrop emite closed', async () => {
65
+ const onClose = vi.fn();
66
+ @Component({
67
+ selector: 'ba-overlay-host-close',
68
+ standalone: true,
69
+ imports: [ModalOverlayComponent],
70
+ template: `<ba-modal-overlay [open]="true" testId="modal-x" (closed)="onClose()" />`,
71
+ })
72
+ class HostClose {
73
+ onClose = onClose;
74
+ }
75
+ await render(HostClose);
76
+ const backdrop = screen.getByTestId('modal-x').querySelector('[aria-hidden="true"]');
77
+ expect(backdrop).not.toBeNull();
78
+ await fireEvent.click(backdrop!);
79
+ expect(onClose).toHaveBeenCalledOnce();
80
+ });
81
+ });
@@ -0,0 +1,43 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+ import { TeleportBodyDirective } from '../../teleport-body.directive';
3
+
4
+ // Overlay mínimo usado por los 4 modales. La fuente React usaba `<ModalBase>` de
5
+ // `@block-auth.io/blockauth-lib-react` (un Dialog headless-ui con transiciones
6
+ // + portal); esa lib aún no es dependencia de este paquete, así que este es un
7
+ // stand-in ligero que preserva las clases `bas-` del panel original y el
8
+ // contrato open/close. La visibilidad la maneja el estado core `$openModals`
9
+ // en cada modal llamador.
10
+ //
11
+ // Se monta en `document.body` vía `baTeleportBody` (equivalente a
12
+ // createPortal(..., document.body) / <Teleport to="body">): las apps host
13
+ // montan `FlowBlockAuth` dentro de contenedores transformados/animados que de
14
+ // otro modo atraparían el overlay `fixed` dentro de ese ancestro.
15
+ @Component({
16
+ selector: 'ba-modal-overlay',
17
+ standalone: true,
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ imports: [TeleportBodyDirective],
20
+ template: `
21
+ @if (open()) {
22
+ <div
23
+ baTeleportBody
24
+ [attr.data-testid]="testId()"
25
+ [class]="'bas-fixed bas-inset-0 bas-z-50 bas-bg-black/40 ' + containerClassName()"
26
+ >
27
+ <div class="bas-absolute bas-inset-0" aria-hidden="true" (click)="closed.emit()"></div>
28
+ <div [class]="'bas-relative ' + panelClassName()" role="dialog" aria-modal="true">
29
+ <ng-content />
30
+ </div>
31
+ </div>
32
+ }
33
+ `,
34
+ })
35
+ export class ModalOverlayComponent {
36
+ open = input.required<boolean>();
37
+ panelClassName = input('');
38
+ containerClassName = input(
39
+ 'bas-flex bas-min-h-full bas-items-end xmd:bas-items-baseline xmd:bas-justify-center',
40
+ );
41
+ testId = input<string | undefined>(undefined);
42
+ closed = output<void>();
43
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/angular';
3
+ import { atom } from 'nanostores';
4
+ import type { ModalName } from '@block-auth.io/core';
5
+ import { ModalRestorationExpiredComponent } from './modal-restoration-expired.component';
6
+ import { provideBlockAuthClientForTest } from '../../testing';
7
+
8
+ vi.mock('@wagmi/core', () => ({
9
+ createConfig: vi.fn(() => ({ connectors: [] })),
10
+ connect: vi.fn(), disconnect: vi.fn(), getAccount: vi.fn(() => ({})),
11
+ signMessage: vi.fn(), watchAccount: vi.fn(() => () => undefined), http: vi.fn(),
12
+ }));
13
+ vi.mock('@wagmi/connectors', () => ({
14
+ injected: vi.fn(() => ({ id: 'injected' })), walletConnect: vi.fn(() => ({ id: 'walletConnect' })),
15
+ }));
16
+
17
+ // ModalRestorationExpired inyecta el client directamente (BlockAuthService no
18
+ // expone finalizeRestorationExpired()), así que el fake sólo necesita
19
+ // state.$openModals + los métodos.
20
+ function makeClient() {
21
+ const $openModals = atom<ModalName[]>([]);
22
+ const openModal = (m: ModalName) => $openModals.set([...$openModals.get(), m]);
23
+ const closeModal = vi.fn(() => $openModals.set([]));
24
+ const finalizeRestorationExpired = vi.fn();
25
+ const client = { state: { $openModals }, openModal, closeModal, finalizeRestorationExpired } as any;
26
+ return { client, $openModals, openModal, closeModal, finalizeRestorationExpired };
27
+ }
28
+
29
+ // Port de packages/react/src/components/modals/ModalRestorationExpired.test.tsx.
30
+ describe('ModalRestorationExpired', () => {
31
+ it('no renderiza cuando el modal está cerrado', async () => {
32
+ const { client } = makeClient();
33
+ await render(ModalRestorationExpiredComponent, { providers: [provideBlockAuthClientForTest(client)] });
34
+ expect(screen.queryByTestId('modal-restoration-expired')).toBeNull();
35
+ });
36
+
37
+ it('renderiza el contenido cuando el modal restorationExpired está abierto', async () => {
38
+ const { client } = makeClient();
39
+ client.openModal('restorationExpired');
40
+ await render(ModalRestorationExpiredComponent, { providers: [provideBlockAuthClientForTest(client)] });
41
+ expect(screen.getByTestId('modal-restoration-expired')).toBeDefined();
42
+ expect(screen.getByText('Restoration Period Expired')).toBeDefined();
43
+ });
44
+
45
+ it('al hacer click en "Continue" llama finalizeRestorationExpired() y luego closeModal()', async () => {
46
+ const { client } = makeClient();
47
+ const callOrder: string[] = [];
48
+ client.finalizeRestorationExpired = vi.fn(() => callOrder.push('finalize'));
49
+ client.closeModal = vi.fn(() => callOrder.push('closeModal'));
50
+ client.openModal('restorationExpired');
51
+
52
+ await render(ModalRestorationExpiredComponent, { providers: [provideBlockAuthClientForTest(client)] });
53
+ await fireEvent.click(screen.getByRole('button', { name: /continue/i }));
54
+
55
+ expect(client.finalizeRestorationExpired).toHaveBeenCalledOnce();
56
+ expect(client.closeModal).toHaveBeenCalledOnce();
57
+ expect(callOrder).toEqual(['finalize', 'closeModal']);
58
+ });
59
+
60
+ it('al hacer click en el backdrop también llama finalizeRestorationExpired() y closeModal()', async () => {
61
+ const { client, finalizeRestorationExpired, closeModal } = makeClient();
62
+ client.openModal('restorationExpired');
63
+
64
+ await render(ModalRestorationExpiredComponent, { providers: [provideBlockAuthClientForTest(client)] });
65
+ const backdrop = screen.getByTestId('modal-restoration-expired').querySelector('[aria-hidden="true"]');
66
+ await fireEvent.click(backdrop!);
67
+
68
+ expect(finalizeRestorationExpired).toHaveBeenCalledOnce();
69
+ expect(closeModal).toHaveBeenCalledOnce();
70
+ });
71
+ });
@@ -0,0 +1,83 @@
1
+ import { ChangeDetectionStrategy, Component, computed } from '@angular/core';
2
+ import { ModalOverlayComponent } from './modal-overlay.component';
3
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
4
+ import { PiWarningFillComponent, FaArrowRightComponent } from '../../icons';
5
+ import { injectBlockAuthClient } from '../../provide-block-auth';
6
+ import { injectStore } from '../../inject-store';
7
+
8
+ const TEXTS = {
9
+ title: 'Restoration Period Expired',
10
+ message:
11
+ 'Account permanently deleted, please complete the registration again to use Blockauth again',
12
+ button: 'Continue',
13
+ };
14
+
15
+ /**
16
+ * Modal de restauración expirada fino, dirigido por el core. Visibilidad por
17
+ * `openModals().includes('restorationExpired')`. Confirmar (o backdrop) ejecuta
18
+ * `finalizeRestorationExpired()` (limpia el storage de auth restante y resetea
19
+ * los flags de borrado/firma) y luego `closeModal()`.
20
+ *
21
+ * El `text` de React era un ReactNode (label + icono); igual que en el port Vue
22
+ * se usa el slot de contenido por defecto de `<ba-btn-base>` (`isIcon=false`)
23
+ * para renderizar la etiqueta + la flecha, en vez de la prop `text`.
24
+ *
25
+ * Se inyecta el client directamente (no via BlockAuthService) porque
26
+ * `finalizeRestorationExpired()` no está expuesto en el facade.
27
+ */
28
+ @Component({
29
+ selector: 'ba-modal-restoration-expired',
30
+ standalone: true,
31
+ changeDetection: ChangeDetectionStrategy.OnPush,
32
+ imports: [ModalOverlayComponent, BtnBaseComponent, PiWarningFillComponent, FaArrowRightComponent],
33
+ template: `
34
+ <ba-modal-overlay
35
+ testId="modal-restoration-expired"
36
+ [open]="open()"
37
+ containerClassName="bas-flex bas-h-full bas-items-end bas-justify-center md:bas-items-center"
38
+ panelClassName="bas-overflow-hidden bas-bg-white bas-rounded-2xl bas-w-[90%] sm:bas-max-w-[540px]"
39
+ (closed)="onOkClicked()"
40
+ >
41
+ <header class="bas-w-full bas-border-b-2 bas-border-solid bas-border-[--bal-primary-100] bas-bg-[--bg-200] bas-py-6 bas-px-8 bas-text-center sm:bas-py-10">
42
+ <h3 class="bas-text-lg bas-font-bold bas-text-[--bal-primary-900] sm:bas-text-2xl">{{ TEXTS.title }}</h3>
43
+ </header>
44
+ <main class="bas-flex bas-flex-col bas-gap-8 bas-p-8 sm:bas-gap-10 sm:bas-p-12">
45
+ <div class="bas-flex bas-flex-col bas-items-center bas-gap-6 bas-text-center">
46
+ <ba-pi-warning-fill class="bas-h-12 bas-w-12 bas-text-black sm:bas-h-20 sm:bas-w-20" />
47
+ <article class="bas-text-sm bas-font-medium bas-leading-relaxed bas-text-black sm:bas-text-base">{{ TEXTS.message }}</article>
48
+ </div>
49
+ <footer class="bas-flex bas-w-full bas-justify-center">
50
+ <ba-btn-base
51
+ btnId="btn-ok-restoration-expired"
52
+ size="small"
53
+ bgClassNames="bas-bg-blue-900"
54
+ ringClassNames="bal-ba-hover-ring-blue-600 bal-ba-focus-ring-blue-600"
55
+ textClassNames="bas-text-white bas-font-semibold"
56
+ classNames="bas-w-full bas-justify-center bas-shadow-md"
57
+ paddingClassNames="bas-py-3"
58
+ [isIcon]="false"
59
+ [disabled]="!open()"
60
+ (action)="onOkClicked()"
61
+ >
62
+ <div class="bas-flex bas-items-center bas-gap-2">
63
+ <span class="bas-uppercase bas-tracking-wider">{{ TEXTS.button }}</span>
64
+ <ba-fa-arrow-right class="bas-text-white" />
65
+ </div>
66
+ </ba-btn-base>
67
+ </footer>
68
+ </main>
69
+ </ba-modal-overlay>
70
+ `,
71
+ })
72
+ export class ModalRestorationExpiredComponent {
73
+ protected readonly TEXTS = TEXTS;
74
+
75
+ private readonly client = injectBlockAuthClient();
76
+ private readonly openModals = injectStore(this.client.state.$openModals);
77
+ readonly open = computed(() => this.openModals().includes('restorationExpired'));
78
+
79
+ onOkClicked(): void {
80
+ this.client.finalizeRestorationExpired();
81
+ this.client.closeModal();
82
+ }
83
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/angular';
3
+ import { atom } from 'nanostores';
4
+ import type { ModalName, RestoreResult } from '@block-auth.io/core';
5
+ import { ModalRestoreComponent } from './modal-restore.component';
6
+ import { provideBlockAuthClientForTest } from '../../testing';
7
+
8
+ vi.mock('@wagmi/core', () => ({
9
+ createConfig: vi.fn(() => ({ connectors: [] })),
10
+ connect: vi.fn(), disconnect: vi.fn(), getAccount: vi.fn(() => ({})),
11
+ signMessage: vi.fn(), watchAccount: vi.fn(() => () => undefined), http: vi.fn(),
12
+ }));
13
+ vi.mock('@wagmi/connectors', () => ({
14
+ injected: vi.fn(() => ({ id: 'injected' })), walletConnect: vi.fn(() => ({ id: 'walletConnect' })),
15
+ }));
16
+
17
+ // ModalRestore inyecta el client directamente (BlockAuthService no expone
18
+ // restore()), así que el fake sólo necesita state.$openModals + los métodos.
19
+ function makeClient() {
20
+ const $openModals = atom<ModalName[]>([]);
21
+ const openModal = (m: ModalName) => $openModals.set([...$openModals.get(), m]);
22
+ const closeModal = vi.fn(() => $openModals.set([]));
23
+ const restore = vi.fn((): Promise<RestoreResult> => Promise.resolve({ success: true }));
24
+ const client = { state: { $openModals }, openModal, closeModal, restore } as any;
25
+ return { client, $openModals, openModal, closeModal, restore };
26
+ }
27
+
28
+ // Port de packages/react/src/components/modals/ModalRestore.test.tsx.
29
+ describe('ModalRestore', () => {
30
+ it('no renderiza cuando el modal está cerrado', async () => {
31
+ const { client } = makeClient();
32
+ await render(ModalRestoreComponent, { providers: [provideBlockAuthClientForTest(client)] });
33
+ expect(screen.queryByTestId('modal-restore')).toBeNull();
34
+ });
35
+
36
+ it('renderiza el contenido cuando el modal restore está abierto', async () => {
37
+ const { client } = makeClient();
38
+ client.openModal('restore');
39
+ await render(ModalRestoreComponent, { providers: [provideBlockAuthClientForTest(client)] });
40
+ expect(screen.getByTestId('modal-restore')).toBeDefined();
41
+ expect(screen.getByText('Do you want to restore your account?')).toBeDefined();
42
+ });
43
+
44
+ it('click en "No" llama solo client.closeModal(), sin invocar restore()', async () => {
45
+ const { client, closeModal, restore } = makeClient();
46
+ client.openModal('restore');
47
+ await render(ModalRestoreComponent, { providers: [provideBlockAuthClientForTest(client)] });
48
+ await fireEvent.click(screen.getByRole('button', { name: /no/i }));
49
+
50
+ expect(closeModal).toHaveBeenCalledOnce();
51
+ expect(restore).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it('click en "Restore" llama closeModal() antes de await restore(), en ese orden', async () => {
55
+ const { client } = makeClient();
56
+ const callOrder: string[] = [];
57
+ client.closeModal = vi.fn(() => callOrder.push('closeModal'));
58
+ let resolveRestore: (result: RestoreResult) => void;
59
+ client.restore = vi.fn(() => {
60
+ callOrder.push('restore');
61
+ return new Promise<RestoreResult>((resolve) => { resolveRestore = resolve; });
62
+ });
63
+ client.openModal('restore');
64
+
65
+ await render(ModalRestoreComponent, { providers: [provideBlockAuthClientForTest(client)] });
66
+ await fireEvent.click(screen.getByRole('button', { name: /restore/i }));
67
+
68
+ expect(client.closeModal).toHaveBeenCalledOnce();
69
+ expect(client.restore).toHaveBeenCalledOnce();
70
+ expect(callOrder).toEqual(['closeModal', 'restore']);
71
+
72
+ resolveRestore!({ success: true });
73
+ });
74
+ });
@@ -0,0 +1,104 @@
1
+ import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
2
+ import { ModalOverlayComponent } from './modal-overlay.component';
3
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
4
+ import { FaTimesComponent, FaUndoComponent } from '../../icons';
5
+ import { injectBlockAuthClient } from '../../provide-block-auth';
6
+ import { injectStore } from '../../inject-store';
7
+
8
+ const TEXTS = {
9
+ title: 'Do you want to restore your account?',
10
+ message: 'By continuing you will be able to return to your usual operations.',
11
+ yes: 'Restore',
12
+ no: 'No',
13
+ };
14
+
15
+ /**
16
+ * Modal de restauración de cuenta fino, dirigido por el core. Visibilidad por
17
+ * `openModals().includes('restore')`; el cierre delega en `closeModal()`.
18
+ * Confirmar cierra el modal y luego `await client.restore()` (el core resuelve
19
+ * la dirección de borrado/DID, emite `sdk:restore` y espera el desenlace). Los
20
+ * botones se deshabilitan mientras la restauración está en curso.
21
+ *
22
+ * Se inyecta el client directamente (no via BlockAuthService) porque `restore()`
23
+ * no está expuesto en el facade.
24
+ */
25
+ @Component({
26
+ selector: 'ba-modal-restore',
27
+ standalone: true,
28
+ changeDetection: ChangeDetectionStrategy.OnPush,
29
+ imports: [ModalOverlayComponent, BtnBaseComponent, FaTimesComponent, FaUndoComponent],
30
+ template: `
31
+ <ba-modal-overlay
32
+ testId="modal-restore"
33
+ [open]="open()"
34
+ containerClassName="bas-flex bas-h-full bas-items-end bas-justify-center md:bas-items-center"
35
+ panelClassName="bas-overflow-hidden bas-bg-white bas-rounded-2xl bas-w-[90%] sm:bas-max-w-[540px]"
36
+ (closed)="onNoClicked()"
37
+ >
38
+ <div class="bas-w-full bas-border-b-2 bas-border-solid bas-border-[--bal-primary-100] bas-bg-[--bg-200] bas-py-6 bas-px-8 sm:bas-py-10 bas-text-center">
39
+ <h3 class="bas-text-lg bas-font-bold bas-text-[--bal-primary-900] sm:bas-text-2xl">{{ TEXTS.title }}</h3>
40
+ </div>
41
+ <div class="bas-flex bas-flex-col bas-gap-8 bas-p-8 sm:bas-gap-10 sm:bas-p-12">
42
+ <div class="bas-flex bas-flex-col bas-gap-4 bas-text-center">
43
+ <p class="bas-text-base bas-text-[--bal-primary-800] sm:bas-text-lg">{{ TEXTS.message }}</p>
44
+ </div>
45
+ <div class="bas-flex bas-w-full bas-flex-col-reverse bas-gap-4 sm:bas-flex-row">
46
+ <ba-btn-base
47
+ btnId="btn-no-restore-account"
48
+ [text]="TEXTS.no"
49
+ size="small"
50
+ bgClassNames="bas-bg-blue-900"
51
+ ringClassNames="bal-ba-hover-ring-blue-600 bal-ba-focus-ring-blue-600"
52
+ textClassNames="bas-text-white bas-font-semibold bas-uppercase bas-tracking-wider"
53
+ classNames="bas-w-full bas-justify-center bas-shadow-md"
54
+ paddingClassNames="bas-py-3"
55
+ [isIcon]="true"
56
+ [disabled]="!open() || isRestoring()"
57
+ (action)="onNoClicked()"
58
+ >
59
+ <ba-fa-times slot="icon" class="bas-mr-2 bas-text-white" />
60
+ </ba-btn-base>
61
+ <ba-btn-base
62
+ btnId="btn-yes-restore-account"
63
+ [text]="TEXTS.yes"
64
+ size="small"
65
+ bgClassNames="bas-bg-green-600 hover:bas-bg-green-700"
66
+ ringClassNames="bal-ba-hover-ring-green-600 bal-ba-focus-ring-green-600"
67
+ textClassNames="bas-text-white bas-font-semibold bas-tracking-wider bas-uppercase"
68
+ classNames="bas-w-full bas-justify-center bas-shadow-lg"
69
+ paddingClassNames="bas-py-3"
70
+ [isIcon]="true"
71
+ [disabled]="!open() || isRestoring()"
72
+ (action)="onYesClicked()"
73
+ >
74
+ <ba-fa-undo slot="icon" class="bas-mr-2 bas-text-white" />
75
+ </ba-btn-base>
76
+ </div>
77
+ </div>
78
+ </ba-modal-overlay>
79
+ `,
80
+ })
81
+ export class ModalRestoreComponent {
82
+ protected readonly TEXTS = TEXTS;
83
+
84
+ private readonly client = injectBlockAuthClient();
85
+ private readonly openModals = injectStore(this.client.state.$openModals);
86
+ readonly open = computed(() => this.openModals().includes('restore'));
87
+ readonly isRestoring = signal(false);
88
+
89
+ onNoClicked(): void {
90
+ if (this.isRestoring()) return;
91
+ this.client.closeModal();
92
+ }
93
+
94
+ async onYesClicked(): Promise<void> {
95
+ if (this.isRestoring()) return;
96
+ this.isRestoring.set(true);
97
+ this.client.closeModal();
98
+ try {
99
+ await this.client.restore();
100
+ } finally {
101
+ this.isRestoring.set(false);
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/angular';
3
+ import { atom, map } from 'nanostores';
4
+ import type { ModalName } from '@block-auth.io/core';
5
+ import { ModalSigninComponent } from './modal-signin.component';
6
+ import { provideBlockAuthClientForTest } from '../../testing';
7
+
8
+ vi.mock('@wagmi/core', () => ({
9
+ createConfig: vi.fn(() => ({ connectors: [] })),
10
+ connect: vi.fn(), disconnect: vi.fn(), getAccount: vi.fn(() => ({})),
11
+ signMessage: vi.fn(), watchAccount: vi.fn(() => () => undefined), http: vi.fn(),
12
+ }));
13
+ vi.mock('@wagmi/connectors', () => ({
14
+ injected: vi.fn(() => ({ id: 'injected' })), walletConnect: vi.fn(() => ({ id: 'walletConnect' })),
15
+ }));
16
+
17
+ // Fake client con stores nanostores-backed: BlockAuthService lee $ws/$ui/$wallet/
18
+ // $openModals, así que los cuatro deben existir. `openModal`/`closeModal`/
19
+ // `cancelTxns` son spies (BlockAuthService captura closeModal/openModal por
20
+ // referencia en construcción, por eso se fijan antes de render).
21
+ function makeClient() {
22
+ const $openModals = atom<ModalName[]>([]);
23
+ const openModal = vi.fn((m: ModalName) => $openModals.set([...$openModals.get(), m]));
24
+ const closeModal = vi.fn(() => $openModals.set([]));
25
+ const cancelTxns = vi.fn();
26
+ const client = {
27
+ state: { $ws: map({}), $ui: map({}), $wallet: map({}), $openModals },
28
+ openModal, closeModal, cancelTxns,
29
+ } as any;
30
+ return { client, $openModals, openModal, closeModal, cancelTxns };
31
+ }
32
+
33
+ // Port de packages/react/src/components/modals/ModalSignin.test.tsx.
34
+ describe('ModalSignin', () => {
35
+ it('no renderiza cuando el modal está cerrado', async () => {
36
+ const { client } = makeClient();
37
+ await render(ModalSigninComponent, { providers: [provideBlockAuthClientForTest(client)] });
38
+ expect(screen.queryByTestId('modal-signin')).toBeNull();
39
+ });
40
+
41
+ it('renderiza el modal signin (teleportado a body) cuando está abierto', async () => {
42
+ const { client } = makeClient();
43
+ client.openModal('signin');
44
+ await render(ModalSigninComponent, { providers: [provideBlockAuthClientForTest(client)] });
45
+ const overlay = screen.getByTestId('modal-signin');
46
+ expect(overlay).toBeDefined();
47
+ expect(overlay.parentElement).toBe(document.body);
48
+ });
49
+
50
+ it('al cerrar (backdrop) llama cancelTxns() y luego closeModal(), en ese orden', async () => {
51
+ const { client } = makeClient();
52
+ const callOrder: string[] = [];
53
+ client.cancelTxns = vi.fn(() => callOrder.push('cancelTxns'));
54
+ client.closeModal = vi.fn(() => callOrder.push('closeModal'));
55
+ client.openModal('signin');
56
+
57
+ await render(ModalSigninComponent, { providers: [provideBlockAuthClientForTest(client)] });
58
+ const backdrop = screen.getByTestId('modal-signin').querySelector('[aria-hidden="true"]');
59
+ expect(backdrop).not.toBeNull();
60
+ await fireEvent.click(backdrop!);
61
+
62
+ expect(client.cancelTxns).toHaveBeenCalledOnce();
63
+ expect(client.closeModal).toHaveBeenCalledOnce();
64
+ expect(callOrder).toEqual(['cancelTxns', 'closeModal']);
65
+ });
66
+ });
@@ -0,0 +1,44 @@
1
+ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2
+ import { ModalOverlayComponent } from './modal-overlay.component';
3
+ import { ContentSigninComponent } from '../contents/content-signin.component';
4
+ import { BlockAuthService } from '../../block-auth.service';
5
+
6
+ /**
7
+ * Modal de sign-in fino, dirigido por el core. La visibilidad se deriva de
8
+ * `openModals().includes('signin')` y el cierre llama a `cancelTxns()` y luego
9
+ * a `closeModal()`. El ORDEN importa (idéntico al `.tsx`): `cancelTxns()`
10
+ * primero (emite `user:txns:update { state:'cancelled' }` sobre el socket) y
11
+ * `closeModal()` después (cierra localmente).
12
+ *
13
+ * El contenido real es `<ba-content-signin [closeTxns]="handleClose" />`
14
+ * (port de A9), que muestra el overview de transacciones / spinner de firma.
15
+ */
16
+ @Component({
17
+ selector: 'ba-modal-signin',
18
+ standalone: true,
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ imports: [ModalOverlayComponent, ContentSigninComponent],
21
+ template: `
22
+ <ba-modal-overlay
23
+ testId="modal-signin"
24
+ [open]="open()"
25
+ panelClassName="xmd:bas-top-24 bas-w-full xsm1:bas-w-[380px] bas-overflow-hidden bas-bg-white bas-text-left bal-ba-shadow-base bal-ba-shadow-xl bas-h-[600px] bas-rounded-tl-lg bas-rounded-tr-lg xmd:bas-rounded-br-lg xmd:bas-rounded-bl-lg"
26
+ (closed)="handleClose()"
27
+ >
28
+ <div class="bas-flex bas-h-full bas-w-full bas-items-center bas-justify-center">
29
+ <div class="bas-relative bas-h-full bas-w-fit">
30
+ <ba-content-signin [closeTxns]="handleClose" />
31
+ </div>
32
+ </div>
33
+ </ba-modal-overlay>
34
+ `,
35
+ })
36
+ export class ModalSigninComponent {
37
+ private readonly ba = inject(BlockAuthService);
38
+ readonly open = computed(() => this.ba.openModals().includes('signin'));
39
+
40
+ readonly handleClose = (): void => {
41
+ this.ba.cancelTxns();
42
+ this.ba.closeModal();
43
+ };
44
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/angular';
3
+ import { atom, map } from 'nanostores';
4
+ import type { ModalName } from '@block-auth.io/core';
5
+ import { ModalSignupComponent } from './modal-signup.component';
6
+ import { provideBlockAuthClientForTest } from '../../testing';
7
+
8
+ vi.mock('@wagmi/core', () => ({
9
+ createConfig: vi.fn(() => ({ connectors: [] })),
10
+ connect: vi.fn(), disconnect: vi.fn(), getAccount: vi.fn(() => ({})),
11
+ signMessage: vi.fn(), watchAccount: vi.fn(() => () => undefined), http: vi.fn(),
12
+ }));
13
+ vi.mock('@wagmi/connectors', () => ({
14
+ injected: vi.fn(() => ({ id: 'injected' })), walletConnect: vi.fn(() => ({ id: 'walletConnect' })),
15
+ }));
16
+
17
+ function makeClient() {
18
+ const $openModals = atom<ModalName[]>([]);
19
+ const openModal = vi.fn((m: ModalName) => $openModals.set([...$openModals.get(), m]));
20
+ const closeModal = vi.fn(() => $openModals.set([]));
21
+ const client = {
22
+ state: { $ws: map({}), $ui: map({}), $wallet: map({}), $openModals },
23
+ openModal, closeModal,
24
+ } as any;
25
+ return { client, $openModals, openModal, closeModal };
26
+ }
27
+
28
+ // Port de packages/react/src/components/modals/ModalSignup.test.tsx.
29
+ describe('ModalSignup', () => {
30
+ it('no renderiza cuando el modal está cerrado', async () => {
31
+ const { client } = makeClient();
32
+ await render(ModalSignupComponent, { providers: [provideBlockAuthClientForTest(client)] });
33
+ expect(screen.queryByTestId('modal-signup')).toBeNull();
34
+ });
35
+
36
+ it('renderiza el modal signup (teleportado a body) cuando está abierto', async () => {
37
+ const { client } = makeClient();
38
+ client.openModal('signup');
39
+ await render(ModalSignupComponent, { providers: [provideBlockAuthClientForTest(client)] });
40
+ const overlay = screen.getByTestId('modal-signup');
41
+ expect(overlay).toBeDefined();
42
+ expect(overlay.parentElement).toBe(document.body);
43
+ });
44
+
45
+ it('al hacer click en el backdrop llama client.closeModal()', async () => {
46
+ const { client, closeModal } = makeClient();
47
+ client.openModal('signup');
48
+ await render(ModalSignupComponent, { providers: [provideBlockAuthClientForTest(client)] });
49
+
50
+ const backdrop = screen.getByTestId('modal-signup').querySelector('[aria-hidden="true"]');
51
+ expect(backdrop).not.toBeNull();
52
+ await fireEvent.click(backdrop!);
53
+
54
+ expect(closeModal).toHaveBeenCalledOnce();
55
+ });
56
+ });
@@ -0,0 +1,40 @@
1
+ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2
+ import { ModalOverlayComponent } from './modal-overlay.component';
3
+ import { ContentSignupComponent } from '../contents/content-signup.component';
4
+ import { BlockAuthService } from '../../block-auth.service';
5
+
6
+ /**
7
+ * Modal de sign-up fino, dirigido por el core. La visibilidad se deriva de
8
+ * `openModals().includes('signup')` y el cierre delega en `closeModal()`.
9
+ *
10
+ * El contenido real es `<ba-content-signup [isEditable]="false"
11
+ * [isEditing]="false" />` (port de A9).
12
+ * TODO: restaurar el estado de carga `<OvalSpinner>` cuando la lib de
13
+ * componentes esté cableada en este paquete (igual que en el `.tsx`).
14
+ */
15
+ @Component({
16
+ selector: 'ba-modal-signup',
17
+ standalone: true,
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ imports: [ModalOverlayComponent, ContentSignupComponent],
20
+ template: `
21
+ <ba-modal-overlay
22
+ testId="modal-signup"
23
+ [open]="open()"
24
+ panelClassName="xmd:bas-top-24 xmd:bas-m-1 bas-min-w-fit bas-overflow-hidden bas-bg-white bas-text-left bal-ba-shadow-base bal-ba-shadow-xl xmd:bas-my-8 sm1:bas-h-[364px] bas-w-full xmd:bas-w-fit lg:bas-h-[376px] bas-h-[508px] bas-rounded-tl-lg bas-rounded-tr-lg xmd:bas-rounded-br-lg xmd:bas-rounded-bl-lg"
25
+ (closed)="onClose()"
26
+ >
27
+ <div class="bas-box-border bas-h-full bas-w-full bas-px-4 bas-pb-4 bas-pt-6 sm1:bas-pt-5 xmd:bas-pb-3.5 lg:bas-px-6 lg:bas-pb-5 lg:bas-pt-[26px]">
28
+ <ba-content-signup [isEditable]="false" [isEditing]="false" />
29
+ </div>
30
+ </ba-modal-overlay>
31
+ `,
32
+ })
33
+ export class ModalSignupComponent {
34
+ private readonly ba = inject(BlockAuthService);
35
+ readonly open = computed(() => this.ba.openModals().includes('signup'));
36
+
37
+ onClose(): void {
38
+ this.ba.closeModal();
39
+ }
40
+ }