@block-auth.io/angular 0.2.9 → 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.
- package/.turbo/turbo-build.log +40 -37
- package/.turbo/turbo-lint.log +4 -4
- package/.turbo/turbo-test.log +102 -1241
- package/.turbo/turbo-typecheck.log +19 -19
- package/CHANGELOG.md +120 -0
- package/package.json +3 -3
- package/src/lib/components/txns/internals/permissions-required-modal.component.test.ts +9 -0
- package/src/lib/components/txns/internals/permissions-required-modal.component.ts +3 -3
- package/src/lib/components/txns/internals/phone-input.component.test.ts +119 -0
- package/src/lib/components/txns/internals/phone-input.component.ts +301 -0
- package/src/lib/components/txns/internals/solicited-data-modal.component.test.ts +561 -4
- package/src/lib/components/txns/internals/solicited-data-modal.component.ts +324 -129
- package/src/lib/components/txns/internals/txn-type.component.ts +2 -2
- package/src/lib/components/txns/internals/txn.component.test.ts +345 -0
- package/src/lib/components/txns/internals/txn.component.ts +157 -74
- package/src/lib/components/txns/txns-overview.component.test.ts +270 -2
- package/src/lib/components/txns/txns-overview.component.ts +153 -23
- package/src/lib/primitives/arrow-icon.component.test.ts +13 -0
- package/src/lib/primitives/arrow-icon.component.ts +8 -1
- package/src/public-api.ts +1 -0
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ChangeDetectionStrategy,
|
|
3
3
|
Component,
|
|
4
|
+
DestroyRef,
|
|
5
|
+
ElementRef,
|
|
6
|
+
afterNextRender,
|
|
4
7
|
computed,
|
|
5
8
|
effect,
|
|
9
|
+
inject,
|
|
6
10
|
input,
|
|
7
11
|
output,
|
|
8
12
|
signal,
|
|
13
|
+
viewChild,
|
|
9
14
|
} from '@angular/core';
|
|
10
|
-
import { $locale, t as translate, type TextKey } from '@block-auth.io/core';
|
|
15
|
+
import { $locale, t as translate, type TextKey, type TxnDataParam } from '@block-auth.io/core';
|
|
11
16
|
import { injectStore } from '../../../inject-store';
|
|
17
|
+
import { PhoneInputComponent } from './phone-input.component';
|
|
18
|
+
import {
|
|
19
|
+
PARAM_ERROR_KEYS,
|
|
20
|
+
PARAM_INPUT_TYPE,
|
|
21
|
+
PARAM_LABEL_KEYS,
|
|
22
|
+
getSolicitedFields,
|
|
23
|
+
getSolicitedHintKey,
|
|
24
|
+
getSolicitedParams,
|
|
25
|
+
isSolicitedDataComplete,
|
|
26
|
+
isSolicitedHintSuccess,
|
|
27
|
+
type SolicitedField,
|
|
28
|
+
} from '@block-auth.io/core';
|
|
12
29
|
|
|
13
30
|
// `handleParamChange`/`onValidationChange` (callback props en react) -> outputs
|
|
14
31
|
// `paramChange`/`validationChange`: notificaciones puras de "algo cambió en la
|
|
@@ -24,116 +41,239 @@ export interface ParamChange {
|
|
|
24
41
|
value: string;
|
|
25
42
|
}
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'users-firstname': 'text',
|
|
46
|
-
'users-lastname': 'text',
|
|
47
|
-
'users-secondlastname': 'text',
|
|
48
|
-
'users-email': 'email',
|
|
49
|
-
'users-phone': 'tel',
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const PARAM_VALIDATORS: Record<string, (v: string) => boolean> = {
|
|
53
|
-
'users-firstname': (v) => /^[A-Za-zÀ-ÖØ-öø-ÿ\s'-]{1,50}$/.test(v.trim()),
|
|
54
|
-
'users-lastname': (v) => /^[A-Za-zÀ-ÖØ-öø-ÿ\s'-]{1,50}$/.test(v.trim()),
|
|
55
|
-
'users-secondlastname': (v) => /^[A-Za-zÀ-ÖØ-öø-ÿ\s'-]{1,50}$/.test(v.trim()),
|
|
56
|
-
'users-email': (v) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v.trim()),
|
|
57
|
-
'users-phone': (v) => /^\+?[\d\s\-().]{6,20}$/.test(v.trim()),
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const PARAM_ERROR_KEYS: Record<string, TextKey> = {
|
|
61
|
-
'users-firstname': 'onlyLettersAllowed',
|
|
62
|
-
'users-lastname': 'onlyLettersAllowed',
|
|
63
|
-
'users-secondlastname': 'onlyLettersAllowed',
|
|
64
|
-
'users-email': 'enterValidEmail',
|
|
65
|
-
'users-phone': 'enterValidPhone',
|
|
66
|
-
};
|
|
44
|
+
export interface ShareChange {
|
|
45
|
+
slug: string;
|
|
46
|
+
shared: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SolicitedFieldView extends SolicitedField {
|
|
50
|
+
isRequired: boolean;
|
|
51
|
+
isPending: boolean;
|
|
52
|
+
label: string;
|
|
53
|
+
inputId: string;
|
|
54
|
+
inputType: string;
|
|
55
|
+
error: string | null;
|
|
56
|
+
hint: string | null;
|
|
57
|
+
isHintSuccess: boolean;
|
|
58
|
+
showDecision: boolean;
|
|
59
|
+
describedBy: string | null;
|
|
60
|
+
isPhone: boolean;
|
|
61
|
+
}
|
|
67
62
|
|
|
68
63
|
@Component({
|
|
69
64
|
selector: 'ba-solicited-data-modal',
|
|
70
65
|
standalone: true,
|
|
66
|
+
imports: [PhoneInputComponent],
|
|
71
67
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
72
68
|
template: `
|
|
73
69
|
<div
|
|
74
|
-
class="bal-ba-
|
|
70
|
+
class="bal-ba-glass-panel bal-ba-glass-pop bal-ba-glass-modal bas-box-border bas-w-[334px] bas-gap-4 bas-p-4"
|
|
75
71
|
>
|
|
76
|
-
@
|
|
77
|
-
<div class="
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'bas-rounded bas-px-1 bas-py-px bas-font-bold bas-uppercase bas-tracking-wide ' +
|
|
86
|
-
(isRequired(slug)
|
|
87
|
-
? 'bas-bg-amber-100 bas-text-amber-600'
|
|
88
|
-
: 'bas-bg-slate-100 bas-text-slate-400')
|
|
89
|
-
"
|
|
90
|
-
>
|
|
91
|
-
{{
|
|
92
|
-
isRequired(slug)
|
|
93
|
-
? requiredLabelTxtResolved()
|
|
94
|
-
: optionalLabelTxtResolved()
|
|
95
|
-
}}
|
|
96
|
-
</span>
|
|
97
|
-
</div>
|
|
98
|
-
<div class="bas-relative">
|
|
99
|
-
<input
|
|
100
|
-
[type]="inputType(slug)"
|
|
101
|
-
[value]="valueOf(slug)"
|
|
102
|
-
[class]="
|
|
103
|
-
'bas-w-full bas-rounded bas-border bas-bg-white bas-px-2 bas-py-1 bas-text-sm bas-text-slate-700 bas-outline-none bas-transition-colors ' +
|
|
104
|
-
(errorOf(slug)
|
|
105
|
-
? 'bas-border-red-400 bas-pr-7 focus:bas-border-red-500'
|
|
106
|
-
: 'bas-border-slate-300 focus:bas-border-[var(--bal-primary-500)]')
|
|
107
|
-
"
|
|
108
|
-
(input)="onInput(slug, $event)"
|
|
109
|
-
(blur)="onBlur(slug)"
|
|
110
|
-
/>
|
|
111
|
-
@if (errorOf(slug)) {
|
|
72
|
+
@if (closable()) {
|
|
73
|
+
<div class="bal-ba-glass-modal-head">
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
id="solicited-data-close"
|
|
77
|
+
[attr.aria-label]="t('close')"
|
|
78
|
+
class="bal-ba-glass-close"
|
|
79
|
+
(click)="closed.emit()"
|
|
80
|
+
>
|
|
112
81
|
<svg
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
82
|
+
viewBox="0 0 10 10"
|
|
83
|
+
fill="none"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
stroke-width="1.6"
|
|
86
|
+
stroke-linecap="round"
|
|
116
87
|
aria-hidden="true"
|
|
117
88
|
>
|
|
118
|
-
<path
|
|
119
|
-
d="M12 2 1 21h22L12 2Zm0 6c.55 0 1 .45 1 1v5a1 1 0 1 1-2 0V9c0-.55.45-1 1-1Zm0 9.5a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Z"
|
|
120
|
-
/>
|
|
89
|
+
<path d="M2 2l6 6M8 2 2 8" />
|
|
121
90
|
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
}
|
|
94
|
+
<div
|
|
95
|
+
#scroll
|
|
96
|
+
[class]="'bal-ba-glass-scroll' + (isScrollable() ? ' bal-ba-glass-scroll-overflow' : '')"
|
|
97
|
+
>
|
|
98
|
+
<div class="bal-ba-glass-stagger bas-flex bas-flex-col bas-gap-3">
|
|
99
|
+
@for (field of fields(); track field.slug) {
|
|
100
|
+
<div
|
|
101
|
+
[class]="'bal-ba-glass-field' + (field.isPending ? ' bal-ba-glass-field-pending' : '')"
|
|
102
|
+
>
|
|
103
|
+
<div class="bal-ba-glass-field-head">
|
|
104
|
+
<label [for]="field.inputId" class="bal-ba-glass-label">
|
|
105
|
+
<span>{{ field.label }}</span>
|
|
106
|
+
@if (field.isRequired) {
|
|
107
|
+
<span class="bal-ba-glass-label-required">{{ requiredLabelTxtResolved() }}</span>
|
|
108
|
+
} @else {
|
|
109
|
+
<span class="bal-ba-glass-label-optional">{{ optionalLabelTxtResolved() }}</span>
|
|
110
|
+
}
|
|
111
|
+
</label>
|
|
112
|
+
@if (withConsent()) {
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
role="switch"
|
|
116
|
+
[id]="'solicited-data-share-' + field.slug"
|
|
117
|
+
[attr.aria-checked]="field.isShared ? 'true' : 'false'"
|
|
118
|
+
[disabled]="field.isEmpty"
|
|
119
|
+
[attr.aria-label]="t('shareData') + ' ' + field.label"
|
|
120
|
+
class="bal-ba-glass-switch"
|
|
121
|
+
(click)="onShare(field)"
|
|
122
|
+
>
|
|
123
|
+
<span aria-hidden="true">{{ t('shareData') }}</span>
|
|
124
|
+
<span class="bal-ba-glass-switch-track" aria-hidden="true">
|
|
125
|
+
<span class="bal-ba-glass-switch-thumb"></span>
|
|
126
|
+
</span>
|
|
127
|
+
</button>
|
|
128
|
+
}
|
|
129
|
+
</div>
|
|
130
|
+
@if (field.isPhone) {
|
|
131
|
+
<ba-phone-input
|
|
132
|
+
#phoneInput
|
|
133
|
+
[inputId]="field.inputId"
|
|
134
|
+
[value]="field.inputValue"
|
|
135
|
+
[invalid]="!!field.error"
|
|
136
|
+
[describedBy]="field.describedBy"
|
|
137
|
+
(valueChange)="paramChange.emit({ slug: field.slug, value: $event })"
|
|
138
|
+
(blurred)="onBlur(field.slug)"
|
|
139
|
+
>
|
|
140
|
+
@if (field.inputValue) {
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
[attr.aria-label]="t('clearField') + ' ' + field.label"
|
|
144
|
+
class="bal-ba-glass-input-clear"
|
|
145
|
+
(click)="onClear(field.slug, phoneInput)"
|
|
146
|
+
>
|
|
147
|
+
<svg
|
|
148
|
+
viewBox="0 0 10 10"
|
|
149
|
+
fill="none"
|
|
150
|
+
stroke="currentColor"
|
|
151
|
+
stroke-width="1.8"
|
|
152
|
+
stroke-linecap="round"
|
|
153
|
+
aria-hidden="true"
|
|
154
|
+
>
|
|
155
|
+
<path d="M2 2l6 6M8 2 2 8" />
|
|
156
|
+
</svg>
|
|
157
|
+
</button>
|
|
158
|
+
}
|
|
159
|
+
</ba-phone-input>
|
|
160
|
+
} @else {
|
|
161
|
+
<div class="bal-ba-glass-input-wrap">
|
|
162
|
+
<input
|
|
163
|
+
#fieldInput
|
|
164
|
+
[id]="field.inputId"
|
|
165
|
+
[type]="field.inputType"
|
|
166
|
+
[value]="field.inputValue"
|
|
167
|
+
[attr.aria-invalid]="field.error ? 'true' : 'false'"
|
|
168
|
+
[attr.aria-describedby]="field.describedBy"
|
|
169
|
+
class="bal-ba-glass-input"
|
|
170
|
+
(input)="onInput(field.slug, $event)"
|
|
171
|
+
(blur)="onBlur(field.slug)"
|
|
172
|
+
/>
|
|
173
|
+
@if (field.inputValue) {
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
[attr.aria-label]="t('clearField') + ' ' + field.label"
|
|
177
|
+
class="bal-ba-glass-input-clear"
|
|
178
|
+
(click)="onClear(field.slug, fieldInput)"
|
|
179
|
+
>
|
|
180
|
+
<svg
|
|
181
|
+
viewBox="0 0 10 10"
|
|
182
|
+
fill="none"
|
|
183
|
+
stroke="currentColor"
|
|
184
|
+
stroke-width="1.8"
|
|
185
|
+
stroke-linecap="round"
|
|
186
|
+
aria-hidden="true"
|
|
187
|
+
>
|
|
188
|
+
<path d="M2 2l6 6M8 2 2 8" />
|
|
189
|
+
</svg>
|
|
190
|
+
</button>
|
|
191
|
+
}
|
|
192
|
+
</div>
|
|
193
|
+
}
|
|
194
|
+
@if (field.error) {
|
|
195
|
+
<p [id]="field.inputId + '-error'" class="bal-ba-glass-error">
|
|
196
|
+
{{ field.error }}
|
|
197
|
+
</p>
|
|
198
|
+
} @if (field.showDecision) {
|
|
199
|
+
<div class="bal-ba-glass-decision">
|
|
200
|
+
@if (field.hint) {
|
|
201
|
+
<p
|
|
202
|
+
[id]="field.inputId + '-hint'"
|
|
203
|
+
[class]="
|
|
204
|
+
'bal-ba-glass-hint' +
|
|
205
|
+
(field.isHintSuccess ? ' bal-ba-glass-hint-success' : '')
|
|
206
|
+
"
|
|
207
|
+
>
|
|
208
|
+
{{ field.hint }}
|
|
209
|
+
</p>
|
|
210
|
+
}
|
|
211
|
+
<div class="bal-ba-glass-decision-actions">
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
[id]="'solicited-data-undo-' + field.slug"
|
|
215
|
+
class="bal-ba-glass-chip-btn"
|
|
216
|
+
(click)="undoParam.emit(field.slug)"
|
|
217
|
+
>
|
|
218
|
+
{{ t('undoChange') }}
|
|
219
|
+
</button>
|
|
220
|
+
@if (!field.isSaved) {
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
[id]="'solicited-data-save-' + field.slug"
|
|
224
|
+
[disabled]="!field.isValid && !field.isEmpty"
|
|
225
|
+
class="bal-ba-glass-chip-btn bal-ba-glass-chip-btn-primary"
|
|
226
|
+
(click)="saveParam.emit(field.slug)"
|
|
227
|
+
>
|
|
228
|
+
{{ t('saveData') }}
|
|
229
|
+
</button>
|
|
230
|
+
}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
} @else { @if (field.hint) {
|
|
234
|
+
<p
|
|
235
|
+
[id]="field.inputId + '-hint'"
|
|
236
|
+
[class]="
|
|
237
|
+
'bal-ba-glass-hint' +
|
|
238
|
+
(field.isHintSuccess ? ' bal-ba-glass-hint-success' : '')
|
|
239
|
+
"
|
|
240
|
+
>
|
|
241
|
+
{{ field.hint }}
|
|
242
|
+
</p>
|
|
243
|
+
} }
|
|
244
|
+
</div>
|
|
122
245
|
}
|
|
123
246
|
</div>
|
|
124
|
-
@if (errorOf(slug)) {
|
|
125
|
-
<p
|
|
126
|
-
style="font-size: 10px"
|
|
127
|
-
class="bas-m-0 bas-px-2 bas-font-medium bas-text-red-500"
|
|
128
|
-
>
|
|
129
|
-
{{ errorOf(slug) }}
|
|
130
|
-
</p>
|
|
131
|
-
}
|
|
132
247
|
</div>
|
|
133
|
-
|
|
134
|
-
<p
|
|
135
|
-
|
|
248
|
+
@if (!allRequiredFilled()) {
|
|
249
|
+
<p id="solicited-data-notice" class="bal-ba-glass-notice bal-ba-glass-rise">
|
|
250
|
+
<svg
|
|
251
|
+
viewBox="0 0 16 16"
|
|
252
|
+
fill="none"
|
|
253
|
+
stroke="currentColor"
|
|
254
|
+
stroke-width="1.5"
|
|
255
|
+
stroke-linecap="round"
|
|
256
|
+
aria-hidden="true"
|
|
257
|
+
class="bal-ba-glass-notice-icon"
|
|
258
|
+
>
|
|
259
|
+
<circle cx="8" cy="8" r="6.25" />
|
|
260
|
+
<path d="M8 7.25v3.5M8 5.1v.01" />
|
|
261
|
+
</svg>
|
|
262
|
+
<span>{{ fillRequiredTxtResolved() }}</span>
|
|
263
|
+
</p>
|
|
264
|
+
} @if (errorTxt()) {
|
|
265
|
+
<p role="alert" class="bal-ba-glass-notice bal-ba-glass-notice-error bal-ba-glass-rise">
|
|
266
|
+
{{ errorTxt() }}
|
|
136
267
|
</p>
|
|
268
|
+
} @if (acceptable()) {
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
id="solicited-data-accept"
|
|
272
|
+
class="bal-ba-glass-action bal-ba-glass-action-primary"
|
|
273
|
+
(click)="accepted.emit()"
|
|
274
|
+
>
|
|
275
|
+
{{ acceptTxt() ?? t('acceptData') }}
|
|
276
|
+
</button>
|
|
137
277
|
}
|
|
138
278
|
</div>
|
|
139
279
|
`,
|
|
@@ -141,76 +281,131 @@ const PARAM_ERROR_KEYS: Record<string, TextKey> = {
|
|
|
141
281
|
export class SolicitedDataModalComponent {
|
|
142
282
|
requiredParams = input<string[]>([]);
|
|
143
283
|
optionalParams = input<string[]>([]);
|
|
284
|
+
params = input<TxnDataParam[] | null>();
|
|
144
285
|
userParamValues = input<Record<string, string>>({});
|
|
286
|
+
sharedParams = input<Record<string, boolean>>({});
|
|
287
|
+
savedParams = input<Record<string, string>>({});
|
|
145
288
|
requiredLabelTxt = input<string>();
|
|
146
289
|
optionalLabelTxt = input<string>();
|
|
147
290
|
fillRequiredTxt = input<string>();
|
|
291
|
+
errorTxt = input<string>();
|
|
292
|
+
closable = input(false);
|
|
293
|
+
acceptable = input(false);
|
|
294
|
+
acceptTxt = input<string>();
|
|
148
295
|
|
|
149
296
|
paramChange = output<ParamChange>();
|
|
297
|
+
shareChange = output<ShareChange>();
|
|
298
|
+
saveParam = output<string>();
|
|
299
|
+
undoParam = output<string>();
|
|
150
300
|
validationChange = output<boolean>();
|
|
301
|
+
closed = output<void>();
|
|
302
|
+
accepted = output<void>();
|
|
151
303
|
|
|
152
304
|
private readonly locale = injectStore($locale);
|
|
153
305
|
protected t = (key: TextKey): string => translate(key, this.locale());
|
|
154
306
|
|
|
155
307
|
protected readonly requiredLabelTxtResolved = computed(
|
|
156
|
-
() => this.requiredLabelTxt() ?? this.t('required')
|
|
308
|
+
() => this.requiredLabelTxt() ?? this.t('required')
|
|
157
309
|
);
|
|
158
310
|
protected readonly optionalLabelTxtResolved = computed(
|
|
159
|
-
() => this.optionalLabelTxt() ?? this.t('optional')
|
|
311
|
+
() => this.optionalLabelTxt() ?? this.t('optional')
|
|
160
312
|
);
|
|
313
|
+
protected readonly withConsent = computed(() => !!this.params());
|
|
161
314
|
protected readonly fillRequiredTxtResolved = computed(
|
|
162
|
-
() =>
|
|
315
|
+
() =>
|
|
316
|
+
this.fillRequiredTxt() ??
|
|
317
|
+
this.t(this.withConsent() ? 'completeRequiredData' : 'fillRequiredFields')
|
|
163
318
|
);
|
|
164
319
|
|
|
165
320
|
private readonly touched = signal<Record<string, boolean>>({});
|
|
321
|
+
private readonly scroll = viewChild.required<ElementRef<HTMLDivElement>>('scroll');
|
|
322
|
+
protected readonly isScrollable = signal(false);
|
|
323
|
+
|
|
324
|
+
private readonly solicitedFields = computed(() =>
|
|
325
|
+
getSolicitedFields(
|
|
326
|
+
getSolicitedParams({
|
|
327
|
+
params: this.params(),
|
|
328
|
+
requiredParams: this.requiredParams(),
|
|
329
|
+
optionalParams: this.optionalParams(),
|
|
330
|
+
}),
|
|
331
|
+
{
|
|
332
|
+
values: this.userParamValues(),
|
|
333
|
+
shared: this.sharedParams(),
|
|
334
|
+
saved: this.savedParams(),
|
|
335
|
+
},
|
|
336
|
+
this.withConsent()
|
|
337
|
+
)
|
|
338
|
+
);
|
|
166
339
|
|
|
167
340
|
protected readonly allRequiredFilled = computed(() =>
|
|
168
|
-
this.
|
|
169
|
-
if (slug === 'modules-kyc') return true;
|
|
170
|
-
const value = this.userParamValues()[slug]?.trim();
|
|
171
|
-
if (!value) return false;
|
|
172
|
-
const validate = PARAM_VALIDATORS[slug];
|
|
173
|
-
return !validate || validate(value);
|
|
174
|
-
}),
|
|
341
|
+
isSolicitedDataComplete(this.solicitedFields())
|
|
175
342
|
);
|
|
176
343
|
|
|
177
|
-
protected readonly
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
344
|
+
protected readonly fields = computed<SolicitedFieldView[]>(() =>
|
|
345
|
+
this.solicitedFields().map((field) => {
|
|
346
|
+
const labelKey = PARAM_LABEL_KEYS[field.slug];
|
|
347
|
+
const label = labelKey ? this.t(labelKey) : field.slug;
|
|
348
|
+
const inputId = `solicited-data-${field.slug}`;
|
|
349
|
+
const errorKey = PARAM_ERROR_KEYS[field.slug];
|
|
350
|
+
const error =
|
|
351
|
+
this.touched()[field.slug] &&
|
|
352
|
+
field.inputValue.trim() &&
|
|
353
|
+
!field.isValid &&
|
|
354
|
+
errorKey
|
|
355
|
+
? this.t(errorKey)
|
|
356
|
+
: null;
|
|
357
|
+
const showDecision = this.withConsent() && field.isChanged;
|
|
358
|
+
const hintKey = getSolicitedHintKey(field, this.withConsent());
|
|
359
|
+
const hint = hintKey ? this.t(hintKey) : null;
|
|
360
|
+
return {
|
|
361
|
+
...field,
|
|
362
|
+
isRequired: field.mode === 'required',
|
|
363
|
+
isPending: field.mode === 'required' && !field.isComplete,
|
|
364
|
+
label,
|
|
365
|
+
inputId,
|
|
366
|
+
inputType: PARAM_INPUT_TYPE[field.slug] || 'text',
|
|
367
|
+
isPhone: PARAM_INPUT_TYPE[field.slug] === 'tel',
|
|
368
|
+
error,
|
|
369
|
+
hint,
|
|
370
|
+
isHintSuccess: isSolicitedHintSuccess(field),
|
|
371
|
+
showDecision,
|
|
372
|
+
describedBy: error ? `${inputId}-error` : hint ? `${inputId}-hint` : null,
|
|
373
|
+
};
|
|
374
|
+
})
|
|
184
375
|
);
|
|
185
376
|
|
|
186
377
|
constructor() {
|
|
187
378
|
effect(() => {
|
|
188
379
|
this.validationChange.emit(this.allRequiredFilled());
|
|
189
380
|
});
|
|
190
|
-
}
|
|
191
381
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (validate && !validate(value)) return this.t(PARAM_ERROR_KEYS[slug]);
|
|
204
|
-
return null;
|
|
382
|
+
const destroyRef = inject(DestroyRef);
|
|
383
|
+
afterNextRender(() => {
|
|
384
|
+
const el = this.scroll().nativeElement;
|
|
385
|
+
const update = () => this.isScrollable.set(el.scrollHeight > el.clientHeight + 1);
|
|
386
|
+
update();
|
|
387
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
388
|
+
const observer = new ResizeObserver(update);
|
|
389
|
+
observer.observe(el);
|
|
390
|
+
if (el.firstElementChild) observer.observe(el.firstElementChild);
|
|
391
|
+
destroyRef.onDestroy(() => observer.disconnect());
|
|
392
|
+
});
|
|
205
393
|
}
|
|
206
394
|
|
|
207
|
-
errorOf = (slug: string): string | null =>
|
|
208
|
-
this.touched()[slug] ? this.getError(slug, this.valueOf(slug)) : null;
|
|
209
|
-
|
|
210
395
|
onBlur(slug: string): void {
|
|
211
396
|
this.touched.update((prev) => ({ ...prev, [slug]: true }));
|
|
212
397
|
}
|
|
213
398
|
|
|
399
|
+
onShare(field: SolicitedField): void {
|
|
400
|
+
if (field.isEmpty) return;
|
|
401
|
+
this.shareChange.emit({ slug: field.slug, shared: !field.isShared });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
onClear(slug: string, inputEl: { focus: () => void }): void {
|
|
405
|
+
this.paramChange.emit({ slug, value: '' });
|
|
406
|
+
inputEl.focus();
|
|
407
|
+
}
|
|
408
|
+
|
|
214
409
|
onInput(slug: string, event: Event): void {
|
|
215
410
|
this.paramChange.emit({
|
|
216
411
|
slug,
|
|
@@ -25,11 +25,11 @@ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core
|
|
|
25
25
|
>
|
|
26
26
|
}
|
|
27
27
|
<div class="bas-flex bas-w-full bas-items-center bas-justify-center bas-gap-3">
|
|
28
|
-
<span class="bal-ba-
|
|
28
|
+
<span class="bal-ba-glass-divider bas-h-px bas-flex-1"></span>
|
|
29
29
|
<p class="bal-ba-transition-2 bas-m-0 bas-cursor-default bas-text-center bas-text-lg bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]">
|
|
30
30
|
{{ txnType() }}
|
|
31
31
|
</p>
|
|
32
|
-
<span class="bal-ba-
|
|
32
|
+
<span class="bal-ba-glass-divider bal-ba-glass-divider-end bas-h-px bas-flex-1"></span>
|
|
33
33
|
</div>
|
|
34
34
|
@if (txnsLength() > 1) {
|
|
35
35
|
<span
|