@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.
- package/.turbo/turbo-build.log +37 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +82 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +10 -0
- package/ng-package.json +6 -0
- package/package.json +50 -0
- package/scripts/css-to-ts.mjs +14 -0
- package/src/lib/__tests__/integration.test.ts +45 -0
- package/src/lib/block-auth.service.test.ts +69 -0
- package/src/lib/block-auth.service.ts +81 -0
- package/src/lib/components/btns/btn-base-provider.component.test.ts +22 -0
- package/src/lib/components/btns/btn-base-provider.component.ts +55 -0
- package/src/lib/components/btns/btn-block-auth.component.test.ts +79 -0
- package/src/lib/components/btns/btn-block-auth.component.ts +104 -0
- package/src/lib/components/btns/btn-gmail.component.test.ts +16 -0
- package/src/lib/components/btns/btn-gmail.component.ts +41 -0
- package/src/lib/components/btns/btn-google-base.component.test.ts +16 -0
- package/src/lib/components/btns/btn-google-base.component.ts +73 -0
- package/src/lib/components/btns/btn-metamask.component.test.ts +20 -0
- package/src/lib/components/btns/btn-metamask.component.ts +98 -0
- package/src/lib/components/btns/btn-microsoft.component.test.ts +16 -0
- package/src/lib/components/btns/btn-microsoft.component.ts +77 -0
- package/src/lib/components/btns/btn-phantom.component.test.ts +20 -0
- package/src/lib/components/btns/btn-phantom.component.ts +93 -0
- package/src/lib/components/btns/btn-signin.component.test.ts +55 -0
- package/src/lib/components/btns/btn-signin.component.ts +108 -0
- package/src/lib/components/btns/btn-wallet-connect.component.test.ts +20 -0
- package/src/lib/components/btns/btn-wallet-connect.component.ts +109 -0
- package/src/lib/components/btns/index.ts +11 -0
- package/src/lib/components/contents/content-signin.component.test.ts +33 -0
- package/src/lib/components/contents/content-signin.component.ts +68 -0
- package/src/lib/components/contents/content-signup.component.test.ts +78 -0
- package/src/lib/components/contents/content-signup.component.ts +225 -0
- package/src/lib/components/dds/change-account.component.test.ts +66 -0
- package/src/lib/components/dds/change-account.component.ts +112 -0
- package/src/lib/components/dds/dropdown-profile-btn.component.test.ts +66 -0
- package/src/lib/components/dds/dropdown-profile-btn.component.ts +185 -0
- package/src/lib/components/dds/dropdown-profile-item.component.test.ts +75 -0
- package/src/lib/components/dds/dropdown-profile-item.component.ts +74 -0
- package/src/lib/components/dds/dropdown-profile.component.test.ts +98 -0
- package/src/lib/components/dds/dropdown-profile.component.ts +258 -0
- package/src/lib/components/flow-block-auth.component.test.ts +93 -0
- package/src/lib/components/flow-block-auth.component.ts +226 -0
- package/src/lib/components/modals/modal-overlay.component.test.ts +81 -0
- package/src/lib/components/modals/modal-overlay.component.ts +43 -0
- package/src/lib/components/modals/modal-restoration-expired.component.test.ts +71 -0
- package/src/lib/components/modals/modal-restoration-expired.component.ts +83 -0
- package/src/lib/components/modals/modal-restore.component.test.ts +74 -0
- package/src/lib/components/modals/modal-restore.component.ts +104 -0
- package/src/lib/components/modals/modal-signin.component.test.ts +66 -0
- package/src/lib/components/modals/modal-signin.component.ts +44 -0
- package/src/lib/components/modals/modal-signup.component.test.ts +56 -0
- package/src/lib/components/modals/modal-signup.component.ts +40 -0
- package/src/lib/components/sections/footer-section.component.test.ts +19 -0
- package/src/lib/components/sections/footer-section.component.ts +49 -0
- package/src/lib/components/sections/internals/providers-buttons.component.test.ts +27 -0
- package/src/lib/components/sections/internals/providers-buttons.component.ts +290 -0
- package/src/lib/components/sections/internals/providers-filters.component.test.ts +25 -0
- package/src/lib/components/sections/internals/providers-filters.component.ts +139 -0
- package/src/lib/components/sections/internals/selected-button.component.test.ts +36 -0
- package/src/lib/components/sections/internals/selected-button.component.ts +92 -0
- package/src/lib/components/sections/login-section.component.test.ts +49 -0
- package/src/lib/components/sections/login-section.component.ts +120 -0
- package/src/lib/components/sections/providers-section.component.test.ts +22 -0
- package/src/lib/components/sections/providers-section.component.ts +187 -0
- package/src/lib/components/txns/internals/permissions-required-modal.component.test.ts +14 -0
- package/src/lib/components/txns/internals/permissions-required-modal.component.ts +35 -0
- package/src/lib/components/txns/internals/solicited-data-modal.component.test.ts +72 -0
- package/src/lib/components/txns/internals/solicited-data-modal.component.ts +187 -0
- package/src/lib/components/txns/internals/txn-error.component.test.ts +100 -0
- package/src/lib/components/txns/internals/txn-error.component.ts +121 -0
- package/src/lib/components/txns/internals/txn-type.component.test.ts +52 -0
- package/src/lib/components/txns/internals/txn-type.component.ts +51 -0
- package/src/lib/components/txns/internals/txn.component.test.ts +188 -0
- package/src/lib/components/txns/internals/txn.component.ts +298 -0
- package/src/lib/components/txns/txns-overview.component.test.ts +107 -0
- package/src/lib/components/txns/txns-overview.component.ts +229 -0
- package/src/lib/icons/ai-outline-disconnect.component.ts +21 -0
- package/src/lib/icons/bi-logo-gmail.component.ts +21 -0
- package/src/lib/icons/cg-profile.component.ts +31 -0
- package/src/lib/icons/check-icon.component.ts +27 -0
- package/src/lib/icons/ci-clock2.component.ts +31 -0
- package/src/lib/icons/fa-arrow-right.component.ts +21 -0
- package/src/lib/icons/fa-times.component.ts +21 -0
- package/src/lib/icons/fa-undo.component.ts +21 -0
- package/src/lib/icons/icons.test.ts +25 -0
- package/src/lib/icons/index.ts +22 -0
- package/src/lib/icons/io-ios-arrow-back.component.ts +21 -0
- package/src/lib/icons/io-ios-arrow-forward.component.ts +21 -0
- package/src/lib/icons/io-md-arrow-back.component.ts +19 -0
- package/src/lib/icons/io-md-arrow-forward.component.ts +19 -0
- package/src/lib/icons/pi-warning-fill.component.ts +21 -0
- package/src/lib/icons/tb-arrows-exchange2.component.ts +30 -0
- package/src/lib/icons/tb-copy-check-filled.component.ts +21 -0
- package/src/lib/icons/tb-copy.component.ts +34 -0
- package/src/lib/icons/ti-vendor-microsoft.component.ts +21 -0
- package/src/lib/inject-store.test.ts +26 -0
- package/src/lib/inject-store.ts +16 -0
- package/src/lib/primitives/arrow-icon.component.test.ts +12 -0
- package/src/lib/primitives/arrow-icon.component.ts +38 -0
- package/src/lib/primitives/btn-arrow-dir.component.test.ts +22 -0
- package/src/lib/primitives/btn-arrow-dir.component.ts +65 -0
- package/src/lib/primitives/btn-base.component.test.ts +38 -0
- package/src/lib/primitives/btn-base.component.ts +140 -0
- package/src/lib/primitives/divider-base.component.test.ts +12 -0
- package/src/lib/primitives/divider-base.component.ts +74 -0
- package/src/lib/primitives/dropdown-arrow.component.test.ts +12 -0
- package/src/lib/primitives/dropdown-arrow.component.ts +46 -0
- package/src/lib/primitives/dropdown-base.component.test.ts +49 -0
- package/src/lib/primitives/dropdown-base.component.ts +329 -0
- package/src/lib/primitives/helpers.ts +151 -0
- package/src/lib/primitives/horizontal-loader.component.test.ts +19 -0
- package/src/lib/primitives/horizontal-loader.component.ts +70 -0
- package/src/lib/primitives/icon-base.component.test.ts +19 -0
- package/src/lib/primitives/icon-base.component.ts +58 -0
- package/src/lib/primitives/img-loader.component.test.ts +12 -0
- package/src/lib/primitives/img-loader.component.ts +118 -0
- package/src/lib/primitives/index.ts +29 -0
- package/src/lib/primitives/input-base.component.test.ts +20 -0
- package/src/lib/primitives/input-base.component.ts +210 -0
- package/src/lib/primitives/lifetime-slider-base.component.test.ts +88 -0
- package/src/lib/primitives/lifetime-slider-base.component.ts +75 -0
- package/src/lib/primitives/oval-spinner.component.test.ts +9 -0
- package/src/lib/primitives/oval-spinner.component.ts +158 -0
- package/src/lib/primitives/pickers/dropdown-picker-animated-base.component.ts +220 -0
- package/src/lib/primitives/pickers/picker-button.component.ts +74 -0
- package/src/lib/primitives/pickers/time-picker-dropdown-selector.component.ts +188 -0
- package/src/lib/primitives/pickers/time-picker.component.ts +124 -0
- package/src/lib/primitives/pickers/time-unit-selector.component.ts +82 -0
- package/src/lib/primitives/progressbar.component.test.ts +18 -0
- package/src/lib/primitives/progressbar.component.ts +93 -0
- package/src/lib/primitives/providersStyles.ts +268 -0
- package/src/lib/primitives/slider-base.component.ts +56 -0
- package/src/lib/provide-block-auth.test.ts +43 -0
- package/src/lib/provide-block-auth.ts +43 -0
- package/src/lib/runtime/block-auth-runtime.directive.test.ts +89 -0
- package/src/lib/runtime/block-auth-runtime.directive.ts +68 -0
- package/src/lib/teleport-body.directive.test.ts +29 -0
- package/src/lib/teleport-body.directive.ts +21 -0
- package/src/lib/testing.ts +15 -0
- package/src/lib/transitions.css +23 -0
- package/src/public-api.ts +85 -0
- package/src/vitest.setup.ts +8 -0
- package/tailwind.config.cjs +6 -0
- package/tsconfig.json +15 -0
- package/tsconfig.lib.json +9 -0
- package/vite.config.mts +44 -0
|
@@ -0,0 +1,66 @@
|
|
|
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 { ChangeAccountComponent } from './change-account.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
|
+
// Port de packages/react/src/components/dds/ChangeAccountComponent.test.tsx
|
|
18
|
+
// (mapa: packages/vue/.../ChangeAccountComponent.test.ts).
|
|
19
|
+
describe('ChangeAccountComponent', () => {
|
|
20
|
+
function makeClient() {
|
|
21
|
+
return {
|
|
22
|
+
web3: { signup: vi.fn() },
|
|
23
|
+
oauth: { signupGoogle: vi.fn(), signupMicrosoft: vi.fn() },
|
|
24
|
+
connectBlockAuth: vi.fn(),
|
|
25
|
+
state: { $ui: map({}), $ws: map({}), $wallet: map({}) },
|
|
26
|
+
} as any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const renderFor = (serviceName: string) =>
|
|
30
|
+
render(ChangeAccountComponent, {
|
|
31
|
+
inputs: { serviceName, text: 'Change account' },
|
|
32
|
+
providers: [provideBlockAuthClientForTest(makeClient())],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renderiza el texto "Change account" para el serviceName blockauth', async () => {
|
|
36
|
+
await renderFor('blockauth');
|
|
37
|
+
expect(screen.getByText('Change account')).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
for (const svc of ['blockauth', 'google', 'microsoft', 'metamask', 'phantom']) {
|
|
41
|
+
it(`renderiza para el provider soportado "${svc}"`, async () => {
|
|
42
|
+
await renderFor(svc);
|
|
43
|
+
expect(screen.getByText('Change account')).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it('no renderiza ningun boton para un serviceName desconocido', async () => {
|
|
48
|
+
await renderFor('desconocido');
|
|
49
|
+
expect(screen.queryByText('Change account')).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('emite changeAccountClicked al pulsar la fila', async () => {
|
|
53
|
+
const onChange = vi.fn();
|
|
54
|
+
@Component({
|
|
55
|
+
standalone: true,
|
|
56
|
+
imports: [ChangeAccountComponent],
|
|
57
|
+
template: `<ba-change-account serviceName="blockauth" text="Change account" (changeAccountClicked)="f()" />`,
|
|
58
|
+
})
|
|
59
|
+
class Host {
|
|
60
|
+
f = onChange;
|
|
61
|
+
}
|
|
62
|
+
await render(Host, { providers: [provideBlockAuthClientForTest(makeClient())] });
|
|
63
|
+
await fireEvent.click(screen.getByText('Change account'));
|
|
64
|
+
expect(onChange).toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
|
2
|
+
import { TbArrowsExchange2Component } from '../../icons';
|
|
3
|
+
import { BtnBlockAuthComponent } from '../btns/btn-block-auth.component';
|
|
4
|
+
import { BtnGmailComponent } from '../btns/btn-gmail.component';
|
|
5
|
+
import { BtnMicrosoftComponent } from '../btns/btn-microsoft.component';
|
|
6
|
+
import { BtnMetamaskComponent } from '../btns/btn-metamask.component';
|
|
7
|
+
import { BtnPhantomComponent } from '../btns/btn-phantom.component';
|
|
8
|
+
import { DropdownProfileItemComponent } from './dropdown-profile-item.component';
|
|
9
|
+
|
|
10
|
+
// Rama de 5 vías sobre `serviceName` (blockauth/google/microsoft/metamask/
|
|
11
|
+
// phantom): cada una envuelve un `<ba-dropdown-profile-item>` (con el icono de
|
|
12
|
+
// intercambio) dentro del botón del provider en modo `isDropdownProfile`, que
|
|
13
|
+
// re-dispara el flujo de conexión de ese provider.
|
|
14
|
+
//
|
|
15
|
+
// El click real de "cambiar cuenta" no lo dispara este output por sí solo: el
|
|
16
|
+
// `<button>` nativo de cada `Btn*` en modo `isDropdownProfile` tiene su propio
|
|
17
|
+
// `(click)` (que ejecuta el signin/connect real); el `dropdown-profile-item`
|
|
18
|
+
// anidado es un `<div>` hijo de ese botón, y al clicarlo el evento burbujea al
|
|
19
|
+
// botón exterior, disparando AMBOS handlers.
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'ba-change-account',
|
|
22
|
+
standalone: true,
|
|
23
|
+
// Igual que `ba-dropdown-profile-item`: host transparente al layout para que
|
|
24
|
+
// el <div bas-w-full> sea hijo directo del flex de la <ul> y llene el ancho
|
|
25
|
+
// (con `bas-items-center` en la ul, un host inline se encogería y centraría).
|
|
26
|
+
host: { style: 'display: contents' },
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
imports: [
|
|
29
|
+
BtnBlockAuthComponent,
|
|
30
|
+
BtnGmailComponent,
|
|
31
|
+
BtnMicrosoftComponent,
|
|
32
|
+
BtnMetamaskComponent,
|
|
33
|
+
BtnPhantomComponent,
|
|
34
|
+
DropdownProfileItemComponent,
|
|
35
|
+
],
|
|
36
|
+
template: `
|
|
37
|
+
<div class="bas-h-9 bas-w-full">
|
|
38
|
+
@switch (serviceName()) {
|
|
39
|
+
@case ('blockauth') {
|
|
40
|
+
<ba-btn-block-auth [text]="text()" [isDropdownProfile]="true">
|
|
41
|
+
<ba-dropdown-profile-item
|
|
42
|
+
id="ba-ddi-change-account-1"
|
|
43
|
+
[text]="text()"
|
|
44
|
+
[firstIcon]="exchangeIcon"
|
|
45
|
+
[isTopBorder]="isTopBorder()"
|
|
46
|
+
[disabled]="disabled()"
|
|
47
|
+
(clicked)="changeAccountClicked.emit()"
|
|
48
|
+
/>
|
|
49
|
+
</ba-btn-block-auth>
|
|
50
|
+
}
|
|
51
|
+
@case ('google') {
|
|
52
|
+
<ba-btn-gmail [text]="text()" [isDropdownProfile]="true">
|
|
53
|
+
<ba-dropdown-profile-item
|
|
54
|
+
id="ba-ddi-change-account-1"
|
|
55
|
+
[text]="text()"
|
|
56
|
+
[firstIcon]="exchangeIcon"
|
|
57
|
+
[isTopBorder]="isTopBorder()"
|
|
58
|
+
[disabled]="disabled()"
|
|
59
|
+
(clicked)="changeAccountClicked.emit()"
|
|
60
|
+
/>
|
|
61
|
+
</ba-btn-gmail>
|
|
62
|
+
}
|
|
63
|
+
@case ('microsoft') {
|
|
64
|
+
<ba-btn-microsoft [text]="text()" [isDropdownProfile]="true">
|
|
65
|
+
<ba-dropdown-profile-item
|
|
66
|
+
id="ba-ddi-change-account-1"
|
|
67
|
+
[text]="text()"
|
|
68
|
+
[firstIcon]="exchangeIcon"
|
|
69
|
+
[isTopBorder]="isTopBorder()"
|
|
70
|
+
[disabled]="disabled()"
|
|
71
|
+
(clicked)="changeAccountClicked.emit()"
|
|
72
|
+
/>
|
|
73
|
+
</ba-btn-microsoft>
|
|
74
|
+
}
|
|
75
|
+
@case ('metamask') {
|
|
76
|
+
<ba-btn-metamask [text]="text()" [isDropdownProfile]="true">
|
|
77
|
+
<ba-dropdown-profile-item
|
|
78
|
+
id="ba-ddi-change-account-1"
|
|
79
|
+
[text]="text()"
|
|
80
|
+
[firstIcon]="exchangeIcon"
|
|
81
|
+
[isTopBorder]="isTopBorder()"
|
|
82
|
+
[disabled]="disabled()"
|
|
83
|
+
(clicked)="changeAccountClicked.emit()"
|
|
84
|
+
/>
|
|
85
|
+
</ba-btn-metamask>
|
|
86
|
+
}
|
|
87
|
+
@case ('phantom') {
|
|
88
|
+
<ba-btn-phantom [text]="text()" [isDropdownProfile]="true">
|
|
89
|
+
<ba-dropdown-profile-item
|
|
90
|
+
id="ba-ddi-change-account-1"
|
|
91
|
+
[text]="text()"
|
|
92
|
+
[firstIcon]="exchangeIcon"
|
|
93
|
+
[isTopBorder]="isTopBorder()"
|
|
94
|
+
[disabled]="disabled()"
|
|
95
|
+
(clicked)="changeAccountClicked.emit()"
|
|
96
|
+
/>
|
|
97
|
+
</ba-btn-phantom>
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</div>
|
|
101
|
+
`,
|
|
102
|
+
})
|
|
103
|
+
export class ChangeAccountComponent {
|
|
104
|
+
serviceName = input('');
|
|
105
|
+
text = input('');
|
|
106
|
+
disabled = input(false);
|
|
107
|
+
isTopBorder = input(false);
|
|
108
|
+
|
|
109
|
+
changeAccountClicked = output<void>();
|
|
110
|
+
|
|
111
|
+
protected readonly exchangeIcon = TbArrowsExchange2Component;
|
|
112
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { DropdownProfileBtnComponent } from './dropdown-profile-btn.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
|
+
// Port de packages/react/src/components/dds/DropdownProfileBtn.test.tsx (mapa:
|
|
18
|
+
// packages/vue/.../DropdownProfileBtn.test.ts).
|
|
19
|
+
describe('DropdownProfileBtn', () => {
|
|
20
|
+
function makeClient(ws: Record<string, any> = {}) {
|
|
21
|
+
return { state: { $ui: map({}), $ws: map({ ...ws }) } } as any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it('renderiza el data-service del serviceName y emite clicked al pulsar', async () => {
|
|
25
|
+
const onClick = vi.fn();
|
|
26
|
+
@Component({
|
|
27
|
+
standalone: true,
|
|
28
|
+
imports: [DropdownProfileBtnComponent],
|
|
29
|
+
template: `<ba-dropdown-profile-btn serviceName="blockauth" (clicked)="f()" />`,
|
|
30
|
+
})
|
|
31
|
+
class Host {
|
|
32
|
+
f = onClick;
|
|
33
|
+
}
|
|
34
|
+
await render(Host, { providers: [provideBlockAuthClientForTest(makeClient())] });
|
|
35
|
+
|
|
36
|
+
const btn = screen.getByTestId('dropdown-profile-btn');
|
|
37
|
+
expect(btn.getAttribute('data-service')).toBe('blockauth');
|
|
38
|
+
await fireEvent.click(btn);
|
|
39
|
+
expect(onClick).toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('copia el DID al clicar la fila del DID sin propagar el click al boton', async () => {
|
|
43
|
+
const writeText = vi.fn();
|
|
44
|
+
Object.assign(navigator, { clipboard: { writeText } });
|
|
45
|
+
const onClick = vi.fn();
|
|
46
|
+
@Component({
|
|
47
|
+
standalone: true,
|
|
48
|
+
imports: [DropdownProfileBtnComponent],
|
|
49
|
+
template: `<ba-dropdown-profile-btn serviceName="blockauth" (clicked)="f()" />`,
|
|
50
|
+
})
|
|
51
|
+
class Host {
|
|
52
|
+
f = onClick;
|
|
53
|
+
}
|
|
54
|
+
// Forzar un DID legible desde $ws (fallback cuando storage está vacío).
|
|
55
|
+
await render(Host, {
|
|
56
|
+
providers: [provideBlockAuthClientForTest(makeClient({ did: '0xabcdef1234567890' }))],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const didRow = screen.getByText(/^did:/);
|
|
60
|
+
await fireEvent.click(didRow);
|
|
61
|
+
// Copia el DID completo, no la etiqueta acortada.
|
|
62
|
+
expect(writeText).toHaveBeenCalledWith('0xabcdef1234567890');
|
|
63
|
+
// stopPropagation: el click de copiar no debe emitir el click del botón.
|
|
64
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input, output, signal } from '@angular/core';
|
|
2
|
+
import { utils } from '@block-auth.io/core';
|
|
3
|
+
import { injectBlockAuthClient } from '../../provide-block-auth';
|
|
4
|
+
import { injectStore } from '../../inject-store';
|
|
5
|
+
import { providersStyles } from '../../primitives/providersStyles';
|
|
6
|
+
import { DropdownArrowComponent } from '../../primitives/dropdown-arrow.component';
|
|
7
|
+
import { TbCopyComponent, TbCopyCheckFilledComponent } from '../../icons';
|
|
8
|
+
|
|
9
|
+
// El click-outside lo maneja el PADRE, que envuelve este componente en su
|
|
10
|
+
// propio `<div>` con una template ref; por eso aquí no hay prop de contenedor.
|
|
11
|
+
// Los `classNames` por tamaño salen de mapas indexados por `dropdownBtnSize`.
|
|
12
|
+
//
|
|
13
|
+
// `rawDid`/`rawAccount` se leen dentro de `computed()`s que dependen de `ui`/`ws`
|
|
14
|
+
// (relee storage vía `utils.browserBase` en cada recompute).
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'ba-dropdown-profile-btn',
|
|
17
|
+
standalone: true,
|
|
18
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
|
+
imports: [DropdownArrowComponent, TbCopyComponent, TbCopyCheckFilledComponent],
|
|
20
|
+
template: `
|
|
21
|
+
<div
|
|
22
|
+
data-testid="dropdown-profile-btn"
|
|
23
|
+
[attr.data-service]="serviceNameResolved()"
|
|
24
|
+
[class]="
|
|
25
|
+
'bal-ba-transition-2 bas-group bas-relative bas-box-border bas-grid bas-h-full bas-w-full bas-cursor-pointer bas-flex-row bas-items-center bas-justify-between bas-gap-1 bas-overflow-hidden bas-rounded-lg bas-mx-auto ' +
|
|
26
|
+
dropdownBtnBgClassNames() + ' bas-border-2 bas-border-solid ' +
|
|
27
|
+
gridClassNames() + ' ' +
|
|
28
|
+
(dropdownBtnBorderClassNames() || style().borderStyles + ' ' + style().hoverBorderStyles)
|
|
29
|
+
"
|
|
30
|
+
[style.minHeight]="height() != null ? height() + 'px' : null"
|
|
31
|
+
[style.width]="widthStyle()"
|
|
32
|
+
(click)="clicked.emit()"
|
|
33
|
+
>
|
|
34
|
+
<span
|
|
35
|
+
[class]="
|
|
36
|
+
'bas-absolute bas-right-2.5 bas-top-1.5 bas-h-2 bas-w-2 bas-rounded-full ' +
|
|
37
|
+
(!isSignedFirstCheck() ? 'bas-bg-gray-400' : !isSigned() ? 'bas-bg-red-400' : 'bas-bg-green-400')
|
|
38
|
+
"
|
|
39
|
+
></span>
|
|
40
|
+
<div class="bas-z-10 bas-flex bas-h-full bas-w-full bas-items-center bas-justify-center bas-rounded-md">
|
|
41
|
+
<img
|
|
42
|
+
[src]="style().imgSrc"
|
|
43
|
+
[alt]="serviceNameResolved() + ' icon'"
|
|
44
|
+
[class]="'bal-ba-transition-2 bal-ba-transform-base bal-ba-hover-scale-110 bas-rounded-md ' + imgSizesClassNames()"
|
|
45
|
+
[style.backgroundColor]="style().imgBgColor"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
<div
|
|
49
|
+
[class]="'bal-ba-transition-2 bas-flex bas-h-full bas-w-full bas-flex-col bas-items-center bas-justify-center bas-font-semibold bas-min-w-0 ' + dropdownBtnTextColorClassNames()"
|
|
50
|
+
>
|
|
51
|
+
<div
|
|
52
|
+
[class]="
|
|
53
|
+
'bal-ba-transition-2 bas-flex bas-max-w-full bas-flex-row bas-items-center bas-justify-center bas-gap-1 bas-cursor-pointer ' +
|
|
54
|
+
(isDidCopied() ? 'bas-text-[#4d849b]' : 'hover:bas-opacity-50')
|
|
55
|
+
"
|
|
56
|
+
(click)="onCopyDid($event)"
|
|
57
|
+
>
|
|
58
|
+
<p class="bas-m-0 bas-truncate bas-text-sm">{{ did() }}</p>
|
|
59
|
+
@if (isDidCopied()) {
|
|
60
|
+
<ba-tb-copy-check-filled class="bal-ba-transition-2 bas-h-[18px] bas-w-[18px] bas-shrink-0" />
|
|
61
|
+
} @else {
|
|
62
|
+
<ba-tb-copy class="bal-ba-transition-2 bas-h-[18px] bas-w-[18px] bas-shrink-0" />
|
|
63
|
+
}
|
|
64
|
+
</div>
|
|
65
|
+
@if (showAccountRow()) {
|
|
66
|
+
<div
|
|
67
|
+
[class]="
|
|
68
|
+
'bal-ba-transition-2 bas-flex bas-max-w-full bas-flex-row bas-items-center bas-justify-center bas-gap-1 bas-cursor-pointer ' +
|
|
69
|
+
(isAccountCopied() ? 'bas-text-[#4d849b]' : 'hover:bas-opacity-50')
|
|
70
|
+
"
|
|
71
|
+
(click)="onCopyAccount($event)"
|
|
72
|
+
>
|
|
73
|
+
<p class="bas-m-0 bas-truncate bas-text-xs">{{ accountSubtitle() }}</p>
|
|
74
|
+
@if (isAccountCopied()) {
|
|
75
|
+
<ba-tb-copy-check-filled class="bal-ba-transition-2 bas-h-4 bas-w-4 bas-shrink-0" />
|
|
76
|
+
} @else {
|
|
77
|
+
<ba-tb-copy class="bal-ba-transition-2 bas-h-4 bas-w-4 bas-shrink-0" />
|
|
78
|
+
}
|
|
79
|
+
</div>
|
|
80
|
+
}
|
|
81
|
+
</div>
|
|
82
|
+
<div [class]="arrowContainerClassNames()">
|
|
83
|
+
<ba-dropdown-arrow
|
|
84
|
+
id="ba-ddp-btn-arrow"
|
|
85
|
+
[isOpen]="isProfileOpen()"
|
|
86
|
+
[color]="style().colorBase"
|
|
87
|
+
[sizeClassNames]="arrowSizesClassNames()"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
`,
|
|
92
|
+
})
|
|
93
|
+
export class DropdownProfileBtnComponent {
|
|
94
|
+
serviceName = input<string | null>(null);
|
|
95
|
+
dropdownBtnBgClassNames = input('bas-bg-transparent');
|
|
96
|
+
dropdownBtnSize = input<'xs' | 'small' | 'base' | 'large'>('xs');
|
|
97
|
+
dropdownBtnBorderClassNames = input<string | undefined>(undefined);
|
|
98
|
+
dropdownBtnTextColorClassNames = input(
|
|
99
|
+
'bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]',
|
|
100
|
+
);
|
|
101
|
+
height = input<number | null>(null);
|
|
102
|
+
isProfileOpen = input(false);
|
|
103
|
+
|
|
104
|
+
clicked = output<void>();
|
|
105
|
+
|
|
106
|
+
private readonly client = injectBlockAuthClient();
|
|
107
|
+
private readonly ui = injectStore(this.client.state.$ui);
|
|
108
|
+
private readonly ws = injectStore(this.client.state.$ws);
|
|
109
|
+
|
|
110
|
+
private readonly gridClassNamesBySize: Record<string, string> = {
|
|
111
|
+
xs: 'bas-grid-cols-[46px_1fr_28px]',
|
|
112
|
+
small: 'bas-grid-cols-[46px_1fr_28px]',
|
|
113
|
+
base: 'bas-grid-cols-[56px_1fr_28px]',
|
|
114
|
+
large: 'bas-grid-cols-[56px_1fr_28px]',
|
|
115
|
+
};
|
|
116
|
+
private readonly widthBySize: Record<string, number> = { xs: 224, small: 224, base: 244, large: 244 };
|
|
117
|
+
private readonly imgSizesClassNamesBySize: Record<string, string> = {
|
|
118
|
+
xs: 'bas-h-9 bas-w-9',
|
|
119
|
+
small: 'bas-h-10 bas-w-10',
|
|
120
|
+
base: 'bas-h-12 bas-w-12',
|
|
121
|
+
large: 'bas-h-12 bas-w-12',
|
|
122
|
+
};
|
|
123
|
+
private readonly arrowSizesClassNamesBySize: Record<string, string> = {
|
|
124
|
+
xs: 'bas-h-3.5 bas-w-3.5',
|
|
125
|
+
small: 'bas-h-3.5 bas-w-3.5',
|
|
126
|
+
base: 'bas-h-4 bas-w-4',
|
|
127
|
+
large: 'bas-h-4 bas-w-4',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
protected readonly serviceNameResolved = computed(
|
|
131
|
+
() => this.serviceName() ?? this.ui().serviceName ?? 'blockauth',
|
|
132
|
+
);
|
|
133
|
+
protected readonly style = computed(
|
|
134
|
+
() => providersStyles[this.serviceNameResolved()] ?? providersStyles['blockauth'],
|
|
135
|
+
);
|
|
136
|
+
protected readonly isSignedFirstCheck = computed(() => this.ui().isSignedFirstCheck);
|
|
137
|
+
protected readonly isSigned = computed(() => this.ui().isSigned);
|
|
138
|
+
|
|
139
|
+
private short = (value: string, start: number, end: number): string =>
|
|
140
|
+
value.length > start + end ? `${value.slice(0, start)}…${value.slice(-end)}` : value;
|
|
141
|
+
|
|
142
|
+
private readonly rawDid = computed(() => {
|
|
143
|
+
const session = utils.browserBase.getSession();
|
|
144
|
+
const external = utils.browserBase.getExternalService();
|
|
145
|
+
return (this.serviceNameResolved() === 'blockauth' ? session.did : external.did) || this.ws().did || '';
|
|
146
|
+
});
|
|
147
|
+
protected readonly did = computed(() => this.short(this.rawDid(), 6, 4).replace('0x', 'did:'));
|
|
148
|
+
private readonly rawAccount = computed(() => utils.browserBase.getExternalService().account || '');
|
|
149
|
+
protected readonly accountSubtitle = computed(() =>
|
|
150
|
+
this.serviceNameResolved() === 'blockauth' ? '' : this.short(this.rawAccount(), 5, 8),
|
|
151
|
+
);
|
|
152
|
+
protected readonly showAccountRow = computed(
|
|
153
|
+
() => this.serviceNameResolved() !== 'blockauth' && !!this.rawAccount(),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
protected readonly isDidCopied = signal(false);
|
|
157
|
+
protected readonly isAccountCopied = signal(false);
|
|
158
|
+
|
|
159
|
+
protected readonly gridClassNames = computed(() => this.gridClassNamesBySize[this.dropdownBtnSize()]);
|
|
160
|
+
protected readonly imgSizesClassNames = computed(() => this.imgSizesClassNamesBySize[this.dropdownBtnSize()]);
|
|
161
|
+
protected readonly arrowSizesClassNames = computed(() => this.arrowSizesClassNamesBySize[this.dropdownBtnSize()]);
|
|
162
|
+
protected readonly widthStyle = computed(() => `min(100%, ${this.widthBySize[this.dropdownBtnSize()]}px)`);
|
|
163
|
+
protected readonly arrowContainerClassNames = computed(() => {
|
|
164
|
+
let classNames = 'bas-h-full bas-w-full bas-justify-center';
|
|
165
|
+
const size = this.dropdownBtnSize();
|
|
166
|
+
if (size === 'xs' || size === 'small') classNames += ' bas-pt-1.5';
|
|
167
|
+
return classNames;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
onCopyDid(e: MouseEvent): void {
|
|
171
|
+
this.copy(this.rawDid(), this.isDidCopied, e);
|
|
172
|
+
}
|
|
173
|
+
onCopyAccount(e: MouseEvent): void {
|
|
174
|
+
this.copy(this.rawAccount(), this.isAccountCopied, e);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private copy(value: string, copiedSignal: { set: (v: boolean) => void }, e: MouseEvent): void {
|
|
178
|
+
// Don't toggle the dropdown open/closed when the user only wants to copy.
|
|
179
|
+
e.stopPropagation();
|
|
180
|
+
if (!value) return;
|
|
181
|
+
void navigator.clipboard?.writeText(value);
|
|
182
|
+
copiedSignal.set(true);
|
|
183
|
+
setTimeout(() => copiedSignal.set(false), 3000);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { DropdownProfileItemComponent } from './dropdown-profile-item.component';
|
|
5
|
+
|
|
6
|
+
// Port de packages/react/src/components/dds/DropdownProfileItem.test.tsx (mapa:
|
|
7
|
+
// packages/vue/.../DropdownProfileItem.test.ts). Puramente presentacional.
|
|
8
|
+
describe('DropdownProfileItem', () => {
|
|
9
|
+
it('renderiza el texto y emite clicked al pulsar', async () => {
|
|
10
|
+
const onClick = vi.fn();
|
|
11
|
+
@Component({
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [DropdownProfileItemComponent],
|
|
14
|
+
template: `<ba-dropdown-profile-item text="Disconnect" (clicked)="f()" />`,
|
|
15
|
+
})
|
|
16
|
+
class Host {
|
|
17
|
+
f = onClick;
|
|
18
|
+
}
|
|
19
|
+
await render(Host);
|
|
20
|
+
await fireEvent.click(screen.getByText('Disconnect'));
|
|
21
|
+
expect(onClick).toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('no emite clicked cuando disabled=true', async () => {
|
|
25
|
+
const onClick = vi.fn();
|
|
26
|
+
@Component({
|
|
27
|
+
standalone: true,
|
|
28
|
+
imports: [DropdownProfileItemComponent],
|
|
29
|
+
template: `<ba-dropdown-profile-item text="Disconnect" [disabled]="true" (clicked)="f()" />`,
|
|
30
|
+
})
|
|
31
|
+
class Host {
|
|
32
|
+
f = onClick;
|
|
33
|
+
}
|
|
34
|
+
await render(Host);
|
|
35
|
+
await fireEvent.click(screen.getByText('Disconnect'));
|
|
36
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renderiza el contenido proyectado de los slots firstIcon/secondIcon', async () => {
|
|
40
|
+
@Component({
|
|
41
|
+
standalone: true,
|
|
42
|
+
imports: [DropdownProfileItemComponent],
|
|
43
|
+
template: `<ba-dropdown-profile-item text="Item">
|
|
44
|
+
<span slot="firstIcon" data-testid="first-icon"></span>
|
|
45
|
+
<span slot="secondIcon" data-testid="second-icon"></span>
|
|
46
|
+
</ba-dropdown-profile-item>`,
|
|
47
|
+
})
|
|
48
|
+
class Host {}
|
|
49
|
+
await render(Host);
|
|
50
|
+
expect(screen.getByTestId('first-icon')).toBeDefined();
|
|
51
|
+
expect(screen.getByTestId('second-icon')).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('aplica el estilo de borde superior cuando isTopBorder=true', async () => {
|
|
55
|
+
const { container } = await render(DropdownProfileItemComponent, {
|
|
56
|
+
inputs: { text: 'Item', isTopBorder: true },
|
|
57
|
+
});
|
|
58
|
+
expect(container.querySelector('li')?.className).toContain('bas-border-t');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Regresión: el host es display:contents para que el <li> (bas-w-full) sea hijo
|
|
62
|
+
// directo del flex de la <ul> y los items queden alineados a la izquierda (no
|
|
63
|
+
// centrados por el `bas-items-center` de la ul encogiendo el host).
|
|
64
|
+
it('el host es transparente al layout (display: contents)', async () => {
|
|
65
|
+
@Component({
|
|
66
|
+
standalone: true,
|
|
67
|
+
imports: [DropdownProfileItemComponent],
|
|
68
|
+
template: `<ba-dropdown-profile-item text="Item" />`,
|
|
69
|
+
})
|
|
70
|
+
class Host {}
|
|
71
|
+
const { container } = await render(Host);
|
|
72
|
+
const host = container.querySelector('ba-dropdown-profile-item') as HTMLElement;
|
|
73
|
+
expect(host.style.display).toBe('contents');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, Type, input, output } from '@angular/core';
|
|
2
|
+
import { NgComponentOutlet } from '@angular/common';
|
|
3
|
+
|
|
4
|
+
// `firstIcon`/`secondIcon` (ReactNode) -> proyección de contenido con
|
|
5
|
+
// `[slot=firstIcon]`/`[slot=secondIcon]` (misma convención `[slot=icon]` ya
|
|
6
|
+
// usada por `ba-btn-base-provider`). `onClick` -> output `clicked`.
|
|
7
|
+
//
|
|
8
|
+
// Además, para el caso de la lista de items adicionales de `DropdownProfile`
|
|
9
|
+
// (iconos dinámicos por-item que no se pueden proyectar estáticamente dentro de
|
|
10
|
+
// un `@for`), se acepta un `firstIcon` opcional como *tipo de componente*
|
|
11
|
+
// (`Type<unknown>`) renderizado vía `NgComponentOutlet` — equivalente Angular
|
|
12
|
+
// idiomático del `VNode | (() => VNode)` del port Vue.
|
|
13
|
+
//
|
|
14
|
+
// La animación framer-motion (`motion.li` + `liItemVariants`) se elimina: el
|
|
15
|
+
// panel completo se anima a nivel de `DropdownProfile` (mismo patrón que
|
|
16
|
+
// `DropdownBase`). `onAnimationComplete` también se elimina aquí; la señal
|
|
17
|
+
// equivalente ("panel abierto") la gestiona `DropdownProfile`.
|
|
18
|
+
@Component({
|
|
19
|
+
selector: 'ba-dropdown-profile-item',
|
|
20
|
+
standalone: true,
|
|
21
|
+
// El host debe ser transparente al layout para que el <li> (bas-w-full) sea
|
|
22
|
+
// hijo directo del flex de la <ul> (como en React, donde <motion.li> se
|
|
23
|
+
// renderiza directamente). Sin esto el host `<ba-dropdown-profile-item>` es un
|
|
24
|
+
// flex-item que, con `bas-items-center` en la ul, se encoge a su contenido y
|
|
25
|
+
// queda CENTRADO en vez de llenar el ancho y alinear el texto a la izquierda.
|
|
26
|
+
host: { style: 'display: contents' },
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
imports: [NgComponentOutlet],
|
|
29
|
+
template: `
|
|
30
|
+
<li
|
|
31
|
+
[id]="id()"
|
|
32
|
+
[class]="
|
|
33
|
+
'bas-box-border bas-max-h-[36px] bas-min-h-[36px] bas-w-full ' +
|
|
34
|
+
(isTopBorder()
|
|
35
|
+
? 'bas-border-t bas-border-[var(--bal-primary-90)] bas-border-solid bas-border-x-0 bas-border-b-0 bas-box-border hover:bas-border-[var(--bal-primary-100)] bal-ba-transition-2'
|
|
36
|
+
: '')
|
|
37
|
+
"
|
|
38
|
+
>
|
|
39
|
+
<div
|
|
40
|
+
[class]="
|
|
41
|
+
'bal-ba-transition-2 bas-box-border bas-flex bas-w-full bas-flex-row bas-items-center bas-justify-between bas-gap-2 bas-px-2 bas-py-1 bas-text-[var(--bal-primary-700)] hover:bas-bg-[var(--bal-primary-10)] hover:bas-text-[var(--bal-primary-900)] ' +
|
|
42
|
+
classNames() + ' ' +
|
|
43
|
+
(isTopBorder() ? 'bas-h-[35px]' : 'bas-h-9') + ' ' +
|
|
44
|
+
(disabled() ? 'bas-cursor-not-allowed bas-opacity-50' : 'bas-cursor-pointer')
|
|
45
|
+
"
|
|
46
|
+
(click)="onItemClicked()"
|
|
47
|
+
>
|
|
48
|
+
<div class="bas-flex bas-h-full bas-w-fit bas-flex-row bas-items-center bas-gap-3">
|
|
49
|
+
@if (firstIcon()) {
|
|
50
|
+
<ng-container [ngComponentOutlet]="firstIcon()" [ngComponentOutletInputs]="{ class: 'bas-h-5 bas-w-5' }" />
|
|
51
|
+
}
|
|
52
|
+
<ng-content select="[slot=firstIcon]" />
|
|
53
|
+
<p class="bas-m-0 bas-text-base">{{ text() }}</p>
|
|
54
|
+
</div>
|
|
55
|
+
<ng-content select="[slot=secondIcon]" />
|
|
56
|
+
</div>
|
|
57
|
+
</li>
|
|
58
|
+
`,
|
|
59
|
+
})
|
|
60
|
+
export class DropdownProfileItemComponent {
|
|
61
|
+
id = input('dd-item-container');
|
|
62
|
+
text = input('');
|
|
63
|
+
isTopBorder = input(false);
|
|
64
|
+
classNames = input('');
|
|
65
|
+
disabled = input(false);
|
|
66
|
+
/** Icono dinámico opcional (tipo de componente) para items en bucle. */
|
|
67
|
+
firstIcon = input<Type<unknown> | null>(null);
|
|
68
|
+
|
|
69
|
+
clicked = output<void>();
|
|
70
|
+
|
|
71
|
+
onItemClicked(): void {
|
|
72
|
+
if (!this.disabled()) this.clicked.emit();
|
|
73
|
+
}
|
|
74
|
+
}
|