@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,104 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import { $extension, $locale } from '@block-auth.io/core';
3
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
4
+ import { injectBlockAuthClient } from '../../provide-block-auth';
5
+ import { injectStore } from '../../inject-store';
6
+
7
+ const CHROME_STORE_URL =
8
+ 'https://chromewebstore.google.com/detail/block-auth-one/jhjjpmjhiamddbfhogknjibkacajkpdg';
9
+
10
+ /**
11
+ * El único botón que ramifica sobre el estado de la extensión (`$extension`,
12
+ * que no está envuelto por `BlockAuthService`, se lee aquí con `injectStore`).
13
+ * Según el estado (installed/isUser/isUserActive/isLocked):
14
+ * - no instalada → abre la Chrome Web Store,
15
+ * - no user / no activo / bloqueada → despacha el evento DOM `ext:open`,
16
+ * - conectable → `client.connectBlockAuth()`.
17
+ * El label sale del mapa localizado `uiConfig.config.content.btns`.
18
+ */
19
+ @Component({
20
+ selector: 'ba-btn-block-auth',
21
+ standalone: true,
22
+ changeDetection: ChangeDetectionStrategy.OnPush,
23
+ imports: [BtnBaseComponent],
24
+ template: `
25
+ @if (isDropdownProfile()) {
26
+ <button
27
+ id="btn-provider-blockauth-dropdown"
28
+ class="bas-h-full bas-w-full bas-border-0 bas-bg-transparent bas-p-0"
29
+ (click)="onBtnBlockAuthClicked()"
30
+ ><ng-content /></button>
31
+ } @else {
32
+ <ba-btn-base
33
+ btnId="btn-provider-blockauth"
34
+ [text]="getBtnText() || text()"
35
+ type="button-outlined"
36
+ size="small"
37
+ paddingClassNames="bas-px-12"
38
+ [isIcon]="true"
39
+ textClassNames="bas-text-white bas-font-semibold bas-leading-6 bas-text-base"
40
+ [disabled]="disabled()"
41
+ [applyDisabledStyles]="false"
42
+ [isLoader]="false"
43
+ [isFocusOutlinedEnabled]="false"
44
+ (action)="onBtnBlockAuthClicked()"
45
+ >
46
+ <img
47
+ slot="icon"
48
+ src="https://dashboard.block-auth.io/img/blockauth_logo.png"
49
+ width="24"
50
+ height="24"
51
+ alt="Block-Auth"
52
+ />
53
+ </ba-btn-base>
54
+ }
55
+ `,
56
+ })
57
+ export class BtnBlockAuthComponent {
58
+ isDropdownProfile = input(false);
59
+ disabled = input(false);
60
+ isEditable = input(false);
61
+ text = input('Conectar');
62
+
63
+ private client = injectBlockAuthClient();
64
+ private ui = injectStore(this.client.state.$ui);
65
+ private ws = injectStore(this.client.state.$ws);
66
+ private ext = injectStore($extension);
67
+ private locale = injectStore($locale);
68
+
69
+ private btnsTexts = computed(
70
+ () =>
71
+ (this.ui().uiConfig as
72
+ | { config?: { content?: { btns?: Record<string, Record<string, string>> } } }
73
+ | null)?.config?.content?.btns,
74
+ );
75
+ private txt = (key: string): string =>
76
+ this.btnsTexts()?.[key]?.[this.locale()] ?? this.text();
77
+
78
+ getBtnText = computed((): string => {
79
+ if (!(this.ws().ready && this.ui().isSdkSignedIn)) return '';
80
+ const ext = this.ext();
81
+ if (!ext.installed) return this.txt('download');
82
+ if (!ext.isUser) return this.txt('createAccount');
83
+ if (!ext.isUserActive) return this.txt('completeRegister');
84
+ if (ext.isLocked) return this.txt('unlockOtp');
85
+ return this.txt('connect');
86
+ });
87
+
88
+ onBtnBlockAuthClicked(): void {
89
+ if (this.isEditable()) return;
90
+ if (!this.ws().ready || !this.ui().isSdkSignedIn) return;
91
+ const ext = this.ext();
92
+ if (!ext.installed) {
93
+ if (typeof window !== 'undefined') window.open(CHROME_STORE_URL, '_blank');
94
+ return;
95
+ }
96
+ if (!ext.isUser || !ext.isUserActive || ext.isLocked) {
97
+ if (typeof document !== 'undefined') {
98
+ document.dispatchEvent(new CustomEvent('ext:open', { detail: {} }));
99
+ }
100
+ return;
101
+ }
102
+ this.client.connectBlockAuth();
103
+ }
104
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/angular';
3
+ import { BtnGmailComponent } from './btn-gmail.component';
4
+ import { provideBlockAuthClientForTest } from '../../testing';
5
+
6
+ describe('BtnGmail', () => {
7
+ it('compone BtnGoogleBase: al hacer click dispara oauth.signupGoogle', async () => {
8
+ const signupGoogleSpy = vi.fn(async () => undefined);
9
+ const fakeClient = { oauth: { signupGoogle: signupGoogleSpy } } as any;
10
+ await render(BtnGmailComponent, {
11
+ providers: [provideBlockAuthClientForTest(fakeClient)],
12
+ });
13
+ await fireEvent.click(screen.getByRole('button'));
14
+ await waitFor(() => expect(signupGoogleSpy).toHaveBeenCalledWith('auth-code'));
15
+ });
16
+ });
@@ -0,0 +1,41 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+ import { BtnGoogleBaseComponent } from './btn-google-base.component';
3
+ import { BiLogoGmailComponent } from '../../icons';
4
+
5
+ /**
6
+ * Envuelve `BtnGoogleBase` con la paleta roja de Gmail y el icono `BiLogoGmail`
7
+ * inline como icono por defecto.
8
+ */
9
+ @Component({
10
+ selector: 'ba-btn-gmail',
11
+ standalone: true,
12
+ changeDetection: ChangeDetectionStrategy.OnPush,
13
+ imports: [BtnGoogleBaseComponent, BiLogoGmailComponent],
14
+ template: `
15
+ <ba-btn-google-base
16
+ [isDropdownProfile]="isDropdownProfile()"
17
+ [text]="text()"
18
+ bgClassNames="bas-bg-[#ed2d2d] hover:bas-bg-[#eb1515]"
19
+ ringClassNames="bal-ba-hover-ring-red-400"
20
+ paddingClassNames="bas-px-12"
21
+ textClassNames="bas-text-white bas-font-semibold bas-leading-6 bas-text-base"
22
+ [iconEnabled]="iconEnabled()"
23
+ [disabled]="disabled()"
24
+ [isFocusOutlinedEnabled]="false"
25
+ [isEditable]="isEditable()"
26
+ (clicked)="clicked.emit()"
27
+ >
28
+ <ba-bi-logo-gmail slot="icon" class="bas-h-6 bas-w-6 bas-text-white" />
29
+ <ng-content />
30
+ </ba-btn-google-base>
31
+ `,
32
+ })
33
+ export class BtnGmailComponent {
34
+ isDropdownProfile = input(false);
35
+ text = input('Gmail');
36
+ iconEnabled = input(true);
37
+ disabled = input(false);
38
+ isEditable = input(false);
39
+
40
+ clicked = output<void>();
41
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/angular';
3
+ import { BtnGoogleBaseComponent } from './btn-google-base.component';
4
+ import { provideBlockAuthClientForTest } from '../../testing';
5
+
6
+ describe('BtnGoogleBase', () => {
7
+ it('al hacer click dispara client.oauth.signupGoogle("auth-code")', async () => {
8
+ const signupGoogleSpy = vi.fn(async () => undefined);
9
+ const fakeClient = { oauth: { signupGoogle: signupGoogleSpy } } as any;
10
+ await render(BtnGoogleBaseComponent, {
11
+ providers: [provideBlockAuthClientForTest(fakeClient)],
12
+ });
13
+ await fireEvent.click(screen.getByRole('button'));
14
+ await waitFor(() => expect(signupGoogleSpy).toHaveBeenCalledWith('auth-code'));
15
+ });
16
+ });
@@ -0,0 +1,73 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
3
+ import { injectBlockAuthClient } from '../../provide-block-auth';
4
+
5
+ /**
6
+ * Botón Google base (icono + label). Toda la lógica OAuth vive en el core;
7
+ * aquí solo se renderiza y el click delega en `client.oauth.signupGoogle(mode)`.
8
+ * Los estilos son inputs reales (con los mismos defaults) para que `BtnGmail`
9
+ * pueda sobrescribirlos.
10
+ *
11
+ * El icono se proyecta con `[slot=icon]` y se re-proyecta hacia `BtnBase`
12
+ * (`ngProjectAs`).
13
+ */
14
+ @Component({
15
+ selector: 'ba-btn-google-base',
16
+ standalone: true,
17
+ changeDetection: ChangeDetectionStrategy.OnPush,
18
+ imports: [BtnBaseComponent],
19
+ template: `
20
+ @if (isDropdownProfile()) {
21
+ <button
22
+ id="btn-provider-google-dropdown"
23
+ class="bas-h-full bas-w-full bas-border-0 bas-bg-transparent bas-p-0"
24
+ (click)="onBtnGmailClicked()"
25
+ ><ng-content /></button>
26
+ } @else {
27
+ <ba-btn-base
28
+ btnId="btn-provider-google"
29
+ [text]="text()"
30
+ type="button-outlined"
31
+ size="small"
32
+ [bgClassNames]="bgClassNames()"
33
+ [ringClassNames]="ringClassNames()"
34
+ [paddingClassNames]="paddingClassNames()"
35
+ [textClassNames]="textClassNames()"
36
+ [isIcon]="iconEnabled()"
37
+ [isFocusOutlinedEnabled]="isFocusOutlinedEnabled()"
38
+ [disabled]="disabled()"
39
+ (action)="onBtnGmailClicked()"
40
+ ><ng-content select="[slot=icon]" ngProjectAs="[slot=icon]" /></ba-btn-base>
41
+ }
42
+ `,
43
+ })
44
+ export class BtnGoogleBaseComponent {
45
+ isDropdownProfile = input(false);
46
+ mode = input<'auth-code' | 'implicit'>('auth-code');
47
+ text = input('Google');
48
+ bgClassNames = input('bas-bg-[#ed2d2d] hover:bas-bg-[#eb1515]');
49
+ ringClassNames = input('bal-ba-hover-ring-red-400');
50
+ paddingClassNames = input('bas-px-12');
51
+ textClassNames = input('bas-text-white bas-font-semibold bas-leading-6 bas-text-base');
52
+ iconEnabled = input(false);
53
+ disabled = input(false);
54
+ isFocusOutlinedEnabled = input(false);
55
+ isEditable = input(false);
56
+
57
+ clicked = output<void>();
58
+
59
+ private client = injectBlockAuthClient();
60
+
61
+ async onBtnGmailClicked(): Promise<void> {
62
+ if (this.isEditable()) return;
63
+ try {
64
+ await this.client.oauth.signupGoogle(this.mode());
65
+ } catch (err) {
66
+ // Exponer la mala configuración (clientId ausente, script GIS no cargado)
67
+ // y las cancelaciones en vez de tragárnoslas: un catch silencioso haría
68
+ // que el botón pareciera inerte cuando en realidad falló.
69
+ console.warn('[blockauth] Google sign-in failed:', err);
70
+ }
71
+ this.clicked.emit();
72
+ }
73
+ }
@@ -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 { BtnMetamaskComponent } from './btn-metamask.component';
5
+ import { provideBlockAuthClientForTest } from '../../testing';
6
+
7
+ describe('BtnMetamask', () => {
8
+ it('al hacer click dispara client.web3.signup("metamask")', async () => {
9
+ const signupSpy = vi.fn(async () => undefined);
10
+ const fakeClient = {
11
+ web3: { signup: signupSpy },
12
+ state: { $wallet: map({ address: null, isConnecting: false }) },
13
+ } as any;
14
+ await render(BtnMetamaskComponent, {
15
+ providers: [provideBlockAuthClientForTest(fakeClient)],
16
+ });
17
+ await fireEvent.click(screen.getByRole('button'));
18
+ await waitFor(() => expect(signupSpy).toHaveBeenCalledWith('metamask'));
19
+ });
20
+ });
@@ -0,0 +1,98 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
2
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
3
+ import { injectBlockAuthClient } from '../../provide-block-auth';
4
+ import { injectStore } from '../../inject-store';
5
+
6
+ /**
7
+ * Botón de conexión MetaMask (EVM). El click delega en
8
+ * `client.web3.signup('metamask')`, que conecta la wallet inyectada y corre
9
+ * todo el handshake de signup en el core.
10
+ */
11
+ const styles = {
12
+ bgClassNames: 'bas-bg-[#eb5900] hover:bas-bg-[#d85100]',
13
+ disabledBgClassNames: 'bas-bg-[rgba(235,89,0,0.75)] hover:bas-bg-[rgba(216,81,0,0.75)]',
14
+ ringClassNames: 'bal-ba-hover-ring-orange-400',
15
+ paddingClassNames: 'bas-px-12',
16
+ textClassNames: 'bas-text-white bas-font-semibold bas-leading-6 bas-text-base',
17
+ loaderWidth: 70,
18
+ loaderColor: '#fefefe',
19
+ };
20
+
21
+ @Component({
22
+ selector: 'ba-btn-metamask',
23
+ standalone: true,
24
+ changeDetection: ChangeDetectionStrategy.OnPush,
25
+ imports: [BtnBaseComponent],
26
+ template: `
27
+ @if (isDropdownProfile()) {
28
+ <button
29
+ id="btn-provider-metamask-dropdown"
30
+ class="bas-h-full bas-w-full bas-border-0 bas-bg-transparent bas-p-0"
31
+ (click)="onMetamaskSigning()"
32
+ ><ng-content /></button>
33
+ } @else {
34
+ <ba-btn-base
35
+ btnId="btn-provider-metamask"
36
+ [text]="btnText()"
37
+ type="button-outlined"
38
+ size="small"
39
+ [bgClassNames]="wallet().isConnecting ? disabledBg : bg"
40
+ [ringClassNames]="ring"
41
+ [paddingClassNames]="padding"
42
+ [textClassNames]="textCls"
43
+ [isLoader]="wallet().isConnecting"
44
+ [loaderWidth]="loaderWidth"
45
+ [loaderColor]="loaderColor"
46
+ [isIcon]="iconEnabled()"
47
+ [isFocusOutlinedEnabled]="false"
48
+ [disabled]="disabled()"
49
+ (action)="onMetamaskSigning()"
50
+ >
51
+ <img
52
+ slot="icon"
53
+ src="https://img.icons8.com/ios-filled/48/ffffff/metamask-logo.png"
54
+ height="24"
55
+ width="24"
56
+ alt="MetaMask"
57
+ />
58
+ </ba-btn-base>
59
+ }
60
+ `,
61
+ })
62
+ export class BtnMetamaskComponent {
63
+ isDropdownProfile = input(false);
64
+ text = input('MetaMask');
65
+ isTextInsteadOfAccount = input(false);
66
+ iconEnabled = input(true);
67
+ disabled = input(false);
68
+ isEditable = input(false);
69
+
70
+ clicked = output<void>();
71
+
72
+ private client = injectBlockAuthClient();
73
+ wallet = injectStore(this.client.state.$wallet);
74
+
75
+ bg = styles.bgClassNames;
76
+ disabledBg = styles.disabledBgClassNames;
77
+ ring = styles.ringClassNames;
78
+ padding = styles.paddingClassNames;
79
+ textCls = styles.textClassNames;
80
+ loaderWidth = styles.loaderWidth;
81
+ loaderColor = styles.loaderColor;
82
+
83
+ btnText = computed(() => {
84
+ const short = (a: string | null) => (a ? `${a.slice(0, 6)}…${a.slice(-4)}` : '');
85
+ return this.isTextInsteadOfAccount() ? this.text() : short(this.wallet().address) || this.text();
86
+ });
87
+
88
+ async onMetamaskSigning(): Promise<void> {
89
+ if (this.isEditable()) return;
90
+ if (this.wallet().isConnecting) return;
91
+ try {
92
+ await this.client.web3.signup('metamask');
93
+ } catch {
94
+ /* conexión/firma cancelada o fallida */
95
+ }
96
+ this.clicked.emit();
97
+ }
98
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/angular';
3
+ import { BtnMicrosoftComponent } from './btn-microsoft.component';
4
+ import { provideBlockAuthClientForTest } from '../../testing';
5
+
6
+ describe('BtnMicrosoft', () => {
7
+ it('al hacer click dispara client.oauth.signupMicrosoft()', async () => {
8
+ const signupMicrosoftSpy = vi.fn(async () => undefined);
9
+ const fakeClient = { oauth: { signupMicrosoft: signupMicrosoftSpy } } as any;
10
+ await render(BtnMicrosoftComponent, {
11
+ providers: [provideBlockAuthClientForTest(fakeClient)],
12
+ });
13
+ await fireEvent.click(screen.getByRole('button'));
14
+ await waitFor(() => expect(signupMicrosoftSpy).toHaveBeenCalled());
15
+ });
16
+ });
@@ -0,0 +1,77 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
3
+ import { injectBlockAuthClient } from '../../provide-block-auth';
4
+ import { TiVendorMicrosoftComponent } from '../../icons';
5
+
6
+ /**
7
+ * Botón Microsoft (glifo + label). El click delega en
8
+ * `client.oauth.signupMicrosoft()` (la config MSAL + el handshake de signup
9
+ * viven en el core).
10
+ */
11
+ const msStyles = {
12
+ bgClassNames: 'bas-bg-[#2b88d8] hover:bas-bg-[#0078d4]',
13
+ ringClassNames: 'bal-ba-hover-ring-blue-400',
14
+ textClassNames: 'bas-text-white bas-font-semibold bas-leading-6 bas-text-base',
15
+ paddingClassNames: 'bas-px-12',
16
+ };
17
+
18
+ @Component({
19
+ selector: 'ba-btn-microsoft',
20
+ standalone: true,
21
+ changeDetection: ChangeDetectionStrategy.OnPush,
22
+ imports: [BtnBaseComponent, TiVendorMicrosoftComponent],
23
+ template: `
24
+ @if (isDropdownProfile()) {
25
+ <button
26
+ id="btn-provider-microsoft-dropdown"
27
+ class="bas-h-full bas-w-full bas-border-0 bas-bg-transparent bas-p-0"
28
+ (click)="microsoftSignin()"
29
+ ><ng-content /></button>
30
+ } @else {
31
+ <ba-btn-base
32
+ btnId="btn-provider-microsoft"
33
+ [text]="text()"
34
+ type="button-outlined"
35
+ size="small"
36
+ [bgClassNames]="bg"
37
+ [ringClassNames]="ring"
38
+ [paddingClassNames]="padding"
39
+ [textClassNames]="textCls"
40
+ [isIcon]="iconEnabled()"
41
+ [isFocusOutlinedEnabled]="false"
42
+ [disabled]="disabled()"
43
+ (action)="microsoftSignin()"
44
+ >
45
+ <ba-ti-vendor-microsoft slot="icon" class="bas-h-6 bas-w-6 bas-text-white" />
46
+ </ba-btn-base>
47
+ }
48
+ `,
49
+ })
50
+ export class BtnMicrosoftComponent {
51
+ isDropdownProfile = input(false);
52
+ text = input('Microsoft');
53
+ iconEnabled = input(true);
54
+ disabled = input(false);
55
+ isEditable = input(false);
56
+
57
+ clicked = output<void>();
58
+
59
+ private client = injectBlockAuthClient();
60
+
61
+ bg = msStyles.bgClassNames;
62
+ ring = msStyles.ringClassNames;
63
+ padding = msStyles.paddingClassNames;
64
+ textCls = msStyles.textClassNames;
65
+
66
+ async microsoftSignin(): Promise<void> {
67
+ if (this.isEditable()) return;
68
+ try {
69
+ await this.client.oauth.signupMicrosoft();
70
+ } catch (err) {
71
+ // Exponer mala configuración (clientId ausente, errores MSAL) y
72
+ // cancelaciones en vez de tragárnoslas.
73
+ console.warn('[blockauth] Microsoft sign-in failed:', err);
74
+ }
75
+ this.clicked.emit();
76
+ }
77
+ }
@@ -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 { BtnPhantomComponent } from './btn-phantom.component';
5
+ import { provideBlockAuthClientForTest } from '../../testing';
6
+
7
+ describe('BtnPhantom', () => {
8
+ it('al hacer click dispara client.web3.signup("phantom")', async () => {
9
+ const signupSpy = vi.fn(async () => undefined);
10
+ const fakeClient = {
11
+ web3: { signup: signupSpy },
12
+ state: { $wallet: map({ address: null, isConnecting: false }) },
13
+ } as any;
14
+ await render(BtnPhantomComponent, {
15
+ providers: [provideBlockAuthClientForTest(fakeClient)],
16
+ });
17
+ await fireEvent.click(screen.getByRole('button'));
18
+ await waitFor(() => expect(signupSpy).toHaveBeenCalledWith('phantom'));
19
+ });
20
+ });
@@ -0,0 +1,93 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
2
+ import { BtnBaseComponent } from '../../primitives/btn-base.component';
3
+ import { imgUrlByService } from '../../primitives/helpers';
4
+ import { injectBlockAuthClient } from '../../provide-block-auth';
5
+ import { injectStore } from '../../inject-store';
6
+
7
+ /**
8
+ * Botón de conexión Phantom (Solana). El click delega en
9
+ * `client.web3.signup('phantom')`.
10
+ */
11
+ const styles = {
12
+ bgClassNames: 'bas-bg-[#9092e7] hover:bas-bg-[#7b7de2]',
13
+ disabledBgClassNames: 'bas-bg-[rgba(144,146,231,0.75)] hover:bas-bg-[rgba(123,125,226,0.75)]',
14
+ ringClassNames: 'bal-ba-hover-ring-puple-400',
15
+ paddingClassNames: 'bas-px-12',
16
+ textClassNames: 'bas-text-white bas-font-semibold bas-leading-6 bas-text-base',
17
+ loaderWidth: 70,
18
+ loaderColor: '#fefefe',
19
+ };
20
+
21
+ @Component({
22
+ selector: 'ba-btn-phantom',
23
+ standalone: true,
24
+ changeDetection: ChangeDetectionStrategy.OnPush,
25
+ imports: [BtnBaseComponent],
26
+ template: `
27
+ @if (isDropdownProfile()) {
28
+ <button
29
+ id="btn-provider-phantom-dropdown"
30
+ class="bas-h-full bas-w-full bas-border-0 bas-bg-transparent bas-p-0"
31
+ (click)="onPhantomSignin()"
32
+ ><ng-content /></button>
33
+ } @else {
34
+ <ba-btn-base
35
+ btnId="btn-provider-phantom"
36
+ [text]="btnText()"
37
+ type="button-outlined"
38
+ size="small"
39
+ [bgClassNames]="wallet().isConnecting ? disabledBg : bg"
40
+ [ringClassNames]="ring"
41
+ [paddingClassNames]="padding"
42
+ [textClassNames]="textCls"
43
+ [isLoader]="wallet().isConnecting"
44
+ [loaderWidth]="loaderWidth"
45
+ [loaderColor]="loaderColor"
46
+ [isIcon]="iconEnabled()"
47
+ [isFocusOutlinedEnabled]="false"
48
+ [disabled]="disabled()"
49
+ (action)="onPhantomSignin()"
50
+ >
51
+ <img slot="icon" [src]="phantomIconSrc" height="24" width="24" alt="Phantom" />
52
+ </ba-btn-base>
53
+ }
54
+ `,
55
+ })
56
+ export class BtnPhantomComponent {
57
+ isDropdownProfile = input(false);
58
+ text = input('Phantom');
59
+ isTextInsteadOfAccount = input(false);
60
+ iconEnabled = input(true);
61
+ disabled = input(false);
62
+ isEditable = input(false);
63
+
64
+ clicked = output<void>();
65
+
66
+ private client = injectBlockAuthClient();
67
+ wallet = injectStore(this.client.state.$wallet);
68
+
69
+ bg = styles.bgClassNames;
70
+ disabledBg = styles.disabledBgClassNames;
71
+ ring = styles.ringClassNames;
72
+ padding = styles.paddingClassNames;
73
+ textCls = styles.textClassNames;
74
+ loaderWidth = styles.loaderWidth;
75
+ loaderColor = styles.loaderColor;
76
+ phantomIconSrc = imgUrlByService('phantom-white');
77
+
78
+ btnText = computed(() => {
79
+ const short = (a: string | null) => (a ? `${a.slice(0, 6)}…${a.slice(-4)}` : '');
80
+ return this.isTextInsteadOfAccount() ? this.text() : short(this.wallet().address) || this.text();
81
+ });
82
+
83
+ async onPhantomSignin(): Promise<void> {
84
+ if (this.isEditable()) return;
85
+ if (this.wallet().isConnecting) return;
86
+ try {
87
+ await this.client.web3.signup('phantom');
88
+ } catch {
89
+ /* conexión/firma cancelada o fallida */
90
+ }
91
+ this.clicked.emit();
92
+ }
93
+ }
@@ -0,0 +1,55 @@
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 { map } from 'nanostores';
5
+ import { BtnSigninComponent } from './btn-signin.component';
6
+ import { provideBlockAuthClientForTest } from '../../testing';
7
+
8
+ function makeClient(ui: Record<string, unknown> = {}) {
9
+ const requestSignin = vi.fn();
10
+ const openModal = vi.fn();
11
+ const $ui = map({ isSigning: false, serviceName: null, uiConfig: null, ...ui });
12
+ const client = { requestSignin, openModal, state: { $ui } } as any;
13
+ return { client, requestSignin, openModal, $ui };
14
+ }
15
+
16
+ describe('BtnSignin', () => {
17
+ it('renderiza con el id correcto', async () => {
18
+ const { client } = makeClient();
19
+ await render(BtnSigninComponent, { providers: [provideBlockAuthClientForTest(client)] });
20
+ expect(screen.getByRole('button').id).toBe('btn-login-signup');
21
+ });
22
+
23
+ it('al hacer click llama requestSignin y NO openModal si enabled es falsy', async () => {
24
+ const { client, requestSignin, openModal } = makeClient();
25
+ await render(BtnSigninComponent, { providers: [provideBlockAuthClientForTest(client)] });
26
+ await fireEvent.click(screen.getByRole('button'));
27
+ expect(requestSignin).toHaveBeenCalledOnce();
28
+ expect(openModal).not.toHaveBeenCalled();
29
+ });
30
+
31
+ it('abre el modal signin si uiConfig.config[serviceName].enabled es true', async () => {
32
+ const { client, requestSignin, openModal } = makeClient({
33
+ serviceName: 'blockauth',
34
+ uiConfig: { config: { blockauth: { enabled: true } } },
35
+ });
36
+ await render(BtnSigninComponent, { providers: [provideBlockAuthClientForTest(client)] });
37
+ await fireEvent.click(screen.getByRole('button'));
38
+ expect(requestSignin).toHaveBeenCalled();
39
+ expect(openModal).toHaveBeenCalledWith('signin');
40
+ });
41
+
42
+ it('no llama requestSignin ni openModal si isEditable es true', async () => {
43
+ const { client, requestSignin, openModal } = makeClient();
44
+ @Component({
45
+ standalone: true,
46
+ imports: [BtnSigninComponent],
47
+ template: `<ba-btn-signin [isEditable]="true" />`,
48
+ })
49
+ class HostComponent {}
50
+ await render(HostComponent, { providers: [provideBlockAuthClientForTest(client)] });
51
+ await fireEvent.click(screen.getByRole('button'));
52
+ expect(requestSignin).not.toHaveBeenCalled();
53
+ expect(openModal).not.toHaveBeenCalled();
54
+ });
55
+ });