@7365admin1/layer-common 3.2.1-staging.72 → 3.2.2

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/components/EntryPassInformation.vue +4 -4
  3. package/components/FeedbackMain.vue +7 -7
  4. package/components/HidAccessLogDashboard.vue +44 -119
  5. package/components/HidIdentityMapping.vue +975 -0
  6. package/components/HidIntercomManagement.vue +201 -808
  7. package/components/HidQrCodeConfiguration.vue +95 -907
  8. package/components/HidServiceSettingsPanel.vue +1 -20
  9. package/components/HidUserEnrollment.vue +81 -392
  10. package/components/Layout/NavigationDrawer.vue +2 -43
  11. package/components/NavigationItem.vue +0 -9
  12. package/components/Nfc/NFCPatrolRouteMain.vue +7 -52
  13. package/components/OnlineFormConfigurationForm.vue +224 -620
  14. package/components/OnlineFormFill.vue +8 -36
  15. package/components/OnlineFormsConfiguration.vue +2 -6
  16. package/components/QrTemplate/CreditCardLandscape.vue +12 -19
  17. package/components/QrTemplate/PrintDialog.vue +196 -209
  18. package/components/SiteSettings.vue +2 -4
  19. package/components/VisitorForm.vue +78 -201
  20. package/components/VisitorManagement.vue +2 -1
  21. package/composables/useComment.ts +3 -3
  22. package/composables/useHidAmico.ts +0 -127
  23. package/composables/useHidNavigation.ts +7 -0
  24. package/composables/useOnlineFormPages.ts +0 -2
  25. package/composables/useSiteSettings.ts +0 -50
  26. package/composables/useWebUsb.ts +37 -127
  27. package/middleware/member.ts +4 -0
  28. package/package.json +1 -2
  29. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +23 -0
  30. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  31. package/plugins/secure-member.client.ts +56 -21
  32. package/plugins/vuetify.ts +9 -37
  33. package/types/site.d.ts +0 -71
  34. package/.changeset/camera-wall-gated-endpoint.md +0 -47
  35. package/.changeset/camera-wall-plain-english.md +0 -60
  36. package/.changeset/camera-wall-selection-and-guidance.md +0 -65
  37. package/.changeset/dark-primary-contrast.md +0 -39
  38. package/components/CameraWall.vue +0 -1034
  39. package/components/CameraWallTile.vue +0 -818
  40. package/components/HidCameraCaptureDialog.vue +0 -199
  41. package/composables/useSipWebPhone.ts +0 -311
  42. package/utils/camera-wall.test.ts +0 -571
  43. package/utils/camera-wall.ts +0 -926
  44. package/utils/theme.test.ts +0 -201
  45. package/utils/theme.ts +0 -136
