@block-auth.io/angular 0.2.10 → 0.3.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.
@@ -5,6 +5,7 @@ import {
5
5
  effect,
6
6
  input,
7
7
  signal,
8
+ untracked,
8
9
  } from '@angular/core';
9
10
  import {
10
11
  $extension,
@@ -12,6 +13,7 @@ import {
12
13
  openExtensionOnTxn,
13
14
  t as translate,
14
15
  type TextKey,
16
+ type TxnDataRequest,
15
17
  } from '@block-auth.io/core';
16
18
  import { injectBlockAuthClient } from '../../provide-block-auth';
17
19
  import { injectStore } from '../../inject-store';
@@ -22,7 +24,21 @@ import { PermissionsRequiredModalComponent } from './internals/permissions-requi
22
24
  import {
23
25
  SolicitedDataModalComponent,
24
26
  type ParamChange,
27
+ type ShareChange,
25
28
  } from './internals/solicited-data-modal.component';
29
+ import {
30
+ EMPTY_SOLICITED_DATA_FORM,
31
+ getSolicitedAcceptParams,
32
+ getSolicitedFields,
33
+ getSolicitedParams,
34
+ isSolicitedDataComplete,
35
+ saveSolicitedValue,
36
+ setSolicitedShared,
37
+ setSolicitedValue,
38
+ undoSolicitedValue,
39
+ type SolicitedDataForm,
40
+ type SolicitedParam,
41
+ } from '@block-auth.io/core';
26
42
 
27
43
  // Orquestador de txns: lee las solicitudes pendientes de `$ws.requests` y la
28
44
  // config de UI de `$ui.uiConfig` (vía injectStore). Se conserva la navegación
@@ -84,13 +100,15 @@ const TXN_ERROR_TEXT_KEYS: Record<string, TextKey> = {
84
100
  [isCountdown]="false"
85
101
  />
86
102
  } @else if (!isEmpty()) {
87
- <div class="bas-flex bas-flex-col bas-items-center bas-gap-3">
103
+ <div
104
+ class="bal-ba-glass-rise bas-flex bas-flex-col bas-items-center bas-gap-3"
105
+ >
88
106
  <img
89
107
  [src]="
90
108
  preParams().iconUrl ||
91
109
  'https://dashboard.block-auth.io/img/blockauth_logo.png'
92
110
  "
93
- class="bas-h-16"
111
+ class="bal-ba-glass-logo bas-h-16"
94
112
  alt="app logo"
95
113
  />
96
114
  <h2 class="bas-m-0 bas-text-base bas-text-[var(--bal-primary-600)]">
@@ -138,9 +156,12 @@ const TXN_ERROR_TEXT_KEYS: Record<string, TextKey> = {
138
156
  <ba-txn
139
157
  [hasSolicitedData]="hasSolicitedData()"
140
158
  [allRequiredFilled]="allRequiredFilled()"
159
+ [solicitedDataPending]="solicitedDataPending()"
141
160
  [closeTxns]="closeTxns()"
142
161
  [selectedTxnPreParams]="preParams()"
143
- [userParamValues]="userParamValues()"
162
+ [userParamValues]="acceptParams().userParams"
163
+ [grantedParams]="acceptParams().grantedParams"
164
+ [revokedParams]="acceptParams().revokedParams"
144
165
  [addError]="addError"
145
166
  (permissionsRequiredClicked)="showPermissions.set(true)"
146
167
  (solicitedDataClicked)="showSolicitedData.set(true)"
@@ -148,7 +169,7 @@ const TXN_ERROR_TEXT_KEYS: Record<string, TextKey> = {
148
169
  </div>
149
170
  } } @if (showPermissions()) {
150
171
  <div
151
- class="bal-ba-transition-2 bal-ba-backdrop-blur-base bal-ba-backdrop-blur-sm bas-absolute bas-z-30 bas-box-border bas-h-full bas-w-full"
172
+ class="bal-ba-glass-backdrop bal-ba-backdrop-blur-base bal-ba-backdrop-blur-sm bas-absolute bas-z-30 bas-box-border bas-h-full bas-w-full"
152
173
  (click)="showPermissions.set(false)"
153
174
  ></div>
154
175
  <div class="bas-absolute bas-top-44 bas-z-40">
@@ -156,18 +177,27 @@ const TXN_ERROR_TEXT_KEYS: Record<string, TextKey> = {
156
177
  </div>
157
178
  } @if (showSolicitedData()) {
158
179
  <div
159
- class="bal-ba-transition-2 bal-ba-backdrop-blur-base bal-ba-backdrop-blur-sm bas-absolute bas-z-30 bas-box-border bas-h-full bas-w-full"
180
+ class="bal-ba-glass-backdrop bal-ba-backdrop-blur-base bal-ba-backdrop-blur-sm bas-absolute bas-z-30 bas-box-border bas-h-full bas-w-full"
160
181
  (click)="showSolicitedData.set(false)"
161
182
  ></div>
162
- <div class="bas-absolute bas-top-32 bas-z-40">
163
- <ba-solicited-data-modal
164
- [requiredParams]="requiredParams()"
165
- [optionalParams]="optionalParams()"
166
- [userParamValues]="userParamValues()"
167
- (paramChange)="handleParamChange($event)"
168
- (validationChange)="allRequiredFilled.set($event)"
169
- />
170
- </div>
183
+ <ba-solicited-data-modal
184
+ class="bal-ba-glass-modal-slot bas-absolute bas-inset-y-6 bas-z-40"
185
+ [requiredParams]="requiredParams()"
186
+ [optionalParams]="optionalParams()"
187
+ [params]="txnData()?.params"
188
+ [userParamValues]="solicitedForm().values"
189
+ [sharedParams]="solicitedForm().shared"
190
+ [savedParams]="solicitedForm().saved"
191
+ [errorTxt]="acceptTxnError() ? t('requiredDataError') : undefined"
192
+ [closable]="true"
193
+ [acceptable]="true"
194
+ (paramChange)="handleParamChange($event)"
195
+ (shareChange)="handleShareChange($event)"
196
+ (saveParam)="handleSaveParam($event)"
197
+ (undoParam)="handleUndoParam($event)"
198
+ (closed)="showSolicitedData.set(false)"
199
+ (accepted)="showSolicitedData.set(false)"
200
+ />
171
201
  }
172
202
  </div>
173
203
  `,
@@ -187,8 +217,12 @@ export class TxnsOverviewComponent {
187
217
  protected readonly selectedIndex = signal(0);
188
218
  protected readonly showPermissions = signal(false);
189
219
  protected readonly showSolicitedData = signal(false);
190
- protected readonly userParamValues = signal<Record<string, string>>({});
191
- protected readonly allRequiredFilled = signal(false);
220
+ protected readonly solicitedForm = signal<SolicitedDataForm>(
221
+ EMPTY_SOLICITED_DATA_FORM,
222
+ );
223
+ protected readonly txnData = signal<TxnDataRequest | null>(null);
224
+ private readonly isLoadingTxnData = signal(false);
225
+ private readonly txnDataReload = signal(0);
192
226
  protected readonly error = signal<{ value: boolean; messageSlug: string }>({
193
227
  value: false,
194
228
  messageSlug: '',
@@ -198,6 +232,7 @@ export class TxnsOverviewComponent {
198
232
  protected readonly did = computed(() => this.ws().did);
199
233
  protected readonly isSigning = computed(() => this.ui().isSigning);
200
234
  protected readonly isOtpRequired = computed(() => this.ui().isOtpRequired);
235
+ protected readonly acceptTxnError = computed(() => this.ui().acceptTxnError);
201
236
  protected readonly openExtensionOnTxn = openExtensionOnTxn;
202
237
  protected readonly isEmpty = computed(() => this.txns().length === 0);
203
238
  protected readonly errorText = computed(() =>
@@ -244,16 +279,86 @@ export class TxnsOverviewComponent {
244
279
  protected readonly optionalParams = computed(
245
280
  () => this.requiredOptionalParams().optionalParams,
246
281
  );
247
- protected readonly hasSolicitedData = computed(
248
- () => this.requiredParams().length > 0 || this.optionalParams().length > 0,
282
+ private readonly selectedTxnId = computed(
283
+ () => this.selectedTxn()?.['id'] as number | string | undefined,
284
+ );
285
+ private readonly solicitedParams = computed(() =>
286
+ getSolicitedParams({
287
+ params: this.txnData()?.params,
288
+ requiredParams: this.requiredParams(),
289
+ optionalParams: this.optionalParams(),
290
+ }),
291
+ );
292
+ protected readonly hasSolicitedData = computed(() =>
293
+ this.txnData()
294
+ ? this.solicitedParams().length > 0
295
+ : this.requiredParams().length > 0 || this.optionalParams().length > 0,
296
+ );
297
+ private readonly solicitedFields = computed(() =>
298
+ getSolicitedFields(
299
+ this.solicitedParams(),
300
+ this.solicitedForm(),
301
+ !!this.txnData(),
302
+ ),
303
+ );
304
+ private readonly isSolicitedComplete = computed(() =>
305
+ isSolicitedDataComplete(this.solicitedFields()),
306
+ );
307
+ protected readonly allRequiredFilled = computed(
308
+ () =>
309
+ (!this.isLoadingTxnData() || this.solicitedParams().length === 0) &&
310
+ this.isSolicitedComplete(),
311
+ );
312
+ protected readonly solicitedDataPending = computed(
313
+ () =>
314
+ this.hasSolicitedData() &&
315
+ !this.isLoadingTxnData() &&
316
+ !this.isSolicitedComplete(),
317
+ );
318
+ protected readonly acceptParams = computed(() =>
319
+ getSolicitedAcceptParams(this.solicitedFields()),
249
320
  );
250
321
 
251
322
  constructor() {
252
- // ACCEPT is gated on `allRequiredFilled`. With no required params there's
253
- // nothing to fill, so it must default to true.
323
+ effect((onCleanup) => {
324
+ const txnId = this.selectedTxnId();
325
+ this.txnDataReload();
326
+ untracked(() => {
327
+ this.txnData.set(null);
328
+ if (txnId == null) {
329
+ this.isLoadingTxnData.set(false);
330
+ return;
331
+ }
332
+ let active = true;
333
+ this.isLoadingTxnData.set(true);
334
+ Promise.resolve()
335
+ .then(() => this.client.txns.dataRequest(txnId))
336
+ .catch(() => null)
337
+ .then((result) => {
338
+ if (!active) return;
339
+ this.txnData.set(result ?? null);
340
+ this.isLoadingTxnData.set(false);
341
+ });
342
+ onCleanup(() => {
343
+ active = false;
344
+ });
345
+ });
346
+ });
347
+
254
348
  effect(() => {
255
- const params = this.requiredParams();
256
- this.allRequiredFilled.set(params.length === 0);
349
+ if (!this.acceptTxnError()) return;
350
+ untracked(() => {
351
+ this.showSolicitedData.set(true);
352
+ this.txnDataReload.update((n) => n + 1);
353
+ });
354
+ });
355
+
356
+ effect(() => {
357
+ this.selectedTxnId();
358
+ untracked(() => {
359
+ this.solicitedForm.set(EMPTY_SOLICITED_DATA_FORM);
360
+ this.client.state.$ui.setKey('acceptTxnError', null);
361
+ });
257
362
  });
258
363
  }
259
364
 
@@ -263,8 +368,33 @@ export class TxnsOverviewComponent {
263
368
  this.client.state.$ui.setKey('isAcceptingTxn', false);
264
369
  };
265
370
 
371
+ private updateParam(
372
+ slug: string,
373
+ update: (
374
+ form: SolicitedDataForm,
375
+ item: SolicitedParam,
376
+ ) => SolicitedDataForm,
377
+ ): void {
378
+ const item = this.solicitedParams().find((param) => param.slug === slug);
379
+ if (item) this.solicitedForm.update((prev) => update(prev, item));
380
+ }
381
+
266
382
  handleParamChange({ slug, value }: ParamChange): void {
267
- this.userParamValues.update((prev) => ({ ...prev, [slug]: value }));
383
+ this.updateParam(slug, (form, item) =>
384
+ setSolicitedValue(form, item, value),
385
+ );
386
+ }
387
+
388
+ handleShareChange({ slug, shared }: ShareChange): void {
389
+ this.solicitedForm.update((prev) => setSolicitedShared(prev, slug, shared));
390
+ }
391
+
392
+ handleSaveParam(slug: string): void {
393
+ this.solicitedForm.update((prev) => saveSolicitedValue(prev, slug));
394
+ }
395
+
396
+ handleUndoParam(slug: string): void {
397
+ this.updateParam(slug, undoSolicitedValue);
268
398
  }
269
399
 
270
400
  onPrevTxnClicked(): void {
@@ -9,4 +9,17 @@ describe('ArrowIconComponent', () => {
9
9
  const { container } = await render(Host);
10
10
  expect(container.querySelector('.bal-ba-arrow-left')).toBeTruthy();
11
11
  });
12
+
13
+ it('es visible por defecto y se oculta con visible=false', async () => {
14
+ @Component({
15
+ standalone: true,
16
+ imports: [ArrowIconComponent],
17
+ template: `<ba-arrow-icon direction="right" /><ba-arrow-icon direction="left" [visible]="false" />`,
18
+ })
19
+ class Host {}
20
+ const { container } = await render(Host);
21
+ const wrappers = container.querySelectorAll('[aria-hidden="true"]');
22
+ expect(wrappers[0].className).not.toContain('bas-invisible');
23
+ expect(wrappers[1].className).toContain('bas-invisible');
24
+ });
12
25
  });
@@ -7,7 +7,13 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
7
7
  standalone: true,
8
8
  changeDetection: ChangeDetectionStrategy.OnPush,
9
9
  template: `
10
- <div class="bas-flex bas-h-[30px] bas-w-[65px] bas-items-center bas-justify-center">
10
+ <div
11
+ aria-hidden="true"
12
+ [class]="
13
+ 'bal-ba-transition-3 bas-flex bas-h-[30px] bas-w-[65px] bas-items-center bas-justify-center ' +
14
+ (visible() ? 'bas-opacity-100' : 'bas-invisible bas-opacity-0')
15
+ "
16
+ >
11
17
  <div
12
18
  [class]="
13
19
  'bal-ba-arrow bas-relative bas-flex bas-h-[65px] bas-w-[65px] bas-cursor-pointer bas-justify-center ' +
@@ -25,6 +31,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
25
31
  })
26
32
  export class ArrowIconComponent {
27
33
  direction = input<'right' | 'left'>('right');
34
+ visible = input(true);
28
35
 
29
36
  spanClassNames = computed(
30
37
  () =>
package/src/public-api.ts CHANGED
@@ -82,4 +82,5 @@ export { PermissionsRequiredModalComponent } from './lib/components/txns/interna
82
82
  export {
83
83
  SolicitedDataModalComponent,
84
84
  type ParamChange,
85
+ type ShareChange,
85
86
  } from './lib/components/txns/internals/solicited-data-modal.component';