uhc-portal-components-react 0.2.26 → 0.2.29
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/CHANGELOG.md +22 -1
- package/README.md +4 -0
- package/dist/BrowserRealtimeCallClient.d.ts +80 -0
- package/dist/BrowserRealtimeCallClient.d.ts.map +1 -0
- package/dist/BrowserRealtimeCallClient.js +104 -0
- package/dist/BrowserRealtimeCallPanel.d.ts +32 -0
- package/dist/BrowserRealtimeCallPanel.d.ts.map +1 -0
- package/dist/BrowserRealtimeCallPanel.js +42 -0
- package/dist/ExistingProfessionalProfilePanel.d.ts +63 -0
- package/dist/ExistingProfessionalProfilePanel.d.ts.map +1 -0
- package/dist/ExistingProfessionalProfilePanel.js +132 -0
- package/dist/PortalHeader.d.ts +3 -2
- package/dist/PortalHeader.d.ts.map +1 -1
- package/dist/PortalHeader.js +4 -1
- package/dist/ProfessionalProfileActivationPanel.d.ts +29 -0
- package/dist/ProfessionalProfileActivationPanel.d.ts.map +1 -0
- package/dist/ProfessionalProfileActivationPanel.js +25 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/styles.css +8 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.2.29 - 2026-10-22
|
|
4
|
+
|
|
5
|
+
- Add one product-neutral WebRTC engine and call screen for audio/video start,
|
|
6
|
+
acceptance, local/remote media binding and hang-up. Host BFF adapters remain
|
|
7
|
+
responsible for subject/service authorization, ICE issuance, signalling and
|
|
8
|
+
durable call state.
|
|
9
|
+
- Keep shared light/dark theme state synchronized with the host document so
|
|
10
|
+
independently deployed portal shells render the same selected theme.
|
|
11
|
+
|
|
12
|
+
## 0.2.28 - 2026-10-22
|
|
13
|
+
|
|
14
|
+
- Add one reusable existing-professional screen with two explicit alternatives:
|
|
15
|
+
scan or upload the signed professional-credential QR, or enter one DID/URN
|
|
16
|
+
plus the organization sector manually.
|
|
17
|
+
- Keep the employee invitation, verified professional credential and device
|
|
18
|
+
registration as separate artifacts and flows.
|
|
19
|
+
|
|
20
|
+
## 0.2.27 - 2026-10-22
|
|
21
|
+
|
|
22
|
+
- Add one brand-neutral employee-invitation activation screen and local PIN.
|
|
23
|
+
Product BFFs resolve organization, sector, role, licence and technical
|
|
24
|
+
identities without exposing provider internals to the browser.
|
|
4
25
|
|
|
5
26
|
## 0.2.26 - 2026-10-22
|
|
6
27
|
|
package/README.md
CHANGED
|
@@ -120,6 +120,10 @@ The package deliberately exposes modules instead of a cloned application:
|
|
|
120
120
|
- `InvitationAcceptancePanel`: controlled invitation-code UI for a related
|
|
121
121
|
individual member; the host refreshes the authorized-subject list after its
|
|
122
122
|
BFF accepts the invitation.
|
|
123
|
+
- `ExistingProfessionalProfilePanel`: existing employees scan or upload a
|
|
124
|
+
signed professional-credential QR, or use the explicit DID/URN plus sector
|
|
125
|
+
fallback. The host verifies the evidence before DCR; this screen never
|
|
126
|
+
accepts an employee invitation.
|
|
123
127
|
- `SubjectAccessRequestPanel`: controlled provider-enabled QR, numeric-code,
|
|
124
128
|
`did:web`, microchip, tattoo, email or telephone locator. Resolution and
|
|
125
129
|
returned identifier evidence never grant subject access.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type RealtimeCallMedia = 'audio' | 'video';
|
|
2
|
+
export type RealtimeCallSignal = Readonly<{
|
|
3
|
+
sequence: number;
|
|
4
|
+
type: 'offer' | 'answer' | 'ice-candidate';
|
|
5
|
+
payload: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type BrowserRealtimeCallSession = Readonly<{
|
|
8
|
+
callId: string;
|
|
9
|
+
localStream: MediaStream;
|
|
10
|
+
remoteStream: MediaStream;
|
|
11
|
+
end(): Promise<void>;
|
|
12
|
+
}>;
|
|
13
|
+
export type BrowserRealtimeCallTransport<Conversation> = Readonly<{
|
|
14
|
+
getIceServers(conversation: Conversation): Promise<readonly RTCIceServer[]>;
|
|
15
|
+
startCall(input: Readonly<{
|
|
16
|
+
conversation: Conversation;
|
|
17
|
+
media: readonly RealtimeCallMedia[];
|
|
18
|
+
}>): Promise<Readonly<{
|
|
19
|
+
callId: string;
|
|
20
|
+
}>>;
|
|
21
|
+
postSignal(input: Readonly<{
|
|
22
|
+
conversation: Conversation;
|
|
23
|
+
callId: string;
|
|
24
|
+
signal: RealtimeCallSignal;
|
|
25
|
+
}>): Promise<void>;
|
|
26
|
+
listSignals(input: Readonly<{
|
|
27
|
+
conversation: Conversation;
|
|
28
|
+
callId: string;
|
|
29
|
+
afterSequence: number;
|
|
30
|
+
}>): Promise<readonly RealtimeCallSignal[]>;
|
|
31
|
+
endCall(input: Readonly<{
|
|
32
|
+
conversation: Conversation;
|
|
33
|
+
callId: string;
|
|
34
|
+
}>): Promise<void>;
|
|
35
|
+
}>;
|
|
36
|
+
export type BrowserRealtimeCallDependencies<Conversation> = Readonly<{
|
|
37
|
+
transport: BrowserRealtimeCallTransport<Conversation>;
|
|
38
|
+
getUserMedia?: (constraints: MediaStreamConstraints) => Promise<MediaStream>;
|
|
39
|
+
createPeerConnection?: (configuration: RTCConfiguration) => RTCPeerConnection;
|
|
40
|
+
createMediaStream?: () => MediaStream;
|
|
41
|
+
schedulePoll?: (callback: () => void, delayMs: number) => unknown;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Product-neutral WebRTC browser engine. Product BFF adapters own call
|
|
45
|
+
* authorization, ICE issuance and opaque signaling persistence; this engine
|
|
46
|
+
* never receives a private destination or sends media through the BFF.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createBrowserRealtimeCallClient<Conversation>(dependencies: BrowserRealtimeCallDependencies<Conversation>): {
|
|
49
|
+
start(input: Readonly<{
|
|
50
|
+
conversation: Conversation;
|
|
51
|
+
media: readonly RealtimeCallMedia[];
|
|
52
|
+
}>): Promise<Readonly<{
|
|
53
|
+
callId: string;
|
|
54
|
+
localStream: MediaStream;
|
|
55
|
+
remoteStream: MediaStream;
|
|
56
|
+
end(): Promise<void>;
|
|
57
|
+
}>>;
|
|
58
|
+
accept(input: Readonly<{
|
|
59
|
+
conversation: Conversation;
|
|
60
|
+
callId: string;
|
|
61
|
+
media: readonly RealtimeCallMedia[];
|
|
62
|
+
}>): Promise<Readonly<{
|
|
63
|
+
callId: string;
|
|
64
|
+
localStream: MediaStream;
|
|
65
|
+
remoteStream: MediaStream;
|
|
66
|
+
end(): Promise<void>;
|
|
67
|
+
}>>;
|
|
68
|
+
join(input: Readonly<{
|
|
69
|
+
conversation: Conversation;
|
|
70
|
+
callId: string;
|
|
71
|
+
media: readonly RealtimeCallMedia[];
|
|
72
|
+
initiator: boolean;
|
|
73
|
+
}>): Promise<Readonly<{
|
|
74
|
+
callId: string;
|
|
75
|
+
localStream: MediaStream;
|
|
76
|
+
remoteStream: MediaStream;
|
|
77
|
+
end(): Promise<void>;
|
|
78
|
+
}>>;
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=BrowserRealtimeCallClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrowserRealtimeCallClient.d.ts","sourceRoot":"","sources":["../src/BrowserRealtimeCallClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product-neutral WebRTC browser engine. Product BFF adapters own call
|
|
3
|
+
* authorization, ICE issuance and opaque signaling persistence; this engine
|
|
4
|
+
* never receives a private destination or sends media through the BFF.
|
|
5
|
+
*/
|
|
6
|
+
export function createBrowserRealtimeCallClient(dependencies) {
|
|
7
|
+
const getUserMedia = dependencies.getUserMedia ?? (constraints => navigator.mediaDevices.getUserMedia(constraints));
|
|
8
|
+
const createPeerConnection = dependencies.createPeerConnection ?? (configuration => new RTCPeerConnection(configuration));
|
|
9
|
+
const createMediaStream = dependencies.createMediaStream ?? (() => new MediaStream());
|
|
10
|
+
const schedulePoll = dependencies.schedulePoll ?? ((callback, delayMs) => globalThis.setTimeout(callback, delayMs));
|
|
11
|
+
async function connect(input) {
|
|
12
|
+
const iceServers = await dependencies.transport.getIceServers(input.conversation);
|
|
13
|
+
const peer = createPeerConnection({ iceServers: [...iceServers] });
|
|
14
|
+
const localStream = await getUserMedia({ audio: input.media.includes('audio'), video: input.media.includes('video') });
|
|
15
|
+
const remoteStream = createMediaStream();
|
|
16
|
+
for (const track of localStream.getTracks())
|
|
17
|
+
peer.addTrack(track, localStream);
|
|
18
|
+
let nextSequence = 1;
|
|
19
|
+
let lastReadSequence = 0;
|
|
20
|
+
let ended = false;
|
|
21
|
+
peer.ontrack = event => {
|
|
22
|
+
for (const track of event.streams[0]?.getTracks() ?? [event.track])
|
|
23
|
+
remoteStream.addTrack(track);
|
|
24
|
+
};
|
|
25
|
+
peer.onicecandidate = event => {
|
|
26
|
+
if (!event.candidate || ended)
|
|
27
|
+
return;
|
|
28
|
+
void dependencies.transport.postSignal({
|
|
29
|
+
conversation: input.conversation,
|
|
30
|
+
callId: input.callId,
|
|
31
|
+
signal: { sequence: nextSequence++, type: 'ice-candidate', payload: JSON.stringify(event.candidate.toJSON()) },
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
async function applySignals() {
|
|
35
|
+
const signals = await dependencies.transport.listSignals({
|
|
36
|
+
conversation: input.conversation, callId: input.callId, afterSequence: lastReadSequence,
|
|
37
|
+
});
|
|
38
|
+
for (const signal of signals) {
|
|
39
|
+
lastReadSequence = Math.max(lastReadSequence, signal.sequence);
|
|
40
|
+
nextSequence = Math.max(nextSequence, signal.sequence + 1);
|
|
41
|
+
if (signal.type === 'ice-candidate')
|
|
42
|
+
await peer.addIceCandidate(JSON.parse(signal.payload));
|
|
43
|
+
else if (signal.type === 'offer' && !input.initiator) {
|
|
44
|
+
await peer.setRemoteDescription(JSON.parse(signal.payload));
|
|
45
|
+
const answer = await peer.createAnswer();
|
|
46
|
+
await peer.setLocalDescription(answer);
|
|
47
|
+
await dependencies.transport.postSignal({
|
|
48
|
+
conversation: input.conversation, callId: input.callId,
|
|
49
|
+
signal: { sequence: nextSequence++, type: 'answer', payload: JSON.stringify(answer) },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (signal.type === 'answer' && input.initiator) {
|
|
53
|
+
await peer.setRemoteDescription(JSON.parse(signal.payload));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (input.initiator) {
|
|
58
|
+
const offer = await peer.createOffer();
|
|
59
|
+
await peer.setLocalDescription(offer);
|
|
60
|
+
await dependencies.transport.postSignal({
|
|
61
|
+
conversation: input.conversation, callId: input.callId,
|
|
62
|
+
signal: { sequence: nextSequence++, type: 'offer', payload: JSON.stringify(offer) },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else
|
|
66
|
+
await applySignals();
|
|
67
|
+
const poll = async () => {
|
|
68
|
+
if (ended)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
await applySignals();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (!ended)
|
|
75
|
+
schedulePoll(() => void poll(), 750);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
schedulePoll(() => void poll(), 250);
|
|
79
|
+
return {
|
|
80
|
+
callId: input.callId,
|
|
81
|
+
localStream,
|
|
82
|
+
remoteStream,
|
|
83
|
+
async end() {
|
|
84
|
+
ended = true;
|
|
85
|
+
for (const track of localStream.getTracks())
|
|
86
|
+
track.stop();
|
|
87
|
+
peer.close();
|
|
88
|
+
await dependencies.transport.endCall({ conversation: input.conversation, callId: input.callId });
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
async start(input) {
|
|
94
|
+
const { callId } = await dependencies.transport.startCall(input);
|
|
95
|
+
return connect({ ...input, callId, initiator: true });
|
|
96
|
+
},
|
|
97
|
+
accept(input) {
|
|
98
|
+
return connect({ ...input, initiator: false });
|
|
99
|
+
},
|
|
100
|
+
join(input) {
|
|
101
|
+
return connect(input);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BrowserRealtimeCallSession, RealtimeCallMedia } from './BrowserRealtimeCallClient.js';
|
|
2
|
+
export type BrowserRealtimeCallPanelClient<Conversation> = Readonly<{
|
|
3
|
+
start(input: Readonly<{
|
|
4
|
+
conversation: Conversation;
|
|
5
|
+
media: readonly RealtimeCallMedia[];
|
|
6
|
+
}>): Promise<BrowserRealtimeCallSession>;
|
|
7
|
+
accept(input: Readonly<{
|
|
8
|
+
conversation: Conversation;
|
|
9
|
+
callId: string;
|
|
10
|
+
media: readonly RealtimeCallMedia[];
|
|
11
|
+
}>): Promise<BrowserRealtimeCallSession>;
|
|
12
|
+
}>;
|
|
13
|
+
export type BrowserRealtimeCallPanelLabels = Readonly<{
|
|
14
|
+
title: string;
|
|
15
|
+
start: string;
|
|
16
|
+
accept: string;
|
|
17
|
+
end: string;
|
|
18
|
+
connecting?: string;
|
|
19
|
+
connected?: string;
|
|
20
|
+
ended?: string;
|
|
21
|
+
failed?: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** Product-neutral, two-sided browser call screen; authority remains in the supplied BFF client. */
|
|
24
|
+
export declare function BrowserRealtimeCallPanel<Conversation>({ conversation, client, incomingCallId, labels, media, onFailure, }: Readonly<{
|
|
25
|
+
conversation: Conversation;
|
|
26
|
+
client: BrowserRealtimeCallPanelClient<Conversation>;
|
|
27
|
+
incomingCallId?: string;
|
|
28
|
+
labels: BrowserRealtimeCallPanelLabels;
|
|
29
|
+
media?: readonly RealtimeCallMedia[];
|
|
30
|
+
onFailure?: (error: unknown) => void;
|
|
31
|
+
}>): import("react").JSX.Element;
|
|
32
|
+
//# sourceMappingURL=BrowserRealtimeCallPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrowserRealtimeCallPanel.d.ts","sourceRoot":"","sources":["../src/BrowserRealtimeCallPanel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAEnG,MAAM,MAAM,8BAA8B,CAAC,YAAY,IAAI,QAAQ,CAAC;IAClE,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,YAAY,EAAE,YAAY,CAAC;QAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;IAChI,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,YAAY,EAAE,YAAY,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;CAClJ,CAAC,CAAA;AAEF,MAAM,MAAM,8BAA8B,GAAG,QAAQ,CAAC;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAC,CAAA;AAEF,oGAAoG;AACpG,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,EACrD,YAAY,EACZ,MAAM,EACN,cAAc,EACd,MAAM,EACN,KAA0B,EAC1B,SAAS,GACV,EAAE,QAAQ,CAAC;IACV,YAAY,EAAE,YAAY,CAAA;IAC1B,MAAM,EAAE,8BAA8B,CAAC,YAAY,CAAC,CAAA;IACpD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,8BAA8B,CAAA;IACtC,KAAK,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;IACpC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACrC,CAAC,+BAiDD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
/** Product-neutral, two-sided browser call screen; authority remains in the supplied BFF client. */
|
|
5
|
+
export function BrowserRealtimeCallPanel({ conversation, client, incomingCallId, labels, media = ['audio', 'video'], onFailure, }) {
|
|
6
|
+
const [session, setSession] = useState();
|
|
7
|
+
const [status, setStatus] = useState('idle');
|
|
8
|
+
const localVideo = useRef(null);
|
|
9
|
+
const remoteVideo = useRef(null);
|
|
10
|
+
const activeSession = useRef(undefined);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
activeSession.current = session;
|
|
13
|
+
if (localVideo.current)
|
|
14
|
+
localVideo.current.srcObject = session?.localStream ?? null;
|
|
15
|
+
if (remoteVideo.current)
|
|
16
|
+
remoteVideo.current.srcObject = session?.remoteStream ?? null;
|
|
17
|
+
}, [session]);
|
|
18
|
+
useEffect(() => () => {
|
|
19
|
+
if (activeSession.current)
|
|
20
|
+
void activeSession.current.end().catch(() => undefined);
|
|
21
|
+
}, []);
|
|
22
|
+
async function open() {
|
|
23
|
+
setStatus('connecting');
|
|
24
|
+
try {
|
|
25
|
+
setSession(incomingCallId
|
|
26
|
+
? await client.accept({ conversation, callId: incomingCallId, media })
|
|
27
|
+
: await client.start({ conversation, media }));
|
|
28
|
+
setStatus('connected');
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
onFailure?.(error);
|
|
32
|
+
setStatus('failed');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function end() {
|
|
36
|
+
const active = session;
|
|
37
|
+
setSession(undefined);
|
|
38
|
+
setStatus('ended');
|
|
39
|
+
await active?.end();
|
|
40
|
+
}
|
|
41
|
+
return _jsxs("section", { className: "uhcp-realtime-call-panel", "aria-label": labels.title, children: [_jsx("h3", { children: labels.title }), !session ? _jsx("button", { type: "button", onClick: () => void open(), disabled: status === 'connecting', children: incomingCallId ? labels.accept : labels.start }) : null, _jsx("p", { role: "status", children: labels[status] || status }), session ? _jsxs("div", { className: "uhcp-realtime-call-media", children: [_jsx("video", { ref: localVideo, "data-testid": "realtime-call-local-video", autoPlay: true, muted: true, playsInline: true }), _jsx("video", { ref: remoteVideo, "data-testid": "realtime-call-remote-video", autoPlay: true, playsInline: true }), _jsx("button", { type: "button", onClick: () => void end(), children: labels.end })] }) : null] });
|
|
42
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type ExistingProfessionalProfileCopy = Readonly<{
|
|
2
|
+
eyebrow?: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
credentialMethodLabel: string;
|
|
6
|
+
manualMethodLabel: string;
|
|
7
|
+
scanProfessionalCredential: string;
|
|
8
|
+
uploadProfessionalCredentialQr: string;
|
|
9
|
+
professionalCredentialReady: string;
|
|
10
|
+
professionalCredentialRequired: string;
|
|
11
|
+
closeCamera: string;
|
|
12
|
+
cameraActive: string;
|
|
13
|
+
credentialQrUnreadable: string;
|
|
14
|
+
professionalIdentifierLabel: string;
|
|
15
|
+
sectorLabel: string;
|
|
16
|
+
pinLabel: string;
|
|
17
|
+
pinConfirmationLabel: string;
|
|
18
|
+
pinMismatch: string;
|
|
19
|
+
submit: string;
|
|
20
|
+
submitting: string;
|
|
21
|
+
}>;
|
|
22
|
+
export type ExistingProfessionalProfileInput = Readonly<{
|
|
23
|
+
method: 'credential';
|
|
24
|
+
professionalCredential: string;
|
|
25
|
+
pin: string;
|
|
26
|
+
}> | Readonly<{
|
|
27
|
+
method: 'identifier';
|
|
28
|
+
professionalIdentifier: string;
|
|
29
|
+
sector: string;
|
|
30
|
+
pin: string;
|
|
31
|
+
}>;
|
|
32
|
+
export type ExistingProfessionalProfileRequest = Readonly<{
|
|
33
|
+
professionalCredential: string;
|
|
34
|
+
pin: string;
|
|
35
|
+
}> | Readonly<{
|
|
36
|
+
professionalIdentifier: string;
|
|
37
|
+
sectorHint: string;
|
|
38
|
+
pin: string;
|
|
39
|
+
}>;
|
|
40
|
+
/** Shared BFF serialization keeps professional evidence distinct from an invitation code. */
|
|
41
|
+
export declare function toExistingProfessionalProfileRequest(input: ExistingProfessionalProfileInput): ExistingProfessionalProfileRequest;
|
|
42
|
+
export type ExistingProfessionalSectorOption = Readonly<{
|
|
43
|
+
value: string;
|
|
44
|
+
label: string;
|
|
45
|
+
}>;
|
|
46
|
+
export type ExistingProfessionalProfilePanelProps = Readonly<{
|
|
47
|
+
copy: ExistingProfessionalProfileCopy;
|
|
48
|
+
sectors: readonly ExistingProfessionalSectorOption[];
|
|
49
|
+
busy: boolean;
|
|
50
|
+
error?: string;
|
|
51
|
+
onScanProfessionalCredential?: () => Promise<string | undefined>;
|
|
52
|
+
onReadProfessionalCredentialImage?: (file: File) => Promise<string | undefined>;
|
|
53
|
+
onSubmit: (input: ExistingProfessionalProfileInput) => void | Promise<void>;
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Registers this portal device for an already-active professional profile.
|
|
57
|
+
* Professional evidence, employee invitations and device registration are
|
|
58
|
+
* separate artifacts. This screen accepts no invitation. A signed credential
|
|
59
|
+
* is the preferred path; a single DID-or-URN field plus sector is the explicit
|
|
60
|
+
* manual fallback and never becomes browser-selected authority by itself.
|
|
61
|
+
*/
|
|
62
|
+
export declare function ExistingProfessionalProfilePanel({ copy, sectors, busy, error, onScanProfessionalCredential, onReadProfessionalCredentialImage, onSubmit }: ExistingProfessionalProfilePanelProps): import("react").JSX.Element;
|
|
63
|
+
//# sourceMappingURL=ExistingProfessionalProfilePanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExistingProfessionalProfilePanel.d.ts","sourceRoot":"","sources":["../src/ExistingProfessionalProfilePanel.tsx"],"names":[],"mappings":"AAWA,MAAM,MAAM,+BAA+B,GAAG,QAAQ,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,iBAAiB,EAAE,MAAM,CAAA;IACzB,0BAA0B,EAAE,MAAM,CAAA;IAClC,8BAA8B,EAAE,MAAM,CAAA;IACtC,2BAA2B,EAAE,MAAM,CAAA;IACnC,8BAA8B,EAAE,MAAM,CAAA;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,2BAA2B,EAAE,MAAM,CAAA;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB,CAAC,CAAA;AAEF,MAAM,MAAM,gCAAgC,GACxC,QAAQ,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,sBAAsB,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAC/E,QAAQ,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,sBAAsB,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAEnG,MAAM,MAAM,kCAAkC,GAC1C,QAAQ,CAAC;IAAE,sBAAsB,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GACzD,QAAQ,CAAC;IAAE,sBAAsB,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAEjF,6FAA6F;AAC7F,wBAAgB,oCAAoC,CAAC,KAAK,EAAE,gCAAgC,GAAG,kCAAkC,CAIhI;AAED,MAAM,MAAM,gCAAgC,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAEzF,MAAM,MAAM,qCAAqC,GAAG,QAAQ,CAAC;IAC3D,IAAI,EAAE,+BAA+B,CAAA;IACrC,OAAO,EAAE,SAAS,gCAAgC,EAAE,CAAA;IACpD,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4BAA4B,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAChE,iCAAiC,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAC/E,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5E,CAAC,CAAA;AAEF;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,4BAA4B,EAAE,iCAAiC,EAAE,QAAQ,EAAE,EAAE,qCAAqC,+BAuIhM"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import jsQrModule from 'jsqr';
|
|
5
|
+
const decodeQrImage = jsQrModule;
|
|
6
|
+
/** Shared BFF serialization keeps professional evidence distinct from an invitation code. */
|
|
7
|
+
export function toExistingProfessionalProfileRequest(input) {
|
|
8
|
+
return input.method === 'credential'
|
|
9
|
+
? Object.freeze({ professionalCredential: input.professionalCredential, pin: input.pin })
|
|
10
|
+
: Object.freeze({ professionalIdentifier: input.professionalIdentifier, sectorHint: input.sector, pin: input.pin });
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Registers this portal device for an already-active professional profile.
|
|
14
|
+
* Professional evidence, employee invitations and device registration are
|
|
15
|
+
* separate artifacts. This screen accepts no invitation. A signed credential
|
|
16
|
+
* is the preferred path; a single DID-or-URN field plus sector is the explicit
|
|
17
|
+
* manual fallback and never becomes browser-selected authority by itself.
|
|
18
|
+
*/
|
|
19
|
+
export function ExistingProfessionalProfilePanel({ copy, sectors, busy, error, onScanProfessionalCredential, onReadProfessionalCredentialImage, onSubmit }) {
|
|
20
|
+
const [method, setMethod] = useState('credential');
|
|
21
|
+
const [professionalCredential, setProfessionalCredential] = useState('');
|
|
22
|
+
const [professionalIdentifier, setProfessionalIdentifier] = useState('');
|
|
23
|
+
const [sector, setSector] = useState(sectors[0]?.value || '');
|
|
24
|
+
const [pin, setPin] = useState('');
|
|
25
|
+
const [pinConfirmation, setPinConfirmation] = useState('');
|
|
26
|
+
const [localError, setLocalError] = useState('');
|
|
27
|
+
const [cameraActive, setCameraActive] = useState(false);
|
|
28
|
+
const videoRef = useRef(null);
|
|
29
|
+
const canvasRef = useRef(null);
|
|
30
|
+
const streamRef = useRef(null);
|
|
31
|
+
const timerRef = useRef(null);
|
|
32
|
+
const scanResolverRef = useRef(null);
|
|
33
|
+
function stopCamera(value) {
|
|
34
|
+
if (timerRef.current)
|
|
35
|
+
window.clearInterval(timerRef.current);
|
|
36
|
+
timerRef.current = null;
|
|
37
|
+
streamRef.current?.getTracks().forEach(track => track.stop());
|
|
38
|
+
streamRef.current = null;
|
|
39
|
+
if (videoRef.current)
|
|
40
|
+
videoRef.current.srcObject = null;
|
|
41
|
+
setCameraActive(false);
|
|
42
|
+
scanResolverRef.current?.(value);
|
|
43
|
+
scanResolverRef.current = null;
|
|
44
|
+
}
|
|
45
|
+
useEffect(() => () => stopCamera(), []);
|
|
46
|
+
async function scanWithCamera() {
|
|
47
|
+
if (onScanProfessionalCredential)
|
|
48
|
+
return onScanProfessionalCredential();
|
|
49
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' } }, audio: false });
|
|
50
|
+
streamRef.current = stream;
|
|
51
|
+
if (!videoRef.current) {
|
|
52
|
+
stream.getTracks().forEach(track => track.stop());
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
videoRef.current.srcObject = stream;
|
|
56
|
+
await videoRef.current.play();
|
|
57
|
+
setCameraActive(true);
|
|
58
|
+
return new Promise(resolve => {
|
|
59
|
+
scanResolverRef.current = resolve;
|
|
60
|
+
const Detector = window.BarcodeDetector;
|
|
61
|
+
timerRef.current = window.setInterval(() => {
|
|
62
|
+
const video = videoRef.current;
|
|
63
|
+
if (!video)
|
|
64
|
+
return;
|
|
65
|
+
const accept = (value) => { if (value)
|
|
66
|
+
stopCamera(value); };
|
|
67
|
+
if (Detector) {
|
|
68
|
+
void new Detector({ formats: ['qr_code'] }).detect(video).then(([barcode]) => accept(barcode?.rawValue)).catch(() => undefined);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const canvas = canvasRef.current;
|
|
72
|
+
if (!canvas || !video.videoWidth || !video.videoHeight)
|
|
73
|
+
return;
|
|
74
|
+
canvas.width = video.videoWidth;
|
|
75
|
+
canvas.height = video.videoHeight;
|
|
76
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
77
|
+
if (!context)
|
|
78
|
+
return;
|
|
79
|
+
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
80
|
+
accept(decodeQrImage(context.getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height)?.data);
|
|
81
|
+
}, 400);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function readCredentialImage(file) {
|
|
85
|
+
if (onReadProfessionalCredentialImage)
|
|
86
|
+
return onReadProfessionalCredentialImage(file);
|
|
87
|
+
const bitmap = await createImageBitmap(file);
|
|
88
|
+
const canvas = canvasRef.current;
|
|
89
|
+
if (!canvas)
|
|
90
|
+
return undefined;
|
|
91
|
+
canvas.width = bitmap.width;
|
|
92
|
+
canvas.height = bitmap.height;
|
|
93
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
94
|
+
if (!context)
|
|
95
|
+
return undefined;
|
|
96
|
+
context.drawImage(bitmap, 0, 0);
|
|
97
|
+
return decodeQrImage(context.getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height)?.data;
|
|
98
|
+
}
|
|
99
|
+
async function captureCredential(read) {
|
|
100
|
+
setLocalError('');
|
|
101
|
+
const credential = String(await read() || '').trim();
|
|
102
|
+
if (!credential) {
|
|
103
|
+
setLocalError(copy.professionalCredentialRequired);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setProfessionalCredential(credential);
|
|
107
|
+
}
|
|
108
|
+
function submit(event) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
if (pin !== pinConfirmation) {
|
|
111
|
+
setLocalError(copy.pinMismatch);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
setLocalError('');
|
|
115
|
+
if (method === 'credential') {
|
|
116
|
+
if (!professionalCredential) {
|
|
117
|
+
setLocalError(copy.professionalCredentialRequired);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
void onSubmit({ method, professionalCredential: professionalCredential.trim(), pin });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
void onSubmit({ method, professionalIdentifier: professionalIdentifier.trim(), sector, pin });
|
|
124
|
+
}
|
|
125
|
+
return _jsx("section", { className: "uhcp-professional-activation", "aria-labelledby": "uhcp-existing-professional-title", children: _jsxs("form", { onSubmit: submit, children: [_jsxs("header", { children: [copy.eyebrow ? _jsx("p", { className: "uhcp-eyebrow", children: copy.eyebrow }) : null, _jsx("h1", { id: "uhcp-existing-professional-title", children: copy.title }), _jsx("p", { children: copy.description })] }), _jsxs("fieldset", { children: [_jsxs("label", { children: [_jsx("input", { type: "radio", name: "professionalEvidenceMethod", value: "credential", checked: method === 'credential', onChange: () => setMethod('credential') }), copy.credentialMethodLabel] }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "professionalEvidenceMethod", value: "identifier", checked: method === 'identifier', onChange: () => setMethod('identifier') }), copy.manualMethodLabel] })] }), method === 'credential'
|
|
126
|
+
? _jsxs("div", { className: "uhcp-professional-credential-capture", children: [_jsx("button", { type: "button", disabled: busy, onClick: () => void captureCredential(scanWithCamera).catch(() => setLocalError(copy.professionalCredentialRequired)), children: copy.scanProfessionalCredential }), _jsxs("label", { children: [_jsx("span", { children: copy.uploadProfessionalCredentialQr }), _jsx("input", { name: "professionalCredentialQrImage", type: "file", accept: "image/png,image/jpeg,image/webp", disabled: busy, onChange: event => {
|
|
127
|
+
const file = event.currentTarget.files?.[0];
|
|
128
|
+
if (file)
|
|
129
|
+
void captureCredential(() => readCredentialImage(file)).catch(() => setLocalError(copy.credentialQrUnreadable));
|
|
130
|
+
} })] }), cameraActive ? _jsxs(_Fragment, { children: [_jsx("video", { ref: videoRef, playsInline: true, muted: true, "aria-label": copy.cameraActive }), _jsx("p", { role: "status", children: copy.cameraActive }), _jsx("button", { type: "button", onClick: () => stopCamera(), children: copy.closeCamera })] }) : _jsx("video", { ref: videoRef, hidden: true, playsInline: true, muted: true }), _jsx("canvas", { ref: canvasRef, hidden: true, "aria-hidden": "true" }), professionalCredential ? _jsx("p", { role: "status", children: copy.professionalCredentialReady }) : null] })
|
|
131
|
+
: _jsxs(_Fragment, { children: [_jsxs("label", { children: [_jsx("span", { children: copy.professionalIdentifierLabel }), _jsx("input", { name: "professionalIdentifier", value: professionalIdentifier, onChange: event => setProfessionalIdentifier(event.currentTarget.value), autoComplete: "off", required: true })] }), _jsxs("label", { children: [_jsx("span", { children: copy.sectorLabel }), _jsx("select", { name: "professionalSector", value: sector, onChange: event => setSector(event.currentTarget.value), required: true, children: sectors.map(option => _jsx("option", { value: option.value, children: option.label }, option.value)) })] })] }), _jsxs("label", { children: [_jsx("span", { children: copy.pinLabel }), _jsx("input", { name: "newPin", type: "password", inputMode: "numeric", minLength: 6, maxLength: 12, autoComplete: "new-password", value: pin, onChange: event => setPin(event.currentTarget.value), required: true })] }), _jsxs("label", { children: [_jsx("span", { children: copy.pinConfirmationLabel }), _jsx("input", { name: "confirmPin", type: "password", inputMode: "numeric", minLength: 6, maxLength: 12, autoComplete: "new-password", value: pinConfirmation, onChange: event => setPinConfirmation(event.currentTarget.value), required: true })] }), (localError || error) ? _jsx("p", { role: "alert", children: localError || error }) : null, _jsx("button", { type: "submit", disabled: busy, children: busy ? copy.submitting : copy.submit })] }) });
|
|
132
|
+
}
|
package/dist/PortalHeader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PortalBrand, type PortalAccountOption, type PortalLanguageOption, type PortalMenuAction, type PortalShellCopy } from './contracts.js';
|
|
1
|
+
import { type PortalBrand, type PortalAccountOption, type PortalLanguageOption, type PortalMenuAction, type PortalShellCopy, type PortalUiTheme } from './contracts.js';
|
|
2
2
|
import { type PortalCurrentSubjectRegistration } from './PortalCurrentSubject.js';
|
|
3
3
|
export type PortalHeaderProps = Readonly<{
|
|
4
4
|
brand: PortalBrand;
|
|
@@ -11,6 +11,7 @@ export type PortalHeaderProps = Readonly<{
|
|
|
11
11
|
onSelectAccount?: (id: string) => void;
|
|
12
12
|
menuActions?: readonly PortalMenuAction[];
|
|
13
13
|
onSignOut?: () => void | Promise<void>;
|
|
14
|
+
onThemeChange?: (theme: PortalUiTheme) => void;
|
|
14
15
|
storageNamespace?: string;
|
|
15
16
|
currentSubject?: PortalCurrentSubjectRegistration;
|
|
16
17
|
}>;
|
|
@@ -20,5 +21,5 @@ export type PortalHeaderProps = Readonly<{
|
|
|
20
21
|
* Authentication is deliberately supplied as data and callbacks. The
|
|
21
22
|
* component never imports Firebase and never treats browser state as a role.
|
|
22
23
|
*/
|
|
23
|
-
export declare function PortalHeader({ brand, copy, accountIdentifier, languageHref, languageOptions, guestActionHref, accountOptions, onSelectAccount, menuActions, onSignOut, storageNamespace, currentSubject, }: PortalHeaderProps): import("react").JSX.Element;
|
|
24
|
+
export declare function PortalHeader({ brand, copy, accountIdentifier, languageHref, languageOptions, guestActionHref, accountOptions, onSelectAccount, menuActions, onSignOut, onThemeChange, storageNamespace, currentSubject, }: PortalHeaderProps): import("react").JSX.Element;
|
|
24
25
|
//# sourceMappingURL=PortalHeader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PortalHeader.d.ts","sourceRoot":"","sources":["../src/PortalHeader.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"PortalHeader.d.ts","sourceRoot":"","sources":["../src/PortalHeader.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EAEpB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAA;AAOvB,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,2BAA2B,CAAA;AAElC,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,KAAK,EAAE,WAAW,CAAA;IAClB,IAAI,EAAE,eAAe,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACjD,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAA;IAC/C,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,WAAW,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACzC,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,CAAC,EAAE,gCAAgC,CAAA;CAClD,CAAC,CAAA;AAEF;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,IAAI,EACJ,iBAAiB,EACjB,YAAY,EACZ,eAAoB,EACpB,eAAe,EACf,cAAmB,EACnB,eAAe,EACf,WAAgB,EAChB,SAAS,EACT,aAAa,EACb,gBAA+B,EAC/B,cAAc,GACf,EAAE,iBAAiB,+BAgLnB"}
|
package/dist/PortalHeader.js
CHANGED
|
@@ -11,7 +11,7 @@ import { PortalCurrentSubjectControl, PortalCurrentSubjectSlot, } from './Portal
|
|
|
11
11
|
* Authentication is deliberately supplied as data and callbacks. The
|
|
12
12
|
* component never imports Firebase and never treats browser state as a role.
|
|
13
13
|
*/
|
|
14
|
-
export function PortalHeader({ brand, copy, accountIdentifier, languageHref, languageOptions = [], guestActionHref, accountOptions = [], onSelectAccount, menuActions = [], onSignOut, storageNamespace = 'uhc-portal', currentSubject, }) {
|
|
14
|
+
export function PortalHeader({ brand, copy, accountIdentifier, languageHref, languageOptions = [], guestActionHref, accountOptions = [], onSelectAccount, menuActions = [], onSignOut, onThemeChange, storageNamespace = 'uhc-portal', currentSubject, }) {
|
|
15
15
|
const [open, setOpen] = useState(false);
|
|
16
16
|
const [density, setDensity] = useState('medium');
|
|
17
17
|
const [theme, setTheme] = useState('system');
|
|
@@ -32,6 +32,8 @@ export function PortalHeader({ brand, copy, accountIdentifier, languageHref, lan
|
|
|
32
32
|
const apply = () => {
|
|
33
33
|
const rendered = resolveRenderedPortalTheme(theme, media?.matches === true);
|
|
34
34
|
document.documentElement.dataset.theme = rendered;
|
|
35
|
+
document.documentElement.classList.toggle('dark', rendered === 'dark');
|
|
36
|
+
document.documentElement.classList.toggle('light', rendered === 'light');
|
|
35
37
|
document.documentElement.style.colorScheme = rendered;
|
|
36
38
|
};
|
|
37
39
|
apply();
|
|
@@ -62,6 +64,7 @@ export function PortalHeader({ brand, copy, accountIdentifier, languageHref, lan
|
|
|
62
64
|
function changeTheme(value) {
|
|
63
65
|
setTheme(value);
|
|
64
66
|
localStorage.setItem(themeKey, value);
|
|
67
|
+
onThemeChange?.(value);
|
|
65
68
|
}
|
|
66
69
|
return (_jsxs("header", { className: "uhcp-header", style: brand.variables, children: [_jsxs("a", { className: "uhcp-brand", href: brand.homeHref, "aria-label": copy.home, children: [brand.mark ? (_jsx(PortalBrandArtwork, { artwork: brand.mark, className: "uhcp-brand-mark" })) : null, _jsxs("span", { children: [_jsx("strong", { children: brand.productName }), _jsx("small", { className: accountIdentifier ? 'uhcp-brand-account' : undefined, children: accountIdentifier || brand.organizationName })] })] }), currentSubject
|
|
67
70
|
? _jsx(PortalCurrentSubjectControl, { ...currentSubject })
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type ProfessionalProfileActivationCopy = Readonly<{
|
|
2
|
+
eyebrow?: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
credentialLabel: string;
|
|
6
|
+
pinLabel: string;
|
|
7
|
+
pinConfirmationLabel: string;
|
|
8
|
+
pinMismatch: string;
|
|
9
|
+
submit: string;
|
|
10
|
+
submitting: string;
|
|
11
|
+
}>;
|
|
12
|
+
export type ProfessionalProfileActivationInput = Readonly<{
|
|
13
|
+
credential: string;
|
|
14
|
+
pin: string;
|
|
15
|
+
}>;
|
|
16
|
+
export type ProfessionalProfileActivationPanelProps = Readonly<{
|
|
17
|
+
copy: ProfessionalProfileActivationCopy;
|
|
18
|
+
busy: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
onSubmit: (input: ProfessionalProfileActivationInput) => void | Promise<void>;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Legacy brand-neutral employee-invitation activation.
|
|
24
|
+
* `credential` is retained as an API-compatible field name, but its value is
|
|
25
|
+
* an invitation/activation code, never a reusable professional credential.
|
|
26
|
+
* Existing professionals use ExistingProfessionalProfilePanel instead.
|
|
27
|
+
*/
|
|
28
|
+
export declare function ProfessionalProfileActivationPanel({ copy, busy, error, onSubmit }: ProfessionalProfileActivationPanelProps): import("react").JSX.Element;
|
|
29
|
+
//# sourceMappingURL=ProfessionalProfileActivationPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProfessionalProfileActivationPanel.d.ts","sourceRoot":"","sources":["../src/ProfessionalProfileActivationPanel.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,iCAAiC,GAAG,QAAQ,CAAC;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB,CAAC,CAAA;AAEF,MAAM,MAAM,kCAAkC,GAAG,QAAQ,CAAC;IACxD,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAC,CAAA;AAEF,MAAM,MAAM,uCAAuC,GAAG,QAAQ,CAAC;IAC7D,IAAI,EAAE,iCAAiC,CAAA;IACvC,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,kCAAkC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9E,CAAC,CAAA;AAEF;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uCAAuC,+BA8B1H"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Legacy brand-neutral employee-invitation activation.
|
|
6
|
+
* `credential` is retained as an API-compatible field name, but its value is
|
|
7
|
+
* an invitation/activation code, never a reusable professional credential.
|
|
8
|
+
* Existing professionals use ExistingProfessionalProfilePanel instead.
|
|
9
|
+
*/
|
|
10
|
+
export function ProfessionalProfileActivationPanel({ copy, busy, error, onSubmit }) {
|
|
11
|
+
const [credential, setCredential] = useState('');
|
|
12
|
+
const [pin, setPin] = useState('');
|
|
13
|
+
const [pinConfirmation, setPinConfirmation] = useState('');
|
|
14
|
+
const [localError, setLocalError] = useState('');
|
|
15
|
+
function submit(event) {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
if (pin !== pinConfirmation) {
|
|
18
|
+
setLocalError(copy.pinMismatch);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
setLocalError('');
|
|
22
|
+
void onSubmit({ credential: credential.trim(), pin });
|
|
23
|
+
}
|
|
24
|
+
return _jsx("section", { className: "uhcp-professional-activation", "aria-labelledby": "uhcp-professional-activation-title", children: _jsxs("form", { onSubmit: submit, children: [_jsxs("header", { children: [copy.eyebrow ? _jsx("p", { className: "uhcp-eyebrow", children: copy.eyebrow }) : null, _jsx("h1", { id: "uhcp-professional-activation-title", children: copy.title }), _jsx("p", { children: copy.description })] }), _jsxs("label", { children: [_jsx("span", { children: copy.credentialLabel }), _jsx("input", { name: "professionalCredential", value: credential, onChange: event => setCredential(event.currentTarget.value), autoComplete: "one-time-code", required: true })] }), _jsxs("label", { children: [_jsx("span", { children: copy.pinLabel }), _jsx("input", { name: "newPin", type: "password", inputMode: "numeric", minLength: 6, maxLength: 12, autoComplete: "new-password", value: pin, onChange: event => setPin(event.currentTarget.value), required: true })] }), _jsxs("label", { children: [_jsx("span", { children: copy.pinConfirmationLabel }), _jsx("input", { name: "confirmPin", type: "password", inputMode: "numeric", minLength: 6, maxLength: 12, autoComplete: "new-password", value: pinConfirmation, onChange: event => setPinConfirmation(event.currentTarget.value), required: true })] }), (localError || error) ? _jsx("p", { role: "alert", children: localError || error }) : null, _jsx("button", { type: "submit", disabled: busy, children: busy ? copy.submitting : copy.submit })] }) });
|
|
25
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -55,5 +55,9 @@ export * from './CardPreview.js';
|
|
|
55
55
|
export * from './CollapsiblePortalSection.js';
|
|
56
56
|
export * from './ElapsedOperationStatus.js';
|
|
57
57
|
export * from './ProfileRecoveryPanel.js';
|
|
58
|
+
export * from './ProfessionalProfileActivationPanel.js';
|
|
59
|
+
export * from './ExistingProfessionalProfilePanel.js';
|
|
60
|
+
export * from './BrowserRealtimeCallClient.js';
|
|
61
|
+
export * from './BrowserRealtimeCallPanel.js';
|
|
58
62
|
export * from './passkey-policy.js';
|
|
59
63
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,mBAAmB,CAAA;AACjC,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,wCAAwC,CAAA;AACtD,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,mCAAmC,CAAA;AACjD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qCAAqC,CAAA;AACnD,cAAc,mCAAmC,CAAA;AACjD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,wBAAwB,CAAA;AACtC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,0CAA0C,CAAA;AACxD,cAAc,kCAAkC,CAAA;AAChD,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,oCAAoC,CAAA;AAClD,cAAc,sCAAsC,CAAA;AACpD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uCAAuC,CAAA;AACrD,cAAc,uBAAuB,CAAA;AACrC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,uBAAuB,CAAA;AACrC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kBAAkB,CAAA;AAChC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,mBAAmB,CAAA;AACjC,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,wCAAwC,CAAA;AACtD,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,mCAAmC,CAAA;AACjD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qCAAqC,CAAA;AACnD,cAAc,mCAAmC,CAAA;AACjD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,wBAAwB,CAAA;AACtC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,0CAA0C,CAAA;AACxD,cAAc,kCAAkC,CAAA;AAChD,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,oCAAoC,CAAA;AAClD,cAAc,sCAAsC,CAAA;AACpD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uCAAuC,CAAA;AACrD,cAAc,uBAAuB,CAAA;AACrC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,uBAAuB,CAAA;AACrC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kBAAkB,CAAA;AAChC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,yCAAyC,CAAA;AACvD,cAAc,uCAAuC,CAAA;AACrD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qBAAqB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -55,4 +55,8 @@ export * from './CardPreview.js';
|
|
|
55
55
|
export * from './CollapsiblePortalSection.js';
|
|
56
56
|
export * from './ElapsedOperationStatus.js';
|
|
57
57
|
export * from './ProfileRecoveryPanel.js';
|
|
58
|
+
export * from './ProfessionalProfileActivationPanel.js';
|
|
59
|
+
export * from './ExistingProfessionalProfilePanel.js';
|
|
60
|
+
export * from './BrowserRealtimeCallClient.js';
|
|
61
|
+
export * from './BrowserRealtimeCallPanel.js';
|
|
58
62
|
export * from './passkey-policy.js';
|
package/dist/styles.css
CHANGED
|
@@ -926,6 +926,14 @@ html[data-theme='dark'] .uhcp-brand-artwork-dark { display: block !important; }
|
|
|
926
926
|
.uhcp-professional-access button { background: linear-gradient(135deg, var(--uhcp-action-start, #0f9ca8), var(--uhcp-action-end, #2563a9)); border: 0; border-radius: 999px; color: #fff; cursor: pointer; font: inherit; font-weight: 800; min-height: 2.8rem; padding: .7rem 1rem; }
|
|
927
927
|
.uhcp-professional-access button:disabled { cursor: wait; opacity: .55; }
|
|
928
928
|
.uhcp-professional-access .uhcp-secondary { background: transparent; border: 1px solid var(--uhcp-line, #cbd5e1); color: inherit; }
|
|
929
|
+
.uhcp-professional-activation { display:grid; margin-inline:auto; max-width:42rem; }
|
|
930
|
+
.uhcp-professional-activation form { display:grid; gap:1rem; }
|
|
931
|
+
.uhcp-professional-activation header { display:grid; gap:.55rem; }
|
|
932
|
+
.uhcp-professional-activation h1,.uhcp-professional-activation p { margin:0; }
|
|
933
|
+
.uhcp-professional-activation label { display:grid; font-weight:700; gap:.4rem; }
|
|
934
|
+
.uhcp-professional-activation input { background:var(--uhcp-surface,#fff); border:1px solid var(--uhcp-line,#cbd5e1); border-radius:.8rem; color:inherit; font:inherit; min-height:2.8rem; padding-inline:.85rem; }
|
|
935
|
+
.uhcp-professional-activation button { background:linear-gradient(135deg,var(--uhcp-action-start,#0f9ca8),var(--uhcp-action-end,#2563a9)); border:0; border-radius:999px; color:#fff; cursor:pointer; font:inherit; font-weight:800; justify-self:start; min-height:2.8rem; padding:.7rem 1rem; }
|
|
936
|
+
.uhcp-professional-activation button:disabled { cursor:wait; opacity:.55; }
|
|
929
937
|
.uhcp-animal-onboarding-actions { align-items:center; display:flex; flex-wrap:wrap; gap:.75rem; justify-content:flex-end; }
|
|
930
938
|
.uhcp-animal-identity-readonly-grid { display:grid; gap:1rem; grid-template-columns:repeat(2,minmax(0,1fr)); margin:0; padding:1rem; }
|
|
931
939
|
.uhcp-animal-identity-readonly-grid div { background:var(--uhcp-surface,#fff); border:1px solid var(--uhcp-border,#cbd5e1); border-radius:.8rem; padding:.8rem; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uhc-portal-components-react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.29",
|
|
4
4
|
"description": "Brand-neutral React portal screens, account preferences and protected-profile UI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"fhir-data-utils-ts": "0.6.21",
|
|
35
35
|
"gdc-common-utils-ts": "2.9.30",
|
|
36
|
+
"jsqr": "1.4.0",
|
|
36
37
|
"sos-data-utils-ts": "0.6.15",
|
|
37
38
|
"terminology-components-react": "0.2.0"
|
|
38
39
|
},
|