@@ -1,199 +0,0 @@
1
- <template>
2
- <v-dialog
3
- :model-value="modelValue"
4
- max-width="520"
5
- persistent
6
- @update:model-value="emit('update:modelValue', $event)"
7
- >
8
- <v-card class="camera-dialog" rounded="lg">
9
- <v-card-title class="d-flex align-center justify-space-between pa-4">
10
- <span class="text-subtitle-1 font-weight-bold">Take Facial Photo</span>
11
- <v-btn
12
- icon="mdi-close"
13
- variant="text"
14
- size="small"
15
- aria-label="Close camera"
16
- @click="close"
17
- />
18
- </v-card-title>
19
-
20
- <v-divider />
21
-
22
- <v-card-text class="pa-4">
23
- <div class="camera-viewport">
24
- <video
25
- ref="videoElement"
26
- class="camera-preview"
27
- autoplay
28
- muted
29
- playsinline
30
- />
31
-
32
- <div v-if="starting" class="camera-state">
33
- <v-progress-circular indeterminate color="primary" />
34
- <span class="mt-3">Starting camera...</span>
35
- </div>
36
-
37
- <div v-else-if="cameraError" class="camera-state px-6 text-center">
38
- <v-icon icon="mdi-camera-off-outline" size="42" color="error" />
39
- <span class="mt-3 text-error">{{ cameraError }}</span>
40
- </div>
41
- </div>
42
-
43
- <canvas ref="canvasElement" class="d-none" />
44
-
45
- <div class="text-caption text-medium-emphasis mt-3 text-center">
46
- Keep your face centered and make sure the area is well lit.
47
- </div>
48
- </v-card-text>
49
-
50
- <v-divider />
51
-
52
- <v-card-actions class="pa-4">
53
- <v-btn variant="text" @click="close">Cancel</v-btn>
54
- <v-spacer />
55
- <v-btn
56
- color="primary"
57
- variant="flat"
58
- prepend-icon="mdi-camera"
59
- :disabled="starting || !!cameraError || !stream"
60
- @click="capture"
61
- >
62
- Take Photo
63
- </v-btn>
64
- </v-card-actions>
65
- </v-card>
66
- </v-dialog>
67
- </template>
68
-
69
- <script setup lang="ts">
70
- const props = defineProps<{ modelValue: boolean }>();
71
- const emit = defineEmits<{
72
- "update:modelValue": [value: boolean];
73
- capture: [dataUrl: string];
74
- }>();
75
-
76
- const videoElement = ref<HTMLVideoElement | null>(null);
77
- const canvasElement = ref<HTMLCanvasElement | null>(null);
78
- const stream = shallowRef<MediaStream | null>(null);
79
- const starting = ref(false);
80
- const cameraError = ref("");
81
-
82
- function stopCamera() {
83
- stream.value?.getTracks().forEach((track) => track.stop());
84
- stream.value = null;
85
- if (videoElement.value) videoElement.value.srcObject = null;
86
- }
87
-
88
- function getCameraErrorMessage(caught: unknown) {
89
- if (caught instanceof DOMException) {
90
- if (caught.name === "NotAllowedError") {
91
- return "Camera permission was denied. Allow camera access and try again.";
92
- }
93
- if (caught.name === "NotFoundError") {
94
- return "No camera was found on this device.";
95
- }
96
- if (caught.name === "NotReadableError") {
97
- return "The camera is currently being used by another application.";
98
- }
99
- }
100
- return "Unable to open the camera. Use HTTPS or check the browser camera permission.";
101
- }
102
-
103
- async function startCamera() {
104
- stopCamera();
105
- cameraError.value = "";
106
- starting.value = true;
107
-
108
- try {
109
- if (!navigator.mediaDevices?.getUserMedia) {
110
- throw new Error("Camera API is unavailable");
111
- }
112
-
113
- stream.value = await navigator.mediaDevices.getUserMedia({
114
- audio: false,
115
- video: {
116
- facingMode: "user",
117
- width: { ideal: 1280 },
118
- height: { ideal: 960 },
119
- },
120
- });
121
-
122
- await nextTick();
123
- if (!videoElement.value) throw new Error("Camera preview is unavailable");
124
- videoElement.value.srcObject = stream.value;
125
- await videoElement.value.play();
126
- } catch (caught: unknown) {
127
- stopCamera();
128
- cameraError.value = getCameraErrorMessage(caught);
129
- } finally {
130
- starting.value = false;
131
- }
132
- }
133
-
134
- function capture() {
135
- const video = videoElement.value;
136
- const canvas = canvasElement.value;
137
- if (!video || !canvas || !video.videoWidth || !video.videoHeight) return;
138
-
139
- canvas.width = video.videoWidth;
140
- canvas.height = video.videoHeight;
141
- const context = canvas.getContext("2d");
142
- if (!context) return;
143
-
144
- context.translate(canvas.width, 0);
145
- context.scale(-1, 1);
146
- context.drawImage(video, 0, 0, canvas.width, canvas.height);
147
-
148
- emit("capture", canvas.toDataURL("image/jpeg", 0.9));
149
- close();
150
- }
151
-
152
- function close() {
153
- stopCamera();
154
- emit("update:modelValue", false);
155
- }
156
-
157
- watch(
158
- () => props.modelValue,
159
- (isOpen) => {
160
- if (isOpen) void startCamera();
161
- else stopCamera();
162
- }
163
- );
164
-
165
- onBeforeUnmount(stopCamera);
166
- </script>
167
-
168
- <style scoped>
169
- .camera-dialog {
170
- overflow: hidden;
171
- }
172
-
173
- .camera-viewport {
174
- position: relative;
175
- width: 100%;
176
- overflow: hidden;
177
- aspect-ratio: 3 / 4;
178
- border-radius: 8px;
179
- background: #111722;
180
- }
181
-
182
- .camera-preview {
183
- width: 100%;
184
- height: 100%;
185
- object-fit: cover;
186
- transform: scaleX(-1);
187
- }
188
-
189
- .camera-state {
190
- position: absolute;
191
- inset: 0;
192
- display: flex;
193
- flex-direction: column;
194
- align-items: center;
195
- justify-content: center;
196
- background: #111722;
197
- color: #ffffff;
198
- }
199
- </style>
@@ -1,311 +0,0 @@
1
- import { computed, onScopeDispose, readonly, ref, shallowRef } from "vue";
2
- import type { SimpleUser, SimpleUserDelegate, SimpleUserOptions } from "sip.js/lib/platform/web";
3
- import type { SessionDescriptionHandlerOptions } from "sip.js/lib/platform/web/session-description-handler/session-description-handler-options";
4
-
5
- export type SipWebPhoneConfig = {
6
- webSocketServer: string;
7
- aor: string;
8
- authorizationUsername: string;
9
- authorizationPassword: string;
10
- displayName?: string;
11
- video?: boolean;
12
- };
13
-
14
- export type SipWebPhoneMedia = {
15
- remoteAudio?: HTMLAudioElement | null;
16
- remoteVideo?: HTMLVideoElement | null;
17
- };
18
-
19
- type ConnectionState = "disconnected" | "connecting" | "registered" | "error";
20
- type CallState = "idle" | "calling" | "incoming" | "active" | "ending";
21
-
22
- export default function useSipWebPhone() {
23
- const simpleUser = shallowRef<SimpleUser>();
24
- const connectionState = ref<ConnectionState>("disconnected");
25
- const callState = ref<CallState>("idle");
26
- const errorMessage = ref("");
27
- const muted = ref(false);
28
- const remoteVideoAvailable = ref(false);
29
- const currentTarget = ref("");
30
- const activeConfig = shallowRef<SipWebPhoneConfig>();
31
-
32
- const isRegistered = computed(() => connectionState.value === "registered");
33
- const hasCall = computed(() => callState.value !== "idle");
34
-
35
- async function connect(config: SipWebPhoneConfig, media: SipWebPhoneMedia) {
36
- validateConfig(config);
37
- await disconnect();
38
-
39
- connectionState.value = "connecting";
40
- errorMessage.value = "";
41
- activeConfig.value = { ...config };
42
- let clearRegistrationTimer = () => {};
43
-
44
- try {
45
- const { SimpleUser: SimpleUserConstructor } = await import("sip.js/lib/platform/web");
46
- let registrationSettled = false;
47
- let resolveRegistration: (() => void) | undefined;
48
- let rejectRegistration: ((error: Error) => void) | undefined;
49
- const registrationPromise = new Promise<void>((resolve, reject) => {
50
- resolveRegistration = resolve;
51
- rejectRegistration = reject;
52
- });
53
- const registrationTimeout = globalThis.setTimeout(() => {
54
- if (registrationSettled) return;
55
- registrationSettled = true;
56
- rejectRegistration?.(new Error("SIP registration timed out. Check the WebSocket server and account credentials."));
57
- }, 15000);
58
- clearRegistrationTimer = () => globalThis.clearTimeout(registrationTimeout);
59
- const settleRegistration = (error?: Error) => {
60
- if (registrationSettled) return;
61
- registrationSettled = true;
62
- clearRegistrationTimer();
63
- if (error) rejectRegistration?.(error);
64
- else resolveRegistration?.();
65
- };
66
- const delegate: SimpleUserDelegate = {
67
- onCallCreated: () => {
68
- if (callState.value !== "incoming") callState.value = "calling";
69
- },
70
- onCallReceived: () => {
71
- callState.value = "incoming";
72
- currentTarget.value = "Incoming call";
73
- },
74
- onCallAnswered: () => {
75
- callState.value = "active";
76
- syncVideoState();
77
- },
78
- onCallHangup: () => {
79
- const previousState = callState.value;
80
- resetCall();
81
- if (previousState === "calling") {
82
- errorMessage.value = "The call was not answered or was rejected by the destination.";
83
- }
84
- },
85
- onRegistered: () => {
86
- connectionState.value = "registered";
87
- settleRegistration();
88
- },
89
- onUnregistered: () => {
90
- if (connectionState.value === "connecting") {
91
- settleRegistration(new Error("SIP registration was rejected. Check the username and password."));
92
- }
93
- connectionState.value = "disconnected";
94
- },
95
- onServerDisconnect: () => {
96
- const error = new Error("The SIP WebSocket server disconnected during registration.");
97
- connectionState.value = "error";
98
- errorMessage.value = error.message;
99
- settleRegistration(error);
100
- resetCall();
101
- },
102
- };
103
- const options: SimpleUserOptions = {
104
- aor: normalizeSipUri(config.aor),
105
- delegate,
106
- media: {
107
- constraints: { audio: true, video: false },
108
- remote: {
109
- audio: media.remoteAudio || undefined,
110
- video: media.remoteVideo || undefined,
111
- },
112
- },
113
- userAgentOptions: {
114
- authorizationUsername: config.authorizationUsername,
115
- authorizationPassword: config.authorizationPassword,
116
- displayName: config.displayName || config.authorizationUsername,
117
- },
118
- };
119
-
120
- const user = new SimpleUserConstructor(config.webSocketServer.trim(), options);
121
- simpleUser.value = user;
122
- await user.connect();
123
- await user.register();
124
- await registrationPromise;
125
- } catch (error) {
126
- clearRegistrationTimer();
127
- await disconnect(false);
128
- connectionState.value = "error";
129
- errorMessage.value = getErrorMessage(error);
130
- throw error;
131
- }
132
- }
133
-
134
- async function disconnect(clearError = true) {
135
- const user = simpleUser.value;
136
- simpleUser.value = undefined;
137
-
138
- if (user) {
139
- try {
140
- if (hasCall.value) await user.hangup();
141
- } catch {
142
- // The session may already have ended remotely.
143
- }
144
- try {
145
- await user.unregister();
146
- } catch {
147
- // Registration may not have completed.
148
- }
149
- try {
150
- await user.disconnect();
151
- } catch {
152
- // The WebSocket may already be closed.
153
- }
154
- }
155
-
156
- connectionState.value = "disconnected";
157
- activeConfig.value = undefined;
158
- resetCall();
159
- if (clearError) errorMessage.value = "";
160
- }
161
-
162
- async function call(target: string) {
163
- const user = requireRegisteredUser();
164
- const destination = normalizeDestination(target, activeConfig.value?.aor || "");
165
- errorMessage.value = "";
166
- currentTarget.value = destination;
167
- callState.value = "calling";
168
- try {
169
- const sessionDescriptionHandlerOptions: SessionDescriptionHandlerOptions = {
170
- constraints: { audio: true, video: false },
171
- offerOptions: {
172
- offerToReceiveAudio: true,
173
- offerToReceiveVideo: Boolean(activeConfig.value?.video),
174
- },
175
- };
176
- await user.call(destination, { sessionDescriptionHandlerOptions });
177
- } catch (error) {
178
- resetCall();
179
- errorMessage.value = getErrorMessage(error);
180
- throw error;
181
- }
182
- }
183
-
184
- async function answer() {
185
- const user = requireRegisteredUser();
186
- const sessionDescriptionHandlerOptions: SessionDescriptionHandlerOptions = {
187
- constraints: { audio: true, video: false },
188
- };
189
- await user.answer({ sessionDescriptionHandlerOptions });
190
- }
191
-
192
- async function decline() {
193
- const user = requireUser();
194
- callState.value = "ending";
195
- await user.decline();
196
- resetCall();
197
- }
198
-
199
- async function hangup() {
200
- const user = requireUser();
201
- callState.value = "ending";
202
- await user.hangup();
203
- resetCall();
204
- }
205
-
206
- function toggleMute() {
207
- const user = requireUser();
208
- const tracks = user.localMediaStream?.getAudioTracks() || [];
209
- if (!tracks.length) throw new Error("No microphone track is available for this call.");
210
- const enable = muted.value;
211
- tracks.forEach((track) => {
212
- track.enabled = enable;
213
- });
214
- muted.value = !enable;
215
- }
216
-
217
- function syncVideoState() {
218
- const user = simpleUser.value;
219
- const remoteStream = user?.remoteMediaStream;
220
- const remoteTrack = remoteStream?.getVideoTracks()[0];
221
-
222
- const updateState = () => {
223
- const currentRemoteTrack = remoteStream?.getVideoTracks()[0];
224
- remoteVideoAvailable.value = Boolean(currentRemoteTrack && currentRemoteTrack.readyState === "live");
225
- };
226
-
227
- updateState();
228
- if (remoteStream) remoteStream.onaddtrack = updateState;
229
-
230
- if (remoteTrack) {
231
- remoteTrack.onended = () => {
232
- remoteVideoAvailable.value = false;
233
- };
234
- }
235
- }
236
-
237
- function resetCall() {
238
- callState.value = "idle";
239
- currentTarget.value = "";
240
- muted.value = false;
241
- remoteVideoAvailable.value = false;
242
- }
243
-
244
- function requireRegisteredUser() {
245
- const user = requireUser();
246
- if (!isRegistered.value) throw new Error("Connect the web phone before making a call.");
247
- return user;
248
- }
249
-
250
- function requireUser() {
251
- if (!simpleUser.value) throw new Error("Web phone is not connected.");
252
- return simpleUser.value;
253
- }
254
-
255
- onScopeDispose(() => {
256
- void disconnect();
257
- });
258
-
259
- return {
260
- connectionState: readonly(connectionState),
261
- callState: readonly(callState),
262
- errorMessage: readonly(errorMessage),
263
- muted: readonly(muted),
264
- remoteVideoAvailable: readonly(remoteVideoAvailable),
265
- currentTarget: readonly(currentTarget),
266
- isRegistered,
267
- hasCall,
268
- connect,
269
- disconnect,
270
- call,
271
- answer,
272
- decline,
273
- hangup,
274
- toggleMute,
275
- };
276
- }
277
-
278
- function validateConfig(config: SipWebPhoneConfig) {
279
- if (!/^wss:\/\//i.test(config.webSocketServer.trim())) {
280
- throw new Error("SIP WebSocket URL must use WSS.");
281
- }
282
- if (!config.aor.trim() || !config.authorizationUsername.trim() || !config.authorizationPassword) {
283
- throw new Error("SIP address, username, and password are required.");
284
- }
285
- }
286
-
287
- function normalizeSipUri(value: string) {
288
- const trimmed = value.trim();
289
- return trimmed.toLowerCase().startsWith("sip:") ? trimmed : `sip:${trimmed}`;
290
- }
291
-
292
- function normalizeDestination(target: string, aor: string) {
293
- const trimmed = target.trim();
294
- if (!trimmed) throw new Error("Enter an extension or SIP address to call.");
295
- if (trimmed.toLowerCase().startsWith("sip:")) return trimmed;
296
- if (trimmed.includes("@")) return `sip:${trimmed}`;
297
-
298
- const domain = normalizeSipUri(aor).split("@")[1];
299
- if (!domain) throw new Error("Enter a full SIP address for the destination.");
300
- return `sip:${trimmed}@${domain}`;
301
- }
302
-
303
- function getErrorMessage(error: unknown) {
304
- if (error instanceof DOMException && error.name === "NotAllowedError") {
305
- return "Microphone permission was denied. Allow microphone access in the browser and reconnect the web phone.";
306
- }
307
- if (error instanceof DOMException && error.name === "NotFoundError") {
308
- return "No microphone was found for this call.";
309
- }
310
- return error instanceof Error ? error.message : "SIP connection failed.";
311
- }