@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,108 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ DestroyRef,
5
+ computed,
6
+ effect,
7
+ inject,
8
+ input,
9
+ output,
10
+ signal,
11
+ } from '@angular/core';
12
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
13
+ import { injectBlockAuthClient } from '../../provide-block-auth';
14
+ import { injectStore } from '../../inject-store';
15
+
16
+ /**
17
+ * Botón "Log In" genérico que dispara el flujo de firma vía
18
+ * `client.requestSignin()`.
19
+ *
20
+ * En click: guarda contra reentrada mientras hay una firma en curso
21
+ * (`isConnecting || isSigning`), marca `isSigning` + el loader del botón, llama
22
+ * a `requestSignin()` (emite `user:txns:create`) y abre el modal de signin con
23
+ * `isSigningLoader = true` para que se abra cubierto por el spinner mientras
24
+ * llegan las txns pendientes (`ContentSignin` lo limpia al recibirlas).
25
+ *
26
+ * Escucha en `document` los eventos `ext:txn:response` / `ext:txn:closed`
27
+ * (listeners registrados en el constructor, limpiados con `DestroyRef`), y un
28
+ * `effect()` sobre `ui().isSigning`.
29
+ */
30
+ @Component({
31
+ selector: 'ba-btn-signin',
32
+ standalone: true,
33
+ changeDetection: ChangeDetectionStrategy.OnPush,
34
+ imports: [BtnBaseComponent],
35
+ template: `
36
+ <ba-btn-base
37
+ btnId="btn-login-signup"
38
+ type="button-outlined"
39
+ [text]="text()"
40
+ size="small"
41
+ [isIcon]="false"
42
+ paddingClassNames="bas-px-8"
43
+ [disabled]="isDisabled() || isConnecting() || ui().isSigning"
44
+ [applyDisabledStyles]="false"
45
+ [isFocusOutlinedEnabled]="false"
46
+ textClassNames="bas-text-white bas-font-semibold bas-leading-6 bas-text-base"
47
+ [bgClassNames]="bgClassNames()"
48
+ [isLoader]="isConnecting() || ui().isSigning"
49
+ (action)="onBtnSigninClicked()"
50
+ />
51
+ `,
52
+ })
53
+ export class BtnSigninComponent {
54
+ text = input('Log In');
55
+ isDisabled = input(false);
56
+ isEditable = input(false);
57
+
58
+ clicked = output<void>();
59
+
60
+ private client = injectBlockAuthClient();
61
+ ui = injectStore(this.client.state.$ui);
62
+ isConnecting = signal(false);
63
+
64
+ bgClassNames = computed(() =>
65
+ this.isDisabled()
66
+ ? 'bas-bg-[var(--bal-primary-500)] hover:bas-bg-[var(--bal-primary-700)]'
67
+ : 'bas-bg-[rgba(0,78,152,0.8)] hover:bas-bg-[rgba(0,62,121,0.8)]',
68
+ );
69
+
70
+ constructor() {
71
+ const destroyRef = inject(DestroyRef);
72
+ const handleTxnResponse = () => this.isConnecting.set(false);
73
+ const handleTxnClosed = () => {
74
+ this.client.state.$ui.setKey('isSigning', false);
75
+ this.isConnecting.set(false);
76
+ };
77
+ document.addEventListener('ext:txn:response', handleTxnResponse);
78
+ document.addEventListener('ext:txn:closed', handleTxnClosed);
79
+ destroyRef.onDestroy(() => {
80
+ document.removeEventListener('ext:txn:response', handleTxnResponse);
81
+ document.removeEventListener('ext:txn:closed', handleTxnClosed);
82
+ });
83
+
84
+ effect(() => {
85
+ if (!this.ui().isSigning) this.isConnecting.set(false);
86
+ });
87
+ }
88
+
89
+ onBtnSigninClicked(): void {
90
+ if (this.isEditable()) return;
91
+ if (this.isConnecting() || this.ui().isSigning) return;
92
+
93
+ this.isConnecting.set(true);
94
+ this.client.state.$ui.setKey('isSigning', true);
95
+
96
+ this.client.requestSignin();
97
+
98
+ const uiState = this.client.state.$ui.get();
99
+ const serviceName = uiState.serviceName ?? 'blockauth';
100
+ const enabled = (uiState.uiConfig as { config?: Record<string, { enabled?: boolean }> } | null)
101
+ ?.config?.[serviceName]?.enabled;
102
+ if (enabled) {
103
+ this.client.openModal('signin');
104
+ this.client.state.$ui.setKey('isSigningLoader', true);
105
+ }
106
+ this.clicked.emit();
107
+ }
108
+ }
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/angular';
3
+ import { map } from 'nanostores';
4
+ import { BtnWalletConnectComponent } from './btn-wallet-connect.component';
5
+ import { provideBlockAuthClientForTest } from '../../testing';
6
+
7
+ describe('BtnWalletConnect', () => {
8
+ it('al hacer click delega en client.wallet.evm.connectWalletConnect()', async () => {
9
+ const connectSpy = vi.fn(async () => undefined);
10
+ const fakeClient = {
11
+ wallet: { evm: { connectWalletConnect: connectSpy } },
12
+ state: { $wallet: map({ address: null, isConnecting: false }) },
13
+ } as any;
14
+ await render(BtnWalletConnectComponent, {
15
+ providers: [provideBlockAuthClientForTest(fakeClient)],
16
+ });
17
+ await fireEvent.click(screen.getByRole('button'));
18
+ await waitFor(() => expect(connectSpy).toHaveBeenCalled());
19
+ });
20
+ });
@@ -0,0 +1,109 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
2
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
3
+ import { providersStyles } from '../../primitives/providersStyles';
4
+ import { injectBlockAuthClient } from '../../provide-block-auth';
5
+ import { injectStore } from '../../inject-store';
6
+
7
+ /**
8
+ * Botón WalletConnect. El click delega en
9
+ * `client.wallet.evm.connectWalletConnect()`. Los colores/iconos de marca por
10
+ * wallet salen del primitivo `providersStyles` indexado por `walletService`.
11
+ */
12
+ const bgByWallet: Record<string, string> = {
13
+ trustwallet: 'bas-bg-[#140dda] hover:bas-bg-[#140dda]',
14
+ binance: 'bas-bg-[#464646] hover:bas-bg-[#191919]',
15
+ uniswap: 'bas-bg-[#f64399] hover:bas-bg-[#d76fa1]',
16
+ };
17
+ const styles = {
18
+ disabledBgClassNames: 'bas-bg-[rgba(235,89,0,0.75)] hover:bas-bg-[rgba(216,81,0,0.75)]',
19
+ ringClassNames: 'bal-ba-hover-ring-gray-400',
20
+ paddingClassNames: 'bas-px-12',
21
+ textClassNames: 'bas-text-white bas-font-semibold bas-leading-6 bas-text-base',
22
+ loaderWidth: 70,
23
+ loaderColor: '#fefefe',
24
+ };
25
+
26
+ @Component({
27
+ selector: 'ba-btn-wallet-connect',
28
+ standalone: true,
29
+ changeDetection: ChangeDetectionStrategy.OnPush,
30
+ imports: [BtnBaseComponent],
31
+ template: `
32
+ @if (isDropdownProfile()) {
33
+ <button
34
+ [id]="'btn-provider-' + walletServiceFormatted() + '-dropdown'"
35
+ class="bas-h-full bas-w-full bas-border-0 bas-bg-transparent bas-p-0"
36
+ (click)="onWalletSigning()"
37
+ ><ng-content /></button>
38
+ } @else {
39
+ <ba-btn-base
40
+ [btnId]="'btn-provider-' + walletServiceFormatted()"
41
+ [text]="btnText()"
42
+ type="button-outlined"
43
+ size="small"
44
+ [bgClassNames]="wallet().isConnecting ? disabledBg : bgClassNames()"
45
+ [ringClassNames]="ring"
46
+ [paddingClassNames]="padding"
47
+ [textClassNames]="textCls"
48
+ [isLoader]="wallet().isConnecting"
49
+ [loaderWidth]="loaderWidth"
50
+ [loaderColor]="loaderColor"
51
+ [isIcon]="iconEnabled()"
52
+ [isFocusOutlinedEnabled]="false"
53
+ [disabled]="disabled()"
54
+ (action)="onWalletSigning()"
55
+ >
56
+ <img
57
+ slot="icon"
58
+ [src]="style()?.imgSrc ?? ''"
59
+ height="24"
60
+ width="24"
61
+ class="bas-rounded-lg"
62
+ [alt]="style()?.name ?? walletService()"
63
+ />
64
+ </ba-btn-base>
65
+ }
66
+ `,
67
+ })
68
+ export class BtnWalletConnectComponent {
69
+ isDropdownProfile = input(false);
70
+ text = input('Connect Wallet');
71
+ isTextInsteadOfAccount = input(false);
72
+ iconEnabled = input(true);
73
+ disabled = input(false);
74
+ isEditable = input(false);
75
+ walletService = input('Trust Wallet');
76
+
77
+ clicked = output<void>();
78
+
79
+ private client = injectBlockAuthClient();
80
+ wallet = injectStore(this.client.state.$wallet);
81
+
82
+ ring = styles.ringClassNames;
83
+ padding = styles.paddingClassNames;
84
+ textCls = styles.textClassNames;
85
+ disabledBg = styles.disabledBgClassNames;
86
+ loaderWidth = styles.loaderWidth;
87
+ loaderColor = styles.loaderColor;
88
+
89
+ walletServiceFormatted = computed(() => this.walletService().toLowerCase().replace(' ', ''));
90
+ style = computed(() => providersStyles[this.walletServiceFormatted()]);
91
+ bgClassNames = computed(
92
+ () => bgByWallet[this.walletServiceFormatted()] ?? 'bas-bg-[#140dda] hover:bas-bg-[#140dda]',
93
+ );
94
+ btnText = computed(() => {
95
+ const short = (a: string | null) => (a ? `${a.slice(0, 6)}…${a.slice(-4)}` : '');
96
+ return this.isTextInsteadOfAccount() ? this.text() : short(this.wallet().address) || this.text();
97
+ });
98
+
99
+ async onWalletSigning(): Promise<void> {
100
+ if (this.isEditable()) return;
101
+ if (this.wallet().isConnecting) return;
102
+ try {
103
+ await this.client.wallet.evm.connectWalletConnect();
104
+ } catch {
105
+ /* connection cancelled/failed */
106
+ }
107
+ this.clicked.emit();
108
+ }
109
+ }
@@ -0,0 +1,11 @@
1
+ // Barrel de los botones de proveedor de @block-auth.io/angular (Task A7),
2
+ // espejo 1:1 de packages/react/src/components/btns/*.
3
+ export { BtnBaseProviderComponent } from './btn-base-provider.component';
4
+ export { BtnBlockAuthComponent } from './btn-block-auth.component';
5
+ export { BtnGmailComponent } from './btn-gmail.component';
6
+ export { BtnGoogleBaseComponent } from './btn-google-base.component';
7
+ export { BtnMetamaskComponent } from './btn-metamask.component';
8
+ export { BtnMicrosoftComponent } from './btn-microsoft.component';
9
+ export { BtnPhantomComponent } from './btn-phantom.component';
10
+ export { BtnSigninComponent } from './btn-signin.component';
11
+ export { BtnWalletConnectComponent } from './btn-wallet-connect.component';
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/angular';
3
+ import { map } from 'nanostores';
4
+ import { ContentSigninComponent } from './content-signin.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
+ function makeClient() {
17
+ return { state: { $ws: map({}), $ui: map({}) } } as any;
18
+ }
19
+
20
+ // Port de packages/react/src/components/contents/ContentSignin.test.tsx (con
21
+ // el cross-check del port Vue que verifica el overview real: A10 cableó el
22
+ // placeholder de A9 al `<ba-txns-overview>` real).
23
+ describe('ContentSignin', () => {
24
+ it('renderiza el contenido de signin', async () => {
25
+ await render(ContentSigninComponent, { providers: [provideBlockAuthClientForTest(makeClient())] });
26
+ expect(screen.getByTestId('content-signin')).toBeDefined();
27
+ });
28
+
29
+ it('sin spinner (estado por defecto) renderiza el overview de txns real', async () => {
30
+ await render(ContentSigninComponent, { providers: [provideBlockAuthClientForTest(makeClient())] });
31
+ expect(screen.getByTestId('txns-overview')).toBeDefined();
32
+ });
33
+ });
@@ -0,0 +1,68 @@
1
+ import { ChangeDetectionStrategy, Component, computed, effect, input, signal } from '@angular/core';
2
+ import { injectBlockAuthClient } from '../../provide-block-auth';
3
+ import { injectStore } from '../../inject-store';
4
+ import { OvalSpinnerComponent } from '../../primitives/oval-spinner.component';
5
+ import { TxnsOverviewComponent } from '../txns/txns-overview.component';
6
+
7
+ /**
8
+ * Contenido de sign-in dirigido por el core (overview de transacciones).
9
+ * Muestra un `<ba-oval-spinner>` mientras `isAcceptingTxn || isSigningLoader`,
10
+ * luego el overview de transacciones. En el montaje emite `user:txns { token }`
11
+ * y auto-limpia el signing loader al recibir transacciones (debounce 200ms) o
12
+ * tras 1.5s.
13
+ *
14
+ * El `effect()` reprograma los timers cada vez que cambian `ws.requests` (o
15
+ * `isLoaded`), y emite `user:txns` una única vez guardado en `isLoaded`.
16
+ * Escribir `isLoaded.set(true)` dentro del propio effect que lee `isLoaded`
17
+ * dispara una segunda ejecución inmediata (limpieza+reprogramación de timers).
18
+ */
19
+ @Component({
20
+ selector: 'ba-content-signin',
21
+ standalone: true,
22
+ changeDetection: ChangeDetectionStrategy.OnPush,
23
+ imports: [OvalSpinnerComponent, TxnsOverviewComponent],
24
+ template: `
25
+ <div data-testid="content-signin" class="bas-flex bas-h-full bas-w-full">
26
+ @if (showSpinner()) {
27
+ <ba-oval-spinner [showLogo]="true" [animateBg]="false" />
28
+ } @else {
29
+ <ba-txns-overview [closeTxns]="closeTxns()" />
30
+ }
31
+ </div>
32
+ `,
33
+ })
34
+ export class ContentSigninComponent {
35
+ closeTxns = input<() => void>(() => {});
36
+
37
+ private readonly client = injectBlockAuthClient();
38
+ private readonly ws = injectStore(this.client.state.$ws);
39
+ private readonly ui = injectStore(this.client.state.$ui);
40
+ private readonly isLoaded = signal(false);
41
+
42
+ protected showSpinner = computed(() => {
43
+ const ui = this.ui() as Record<string, any>;
44
+ return !!ui.isAcceptingTxn || !!ui.isSigningLoader;
45
+ });
46
+
47
+ constructor() {
48
+ effect((onCleanup) => {
49
+ const requests = (this.ws() as Record<string, any>).requests ?? [];
50
+ const loaded = this.isLoaded();
51
+ const t1 = setTimeout(() => {
52
+ if (requests.length > 0) this.client.state.$ui.setKey('isSigningLoader', false);
53
+ }, 200);
54
+ const t2 = setTimeout(() => {
55
+ this.client.state.$ui.setKey('isSigningLoader', false);
56
+ }, 1500);
57
+ if (!loaded && this.client.provider?.wsc) {
58
+ const s = this.client.state.$ws.get();
59
+ this.client.provider.wsc.emit('user:txns', { token: (s as Record<string, any>).token });
60
+ this.isLoaded.set(true);
61
+ }
62
+ onCleanup(() => {
63
+ clearTimeout(t1);
64
+ clearTimeout(t2);
65
+ });
66
+ });
67
+ }
68
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen } from '@testing-library/angular';
3
+ import { map } from 'nanostores';
4
+
5
+ vi.mock('@wagmi/core', () => ({
6
+ createConfig: vi.fn(() => ({ connectors: [] })),
7
+ connect: vi.fn(), disconnect: vi.fn(), getAccount: vi.fn(() => ({})),
8
+ signMessage: vi.fn(), watchAccount: vi.fn(() => () => undefined), http: vi.fn(),
9
+ }));
10
+ vi.mock('@wagmi/connectors', () => ({
11
+ injected: vi.fn(() => ({ id: 'injected' })), walletConnect: vi.fn(() => ({ id: 'walletConnect' })),
12
+ }));
13
+
14
+ // Espía makeQrDataUrl para evitar canvas/qrcode en jsdom y verificar el
15
+ // cableado selección de servicio -> QR. deepLinkFor se deja real (función pura).
16
+ const FAKE_QR_DATA_URL = 'data:image/png;base64,FAKEQR==';
17
+ vi.mock('@block-auth.io/core', async (importOriginal) => {
18
+ const actual = await importOriginal<typeof import('@block-auth.io/core')>();
19
+ return { ...actual, makeQrDataUrl: vi.fn(async () => FAKE_QR_DATA_URL) };
20
+ });
21
+
22
+ import { ContentSignupComponent } from './content-signup.component';
23
+ import { makeQrDataUrl } from '@block-auth.io/core';
24
+ import { provideBlockAuthClientForTest } from '../../testing';
25
+
26
+ // El $ui es propio del client falso (no el singleton del core), así que el
27
+ // uiConfig se fija directamente en el map, sin setUiConfig ni reset global.
28
+ function makeClient(ui: Record<string, unknown> = {}) {
29
+ return {
30
+ state: {
31
+ $ui: map<Record<string, unknown>>({ ...ui }),
32
+ $ws: map({}),
33
+ $wallet: map({}),
34
+ $openModals: map({}),
35
+ },
36
+ } as any;
37
+ }
38
+
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ (makeQrDataUrl as ReturnType<typeof vi.fn>).mockResolvedValue(FAKE_QR_DATA_URL);
42
+ });
43
+
44
+ // Port de packages/react/src/components/contents/ContentSignup.test.tsx.
45
+ describe('ContentSignup', () => {
46
+ it('renderiza el contenido de signup', async () => {
47
+ await render(ContentSignupComponent, { providers: [provideBlockAuthClientForTest(makeClient())] });
48
+ expect(screen.getByTestId('content-signup')).toBeDefined();
49
+ });
50
+
51
+ it('no llama a makeQrDataUrl cuando el servicio inicial no es web3', async () => {
52
+ // El servicio por defecto es 'blockauth' (FALLBACK_PROVIDERS[0]), no web3.
53
+ const client = makeClient({ uiConfig: { domain: 'app.com' } });
54
+ await render(ContentSignupComponent, { providers: [provideBlockAuthClientForTest(client)] });
55
+ expect(makeQrDataUrl).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it('solo muestra los proveedores Web2 (google/microsoft/blockauth), no web3', async () => {
59
+ const client = makeClient({
60
+ uiConfig: {
61
+ domain: 'app.com',
62
+ config: {
63
+ blockauth: { enabled: true, orderIndex: 0 },
64
+ google: { enabled: true, orderIndex: 1 },
65
+ microsoft: { enabled: true, orderIndex: 2 },
66
+ metamask: { enabled: true, orderIndex: 3 },
67
+ phantom: { enabled: true, orderIndex: 4 },
68
+ },
69
+ },
70
+ });
71
+ await render(ContentSignupComponent, { providers: [provideBlockAuthClientForTest(client)] });
72
+ expect(screen.getByAltText('BlockAuth')).toBeDefined();
73
+ expect(screen.getByAltText('Google')).toBeDefined();
74
+ expect(screen.getByAltText('Microsoft')).toBeDefined();
75
+ expect(screen.queryByAltText('MetaMask')).toBeNull();
76
+ expect(screen.queryByAltText('Phantom')).toBeNull();
77
+ });
78
+ });
@@ -0,0 +1,225 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ input,
7
+ signal,
8
+ untracked,
9
+ } from '@angular/core';
10
+ import {
11
+ $locale,
12
+ deepLinkFor,
13
+ makeQrDataUrl,
14
+ t as translate,
15
+ type TextKey,
16
+ } from '@block-auth.io/core';
17
+ import { injectBlockAuthClient } from '../../provide-block-auth';
18
+ import { injectStore } from '../../inject-store';
19
+ import { providersStyles } from '../../primitives/providersStyles';
20
+ import { ProgressbarComponent } from '../../primitives/progressbar.component';
21
+ import { ProvidersSectionComponent } from '../sections/providers-section.component';
22
+ import { LoginSectionComponent } from '../sections/login-section.component';
23
+ import { FooterSectionComponent } from '../sections/footer-section.component';
24
+ import type { WebTypeItem } from '../sections/internals/providers-filters.component';
25
+
26
+ /**
27
+ * Contenido de sign-up: el `<ba-progressbar>` de 3 pasos, la
28
+ * `<ba-providers-section>` (cabecera + filtros + grid), la
29
+ * `<ba-login-section>` (logo + CTA + descripción) y la `<ba-footer-section>`,
30
+ * en el grid responsive de dos columnas.
31
+ */
32
+ const ENABLED_PROVIDERS = ['google', 'microsoft', 'blockauth'];
33
+ const WEB2 = ['google', 'microsoft'];
34
+ const FALLBACK_PROVIDERS = ['blockauth', 'google', 'microsoft'];
35
+
36
+ function applyFilters(providers: string[], { webType, search }: { webType: string; search: string }): string[] {
37
+ let result = providers;
38
+ if (webType === 'web2') result = result.filter((item) => WEB2.includes(item));
39
+ else if (webType === 'web3') result = result.filter((item) => !WEB2.includes(item));
40
+ if (search) {
41
+ const q = search.toLowerCase();
42
+ result = result.filter((item) => item.toLowerCase().includes(q));
43
+ }
44
+ return result;
45
+ }
46
+
47
+ @Component({
48
+ selector: 'ba-content-signup',
49
+ standalone: true,
50
+ changeDetection: ChangeDetectionStrategy.OnPush,
51
+ imports: [ProgressbarComponent, ProvidersSectionComponent, LoginSectionComponent, FooterSectionComponent],
52
+ template: `
53
+ <div
54
+ data-testid="content-signup"
55
+ [attr.data-device]="device()"
56
+ [class]="
57
+ 'bas-grid bas-h-full ' +
58
+ (!isEditable()
59
+ ? 'bas-w-full bas-grid-cols-1 bas-grid-rows-[52px_140px_228px_48px] sm1:bas-w-fit sm1:bas-grid-cols-[240px_384px] sm1:bas-grid-rows-[52px_auto_auto] lg:bas-grid-cols-[240px_640px]'
60
+ : 'bas-w-fit')
61
+ "
62
+ >
63
+ <ba-providers-section
64
+ [container]="container()"
65
+ [uiConfigProvidersInfo]="uiConfigProvidersInfo()"
66
+ [selectedService]="selectedService()"
67
+ (selectedServiceChange)="selectedService.set($event)"
68
+ [showedProviders]="showedProviders()"
69
+ (changeWebType)="onChangeWebType($event)"
70
+ (changeProviderSearch)="onChangeProviderSearch($event)"
71
+ [isEditable]="isEditable()"
72
+ [isEditing]="isEditing()"
73
+ [device]="device()"
74
+ />
75
+
76
+ <section
77
+ [class]="
78
+ 'bas-flex bas-items-center bas-justify-center ' + (!isEditable() ? 'bas-row-start-1 sm1:bas-row-start-auto' : '')
79
+ "
80
+ >
81
+ <ba-progressbar
82
+ [step1]="false"
83
+ [fillStep1]="true"
84
+ [step2]="!hasBeenConnected() && showImgLoader()"
85
+ [step3]="!showImgLoader()"
86
+ [goBackToStep2]="(serviceName() !== selectedService() || !isServiceConnected()) && hasBeenConnected()"
87
+ />
88
+ </section>
89
+
90
+ <ba-login-section
91
+ [imgQR]="imgQR() ?? ''"
92
+ [showImgLoader]="showImgLoader()"
93
+ [selectedService]="selectedService()"
94
+ [isServiceConnected]="isServiceConnected()"
95
+ [isEditable]="isEditable()"
96
+ [device]="device()"
97
+ [btnsConnectTxt]="btnsConnectTxt()"
98
+ [loginTxt]="loginTxt()"
99
+ [lastStepTxt]="lastStepTxt()"
100
+ />
101
+
102
+ <ba-footer-section
103
+ [isEditable]="isEditable()"
104
+ [device]="device() === 'phone' ? 'phone' : 'desktop'"
105
+ [text1]="footerText1()"
106
+ [text2]="footerText2()"
107
+ [href]="footerHref()"
108
+ />
109
+ </div>
110
+ `,
111
+ })
112
+ export class ContentSignupComponent {
113
+ container = input<HTMLElement | null>(null);
114
+ isEditable = input(false);
115
+ isEditing = input(false);
116
+ device = input<'desktop' | 'tablet' | 'phone'>('desktop');
117
+
118
+ private readonly client = injectBlockAuthClient();
119
+ private readonly ui = injectStore(this.client.state.$ui);
120
+ private readonly locale = injectStore($locale);
121
+ private readonly t = (key: TextKey): string => translate(key, this.locale());
122
+
123
+ // Etiquetas localizadas del text-map del backend, con fallback al i18n del core.
124
+ private readonly content = computed(() => (this.ui() as Record<string, any>)?.uiConfig?.config?.content);
125
+ protected btnsConnectTxt = computed(() => this.content()?.btns?.connect?.[this.locale()] ?? '');
126
+ protected loginTxt = computed(() => this.content()?.login?.[this.locale()] ?? this.t('login'));
127
+ protected lastStepTxt = computed(() => this.content()?.lastStep?.[this.locale()] ?? this.t('lastStep'));
128
+ protected footerText1 = computed(() => this.content()?.footer?.first?.[this.locale()] ?? this.t('footerFirst'));
129
+ protected footerText2 = computed(() => this.content()?.footer?.second?.[this.locale()] ?? this.t('footerSecond'));
130
+ protected footerHref = computed(() => this.content()?.footer?.url ?? 'https://block-auth.io');
131
+
132
+ protected serviceName = computed(() => (this.ui() as Record<string, any>)?.serviceName);
133
+ protected isServiceConnected = computed(() => !!(this.ui() as Record<string, any>)?.isServiceConnected);
134
+ private readonly domain = computed(
135
+ () => (this.ui() as Record<string, any>)?.uiConfig?.domain as string | undefined,
136
+ );
137
+
138
+ protected uiConfigProvidersInfo = computed(() => {
139
+ const config = (this.ui() as Record<string, any>)?.uiConfig?.config;
140
+ if (!config) return undefined;
141
+ return {
142
+ providersColumns: Number(config.providersColumns) || undefined,
143
+ showProviderNameOnMobile: config.showProviderNameOnMobile,
144
+ enableSearch: config.enableSearch,
145
+ enableWebTypeFilter: config.enableWebTypeFilter,
146
+ };
147
+ });
148
+
149
+ private readonly allProviders = computed<string[]>(() => {
150
+ const config = (this.ui() as Record<string, any>)?.uiConfig?.config;
151
+ if (config) {
152
+ const enabled = Object.keys(config)
153
+ .filter(
154
+ (key) =>
155
+ ENABLED_PROVIDERS.includes(key) &&
156
+ providersStyles[key] &&
157
+ typeof config[key] === 'object' &&
158
+ config[key] !== null &&
159
+ 'enabled' in config[key] &&
160
+ 'orderIndex' in config[key],
161
+ )
162
+ .filter((key) => config[key].enabled)
163
+ .sort((a, b) => config[a].orderIndex - config[b].orderIndex);
164
+ if (enabled.length > 0) return enabled;
165
+ }
166
+ return FALLBACK_PROVIDERS;
167
+ });
168
+
169
+ protected selectedService = signal<string>(this.allProviders()[0] ?? 'blockauth');
170
+ protected showedProviders = signal<string[]>(this.allProviders());
171
+ protected hasBeenConnected = signal(false);
172
+ protected imgQR = signal<string | null>(null);
173
+ // Estado de filtros no reactivo: mutarlo no dispara reactividad.
174
+ private filterState: { webType: string; search: string } = { webType: 'all', search: '' };
175
+
176
+ protected showImgLoader = computed(
177
+ () => !(this.isServiceConnected() && this.serviceName() === this.selectedService()),
178
+ );
179
+
180
+ constructor() {
181
+ // Efecto 1: re-deriva showedProviders/selectedService cuando cambia allProviders.
182
+ effect(() => {
183
+ const providers = this.allProviders();
184
+ this.showedProviders.set(applyFilters(providers, this.filterState));
185
+ const cur = untracked(() => this.selectedService());
186
+ this.selectedService.set(providers.includes(cur) ? cur : providers[0] ?? 'blockauth');
187
+ });
188
+
189
+ // Efecto 2: marca hasBeenConnected cuando el servicio seleccionado conecta.
190
+ effect(() => {
191
+ if (this.serviceName() === this.selectedService() && this.isServiceConnected()) {
192
+ this.hasBeenConnected.set(true);
193
+ }
194
+ });
195
+
196
+ // Efecto 3: genera el QR para providers web3 (metamask/phantom), con guard
197
+ // de cancelación.
198
+ effect((onCleanup) => {
199
+ const svc = this.selectedService();
200
+ const dom = this.domain();
201
+ const isWeb3 = svc === 'metamask' || svc === 'phantom';
202
+ if (!isWeb3 || !dom) {
203
+ this.imgQR.set(null);
204
+ return;
205
+ }
206
+ let cancelled = false;
207
+ const deepLink = deepLinkFor(svc, dom);
208
+ makeQrDataUrl(deepLink).then((dataUrl) => {
209
+ if (!cancelled) this.imgQR.set(dataUrl);
210
+ });
211
+ onCleanup(() => {
212
+ cancelled = true;
213
+ });
214
+ });
215
+ }
216
+
217
+ onChangeWebType(webType: WebTypeItem): void {
218
+ this.filterState = { ...this.filterState, webType: webType.value };
219
+ this.showedProviders.set(applyFilters(this.allProviders(), this.filterState));
220
+ }
221
+ onChangeProviderSearch(value: string): void {
222
+ this.filterState = { ...this.filterState, search: value };
223
+ this.showedProviders.set(applyFilters(this.allProviders(), this.filterState));
224
+ }
225
+ }