@smooai/chat-widget 0.11.0 → 0.12.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/dist/chat-widget.global.js +268 -9
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +262 -236
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +249 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/conversation.ts +112 -2
- package/src/element.ts +178 -5
- package/src/styles.ts +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smooai/chat-widget",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Embeddable AI chat as a framework-light web component — the Aurora Glass design, streaming replies, grounded sources, and per-brand theming. Speaks the smooth-operator WebSocket protocol.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SmooAI",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"access": "public"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@smooai/smooth-operator": "^1.
|
|
61
|
+
"@smooai/smooth-operator": "^1.21.1",
|
|
62
62
|
"libphonenumber-js": "^1.13.7",
|
|
63
63
|
"zustand": "^5.0.14"
|
|
64
64
|
},
|
package/src/conversation.ts
CHANGED
|
@@ -93,6 +93,9 @@ export type ConnectionStatus = 'idle' | 'connecting' | 'ready' | 'error' | 'clos
|
|
|
93
93
|
* Resume with {@link ConversationController.verifyOtp}.
|
|
94
94
|
* - `confirm` — the agent wants to run a state-mutating tool and needs approval.
|
|
95
95
|
* Resume with {@link ConversationController.confirmTool}.
|
|
96
|
+
* - `interaction` — the agent raised a Rich Interaction (structured card, e.g.
|
|
97
|
+
* identity intake). Resume with {@link ConversationController.submitInteraction}
|
|
98
|
+
* or {@link ConversationController.declineInteraction}.
|
|
96
99
|
*/
|
|
97
100
|
export type Interrupt =
|
|
98
101
|
| {
|
|
@@ -106,7 +109,28 @@ export type Interrupt =
|
|
|
106
109
|
error?: string;
|
|
107
110
|
attemptsRemaining?: number;
|
|
108
111
|
}
|
|
109
|
-
| { kind: 'confirm'; toolId?: string; actionDescription?: string }
|
|
112
|
+
| { kind: 'confirm'; toolId?: string; actionDescription?: string }
|
|
113
|
+
| {
|
|
114
|
+
kind: 'interaction';
|
|
115
|
+
/** Server-generated interaction instance id (echoed on submit). */
|
|
116
|
+
interactionId: string;
|
|
117
|
+
/** The Rich Interaction kind (e.g. `identity_intake`) — selects the card. */
|
|
118
|
+
interactionKind: string;
|
|
119
|
+
/** Kind-specific render spec (identity_intake: `{ fields: [...] }`). */
|
|
120
|
+
spec: Record<string, unknown>;
|
|
121
|
+
/** Why the agent raised it (card header copy). */
|
|
122
|
+
reason?: string;
|
|
123
|
+
/** Per-field server-side validation errors (from `interaction_invalid`). */
|
|
124
|
+
errors?: { field: string; message: string }[];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The Rich-Interaction render capabilities this widget declares at session
|
|
129
|
+
* create (`supports`). Must stay aligned with the card registry in
|
|
130
|
+
* `element.ts` (`INTERACTION_CARDS`) — registering a card IS declaring its
|
|
131
|
+
* capability; a test asserts the two match.
|
|
132
|
+
*/
|
|
133
|
+
export const SUPPORTED_INTERACTION_CAPABILITIES: readonly string[] = ['identity_form'];
|
|
110
134
|
|
|
111
135
|
export interface UserInfo {
|
|
112
136
|
name?: string;
|
|
@@ -232,6 +256,9 @@ export class ConversationController {
|
|
|
232
256
|
/** requestId of the in-flight turn — used to resume OTP / tool confirmations. */
|
|
233
257
|
private activeRequestId: string | null = null;
|
|
234
258
|
private interrupt: Interrupt | null = null;
|
|
259
|
+
/** Values from the last interaction submit, merged into the persisted
|
|
260
|
+
* identity (identity_intake only) once the server acks them. */
|
|
261
|
+
private pendingInteractionValues: { kind: string; values: Record<string, unknown> } | null = null;
|
|
235
262
|
private identityRestore: IdentityRestore = { phase: 'idle' };
|
|
236
263
|
/**
|
|
237
264
|
* True once the resume probe (persisted-pointer get_session OR the
|
|
@@ -337,6 +364,41 @@ export class ConversationController {
|
|
|
337
364
|
this.setInterrupt(null);
|
|
338
365
|
}
|
|
339
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Submit a Rich Interaction card to resume the parked turn. The server
|
|
369
|
+
* routes to the kind's validator: invalid values come back as an
|
|
370
|
+
* `interaction_invalid` event (the card re-renders with per-field errors —
|
|
371
|
+
* the turn stays parked); a valid submit is acked and the turn resumes.
|
|
372
|
+
* No-op if not awaiting an interaction.
|
|
373
|
+
*/
|
|
374
|
+
submitInteraction(values: Record<string, unknown>): void {
|
|
375
|
+
if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== 'interaction') return;
|
|
376
|
+
// Stash the values so the ack (immediate_response) can merge accepted
|
|
377
|
+
// identity values into the persisted store.
|
|
378
|
+
this.pendingInteractionValues = { kind: this.interrupt.interactionKind, values };
|
|
379
|
+
this.client.submitInteraction({
|
|
380
|
+
sessionId: this.sessionId,
|
|
381
|
+
requestId: this.activeRequestId,
|
|
382
|
+
interactionId: this.interrupt.interactionId,
|
|
383
|
+
kind: this.interrupt.interactionKind,
|
|
384
|
+
values,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Decline the pending Rich Interaction; the agent continues without it. */
|
|
389
|
+
declineInteraction(): void {
|
|
390
|
+
if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== 'interaction') return;
|
|
391
|
+
this.client.submitInteraction({
|
|
392
|
+
sessionId: this.sessionId,
|
|
393
|
+
requestId: this.activeRequestId,
|
|
394
|
+
interactionId: this.interrupt.interactionId,
|
|
395
|
+
kind: this.interrupt.interactionKind,
|
|
396
|
+
declined: true,
|
|
397
|
+
});
|
|
398
|
+
this.pendingInteractionValues = null;
|
|
399
|
+
this.setInterrupt(null);
|
|
400
|
+
}
|
|
401
|
+
|
|
340
402
|
private nextId(prefix: string): string {
|
|
341
403
|
this.seq += 1;
|
|
342
404
|
return `${prefix}-${this.seq}-${Date.now().toString(36)}`;
|
|
@@ -543,6 +605,10 @@ export class ConversationController {
|
|
|
543
605
|
userName: state.identity.name,
|
|
544
606
|
userEmail: state.identity.email,
|
|
545
607
|
browserFingerprint: this.fingerprint(),
|
|
608
|
+
// Declare the Rich-Interaction cards this widget can render (derived
|
|
609
|
+
// from the card registry), so the server emits `interaction_required`
|
|
610
|
+
// for those kinds instead of the conversational fallback.
|
|
611
|
+
supports: [...SUPPORTED_INTERACTION_CAPABILITIES],
|
|
546
612
|
...(metadata ? { metadata } : {}),
|
|
547
613
|
});
|
|
548
614
|
this.sessionId = session.sessionId;
|
|
@@ -700,7 +766,51 @@ export class ConversationController {
|
|
|
700
766
|
case 'write_confirmation_required':
|
|
701
767
|
this.setInterrupt({ kind: 'confirm', toolId: str(inner.toolId), actionDescription: str(inner.actionDescription) });
|
|
702
768
|
break;
|
|
703
|
-
|
|
769
|
+
case 'interaction_required': {
|
|
770
|
+
const interactionId = str(inner.interactionId);
|
|
771
|
+
const kind = str(inner.kind);
|
|
772
|
+
const spec = inner.spec && typeof inner.spec === 'object' ? (inner.spec as Record<string, unknown>) : {};
|
|
773
|
+
if (!interactionId || !kind) break; // not renderable — ignore
|
|
774
|
+
this.pendingInteractionValues = null;
|
|
775
|
+
this.setInterrupt({
|
|
776
|
+
kind: 'interaction',
|
|
777
|
+
interactionId,
|
|
778
|
+
interactionKind: kind,
|
|
779
|
+
spec,
|
|
780
|
+
reason: str(inner.reason),
|
|
781
|
+
});
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
case 'interaction_invalid':
|
|
785
|
+
if (this.interrupt?.kind === 'interaction' && this.interrupt.interactionId === str(inner.interactionId)) {
|
|
786
|
+
const errors: { field: string; message: string }[] = [];
|
|
787
|
+
if (Array.isArray(inner.errors)) {
|
|
788
|
+
for (const e of inner.errors) {
|
|
789
|
+
if (!e || typeof e !== 'object') continue;
|
|
790
|
+
const o = e as Record<string, unknown>;
|
|
791
|
+
const field = str(o.field);
|
|
792
|
+
if (field) errors.push({ field, message: str(o.message) ?? 'Invalid value' });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
this.pendingInteractionValues = null;
|
|
796
|
+
this.setInterrupt({ ...this.interrupt, errors });
|
|
797
|
+
}
|
|
798
|
+
break;
|
|
799
|
+
case 'immediate_response':
|
|
800
|
+
// Mid-turn immediate_response while an interaction card is showing
|
|
801
|
+
// is the submit/decline ack: the park resolved — clear the card
|
|
802
|
+
// and, for accepted identity values, persist them.
|
|
803
|
+
if (this.interrupt?.kind === 'interaction') {
|
|
804
|
+
const pending = this.pendingInteractionValues;
|
|
805
|
+
if (pending && pending.kind === 'identity_intake') {
|
|
806
|
+
const v = pending.values as { name?: string; email?: string; phone?: string };
|
|
807
|
+
this.store.getState().mergeIdentity({ name: v.name, email: v.email, phone: v.phone });
|
|
808
|
+
}
|
|
809
|
+
this.pendingInteractionValues = null;
|
|
810
|
+
this.setInterrupt(null);
|
|
811
|
+
}
|
|
812
|
+
break;
|
|
813
|
+
default:
|
|
704
814
|
break;
|
|
705
815
|
}
|
|
706
816
|
}
|
package/src/element.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { AsYouType, isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js/min';
|
|
18
18
|
import type { ChatWidgetConfig, ChatWidgetMode, ChatWidgetTheme } from './config.js';
|
|
19
19
|
import { needsUserInfo, resolveConfig } from './config.js';
|
|
20
|
-
import { type ChatMessage, type Citation, type ConnectionStatus, ConversationController, type IdentityRestore, type Interrupt } from './conversation.js';
|
|
20
|
+
import { type ChatMessage, type Citation, type ConnectionStatus, ConversationController, type IdentityRestore, type Interrupt, SUPPORTED_INTERACTION_CAPABILITIES } from './conversation.js';
|
|
21
21
|
import { SMOOTH_ICON_SVG } from './logo.js';
|
|
22
22
|
import { cleanCitationSnippet, escapeHtml, renderMarkdown, safeHttpUrl } from './markdown.js';
|
|
23
23
|
import { buildStyles } from './styles.js';
|
|
@@ -71,10 +71,166 @@ const ICON = {
|
|
|
71
71
|
chev: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="m9 6 6 6-6 6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
72
72
|
/** OTP interrupt — a padlock. */
|
|
73
73
|
lock: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="5" y="10.5" width="14" height="9.5" rx="2.2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" stroke="currentColor" stroke-width="1.7"/></svg>`,
|
|
74
|
+
/** Identity-intake interrupt — a person. */
|
|
75
|
+
user: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="8.2" r="3.4" stroke="currentColor" stroke-width="1.7"/><path d="M5.5 19.5c.8-3.1 3.4-4.8 6.5-4.8s5.7 1.7 6.5 4.8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`,
|
|
74
76
|
/** Tool-confirmation interrupt — a shield. */
|
|
75
77
|
shield: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3 5 6v5c0 4.4 3 7.2 7 8.5 4-1.3 7-4.1 7-8.5V6l-7-3Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m9 11.5 2 2 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
76
78
|
} as const;
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The Rich Interactions **card registry**: interaction kind → overlay card.
|
|
82
|
+
* `interaction_required { kind }` looks its card up here; registering a card IS
|
|
83
|
+
* declaring the widget's render capability for that kind (see
|
|
84
|
+
* `SUPPORTED_INTERACTION_CAPABILITIES` in conversation.ts — a test keeps the
|
|
85
|
+
* two aligned). Adding a kind = one card builder + one entry.
|
|
86
|
+
*
|
|
87
|
+
* The existing OTP and tool-approval overlays are prior instances of this same
|
|
88
|
+
* shape and should retrofit onto this registry later (their wire events predate
|
|
89
|
+
* the pattern).
|
|
90
|
+
*/
|
|
91
|
+
export interface InteractionCardContext {
|
|
92
|
+
/** Submit kind-shaped values (resumes the parked turn; server validates). */
|
|
93
|
+
submit: (values: Record<string, unknown>) => void;
|
|
94
|
+
/** Decline the interaction (the agent continues without it). */
|
|
95
|
+
decline: () => void;
|
|
96
|
+
/** Persisted visitor identity, for pre-filling known fields. */
|
|
97
|
+
prefill: { name?: string; email?: string; phone?: string };
|
|
98
|
+
/** Wire live phone formatting + validity hint onto a form's phone input. */
|
|
99
|
+
wirePhoneField: (form: HTMLFormElement) => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface InteractionCard {
|
|
103
|
+
/** The render capability this card provides (goes into `supports`). */
|
|
104
|
+
capability: string;
|
|
105
|
+
/** Card header title. */
|
|
106
|
+
title: string;
|
|
107
|
+
/** Static, trusted header icon SVG. */
|
|
108
|
+
icon: string;
|
|
109
|
+
/** Build the card body for an `interaction` interrupt. */
|
|
110
|
+
build: (it: Extract<Interrupt, { kind: 'interaction' }>, ctx: InteractionCardContext) => HTMLElement;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The identity_intake card: the fields the agent requested (pre-chat form field
|
|
115
|
+
* pattern — same classes, same phone formatting), per-field server errors, a
|
|
116
|
+
* submit and a decline affordance. Server-supplied text (reason, labels, error
|
|
117
|
+
* messages) is set via `textContent` — never innerHTML.
|
|
118
|
+
*/
|
|
119
|
+
function buildIdentityIntakeCard(it: Extract<Interrupt, { kind: 'interaction' }>, ctx: InteractionCardContext): HTMLElement {
|
|
120
|
+
const form = document.createElement('form');
|
|
121
|
+
form.className = 'int-form';
|
|
122
|
+
form.noValidate = true;
|
|
123
|
+
|
|
124
|
+
if (it.reason) {
|
|
125
|
+
const desc = document.createElement('div');
|
|
126
|
+
desc.className = 'int-desc';
|
|
127
|
+
desc.textContent = it.reason;
|
|
128
|
+
form.appendChild(desc);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const DEFAULTS: Record<string, { label: string; type: string; autocomplete: string }> = {
|
|
132
|
+
name: { label: 'Name', type: 'text', autocomplete: 'name' },
|
|
133
|
+
email: { label: 'Email', type: 'email', autocomplete: 'email' },
|
|
134
|
+
phone: { label: 'Phone', type: 'tel', autocomplete: 'tel' },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Parse the kind's spec defensively: `{ fields: [{ key, required, label? }] }`.
|
|
138
|
+
const rawFields = Array.isArray(it.spec.fields) ? it.spec.fields : [];
|
|
139
|
+
const fields: { key: 'name' | 'email' | 'phone'; required: boolean; label?: string }[] = [];
|
|
140
|
+
for (const f of rawFields) {
|
|
141
|
+
if (!f || typeof f !== 'object') continue;
|
|
142
|
+
const o = f as Record<string, unknown>;
|
|
143
|
+
const key = typeof o.key === 'string' ? o.key : '';
|
|
144
|
+
if (key !== 'name' && key !== 'email' && key !== 'phone') continue;
|
|
145
|
+
fields.push({ key, required: o.required === true, label: typeof o.label === 'string' ? o.label : undefined });
|
|
146
|
+
}
|
|
147
|
+
if (fields.length === 0) fields.push({ key: 'email', required: true });
|
|
148
|
+
|
|
149
|
+
for (const f of fields) {
|
|
150
|
+
const d = DEFAULTS[f.key]!;
|
|
151
|
+
const label = document.createElement('label');
|
|
152
|
+
label.className = 'pc-field';
|
|
153
|
+
const caption = document.createElement('span');
|
|
154
|
+
caption.textContent = f.label ?? d.label;
|
|
155
|
+
const input = document.createElement('input');
|
|
156
|
+
input.name = f.key;
|
|
157
|
+
input.type = d.type;
|
|
158
|
+
input.setAttribute('autocomplete', d.autocomplete);
|
|
159
|
+
input.required = f.required;
|
|
160
|
+
const prefill = ctx.prefill[f.key];
|
|
161
|
+
if (prefill) input.value = prefill;
|
|
162
|
+
label.append(caption, input);
|
|
163
|
+
if (f.key === 'phone') {
|
|
164
|
+
const hint = document.createElement('span');
|
|
165
|
+
hint.className = 'pc-hint';
|
|
166
|
+
hint.setAttribute('aria-live', 'polite');
|
|
167
|
+
label.appendChild(hint);
|
|
168
|
+
}
|
|
169
|
+
// Per-field server-side validation error (interaction_invalid).
|
|
170
|
+
const serverError = it.errors?.find((e) => e.field === f.key);
|
|
171
|
+
if (serverError) {
|
|
172
|
+
label.classList.add('invalid');
|
|
173
|
+
const err = document.createElement('span');
|
|
174
|
+
err.className = 'int-error';
|
|
175
|
+
err.textContent = serverError.message;
|
|
176
|
+
label.appendChild(err);
|
|
177
|
+
}
|
|
178
|
+
form.appendChild(label);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const row = document.createElement('div');
|
|
182
|
+
row.className = 'int-row';
|
|
183
|
+
const decline = document.createElement('button');
|
|
184
|
+
decline.className = 'int-btn';
|
|
185
|
+
decline.type = 'button';
|
|
186
|
+
decline.textContent = 'No thanks';
|
|
187
|
+
decline.addEventListener('click', () => ctx.decline());
|
|
188
|
+
const share = document.createElement('button');
|
|
189
|
+
share.className = 'int-btn primary';
|
|
190
|
+
share.type = 'submit';
|
|
191
|
+
share.textContent = 'Share details';
|
|
192
|
+
row.append(decline, share);
|
|
193
|
+
form.appendChild(row);
|
|
194
|
+
|
|
195
|
+
form.addEventListener('submit', (ev) => {
|
|
196
|
+
ev.preventDefault();
|
|
197
|
+
if (!form.reportValidity()) return;
|
|
198
|
+
const data = new FormData(form);
|
|
199
|
+
const val = (k: string) => ((data.get(k) as string | null)?.trim() || undefined);
|
|
200
|
+
|
|
201
|
+
// Phone: mirror the pre-chat rules — block a required-but-invalid number,
|
|
202
|
+
// prefer canonical E.164 when it parses (the server re-validates anyway).
|
|
203
|
+
const rawPhone = val('phone');
|
|
204
|
+
const phoneInput = form.querySelector('input[name="phone"]') as HTMLInputElement | null;
|
|
205
|
+
if (rawPhone && phoneInput && !isValidPhoneNumber(rawPhone, PHONE_DEFAULT_REGION)) {
|
|
206
|
+
const field = phoneInput.closest('.pc-field');
|
|
207
|
+
field?.classList.add('invalid');
|
|
208
|
+
const hint = field?.querySelector('.pc-hint');
|
|
209
|
+
if (hint) hint.textContent = 'Enter a valid phone number';
|
|
210
|
+
if (phoneInput.hasAttribute('required')) {
|
|
211
|
+
phoneInput.focus();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const phone = rawPhone ? (phoneToE164(rawPhone) ?? rawPhone) : undefined;
|
|
216
|
+
ctx.submit({ name: val('name'), email: val('email'), phone });
|
|
217
|
+
});
|
|
218
|
+
// Same live phone formatting + validity hint as the pre-chat form.
|
|
219
|
+
ctx.wirePhoneField(form);
|
|
220
|
+
queueMicrotask(() => (form.querySelector('input') as HTMLInputElement | null)?.focus());
|
|
221
|
+
return form;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Kind → card. See the registry doc above. */
|
|
225
|
+
export const INTERACTION_CARDS: Record<string, InteractionCard> = {
|
|
226
|
+
identity_intake: {
|
|
227
|
+
capability: 'identity_form',
|
|
228
|
+
title: 'Share your details',
|
|
229
|
+
icon: ICON.user,
|
|
230
|
+
build: buildIdentityIntakeCard,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
78
234
|
// `safeHttpUrl` / `escapeHtml` live in `./markdown.js` (the markdown renderer
|
|
79
235
|
// needs them too); re-exported here for back-compat with existing importers.
|
|
80
236
|
export { escapeHtml, safeHttpUrl } from './markdown.js';
|
|
@@ -480,21 +636,38 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
480
636
|
head.className = 'int-head';
|
|
481
637
|
const ico = document.createElement('span');
|
|
482
638
|
ico.className = 'int-ico';
|
|
483
|
-
|
|
639
|
+
const card_meta = it.kind === 'interaction' ? INTERACTION_CARDS[it.interactionKind] : undefined;
|
|
640
|
+
if (it.kind === 'interaction' && !card_meta) {
|
|
641
|
+
// A kind we have no card for (shouldn't happen — we only declare
|
|
642
|
+
// capabilities for registered cards). Decline so the turn never hangs.
|
|
643
|
+
this.controller?.declineInteraction();
|
|
644
|
+
el.classList.add('hidden');
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
ico.innerHTML = it.kind === 'otp' ? ICON.lock : it.kind === 'interaction' ? (card_meta?.icon ?? ICON.user) : ICON.shield; // static, trusted
|
|
484
648
|
const title = document.createElement('span');
|
|
485
649
|
title.className = 'int-title';
|
|
486
|
-
title.textContent = it.kind === 'otp' ? 'Verification required' : 'Confirm this action';
|
|
650
|
+
title.textContent = it.kind === 'otp' ? 'Verification required' : it.kind === 'interaction' ? (card_meta?.title ?? 'One more thing') : 'Confirm this action';
|
|
487
651
|
head.append(ico, title);
|
|
488
652
|
card.appendChild(head);
|
|
489
653
|
|
|
490
|
-
if (it.actionDescription) {
|
|
654
|
+
if (it.kind !== 'interaction' && it.actionDescription) {
|
|
491
655
|
const desc = document.createElement('div');
|
|
492
656
|
desc.className = 'int-desc';
|
|
493
657
|
desc.textContent = it.actionDescription;
|
|
494
658
|
card.appendChild(desc);
|
|
495
659
|
}
|
|
496
660
|
|
|
497
|
-
if (it.kind === '
|
|
661
|
+
if (it.kind === 'interaction') {
|
|
662
|
+
card.appendChild(
|
|
663
|
+
card_meta!.build(it, {
|
|
664
|
+
submit: (values) => this.controller?.submitInteraction(values),
|
|
665
|
+
decline: () => this.controller?.declineInteraction(),
|
|
666
|
+
prefill: this.controller?.getStore().getState().identity ?? {},
|
|
667
|
+
wirePhoneField: (form) => this.wirePhoneField(form),
|
|
668
|
+
}),
|
|
669
|
+
);
|
|
670
|
+
} else if (it.kind === 'otp') {
|
|
498
671
|
if (it.sent?.maskedDestination) {
|
|
499
672
|
const sent = document.createElement('div');
|
|
500
673
|
sent.className = 'int-sent';
|
package/src/styles.ts
CHANGED
|
@@ -643,6 +643,9 @@ ${
|
|
|
643
643
|
border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
|
|
644
644
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
|
|
645
645
|
}
|
|
646
|
+
.int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
|
|
647
|
+
.int-form .pc-field input { width: 100%; box-sizing: border-box; }
|
|
648
|
+
.int-form .int-error { margin-top: 2px; }
|
|
646
649
|
.int-btn {
|
|
647
650
|
border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
|
|
648
651
|
background: var(--sac-surface-2);
|
|
@@ -662,8 +665,14 @@ ${
|
|
|
662
665
|
color: var(--sac-primary-text);
|
|
663
666
|
box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
|
|
664
667
|
}
|
|
665
|
-
.int-row .int-
|
|
666
|
-
.int-
|
|
668
|
+
.int-row .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
|
|
669
|
+
.int-form .pc-field input { width: 100%; box-sizing: border-box; }
|
|
670
|
+
.int-form .int-error { margin-top: 2px; }
|
|
671
|
+
.int-btn { flex: 1; }
|
|
672
|
+
.int-row .int-input + .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
|
|
673
|
+
.int-form .pc-field input { width: 100%; box-sizing: border-box; }
|
|
674
|
+
.int-form .int-error { margin-top: 2px; }
|
|
675
|
+
.int-btn { flex: 0 0 auto; }
|
|
667
676
|
.int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
|
|
668
677
|
.int-card { position: relative; }
|
|
669
678
|
.int-close {
|