@localzet/data-connector 1.0.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.
Files changed (51) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +52 -0
  3. package/dist/api/index.d.ts +4 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +3 -0
  6. package/dist/api/mixIdApi.d.ts +76 -0
  7. package/dist/api/mixIdApi.d.ts.map +1 -0
  8. package/dist/api/mixIdApi.js +275 -0
  9. package/dist/api/offlineQueue.d.ts +24 -0
  10. package/dist/api/offlineQueue.d.ts.map +1 -0
  11. package/dist/api/offlineQueue.js +137 -0
  12. package/dist/api/websocket.d.ts +28 -0
  13. package/dist/api/websocket.d.ts.map +1 -0
  14. package/dist/api/websocket.js +201 -0
  15. package/dist/components/MixIdCallbackPage.d.ts +6 -0
  16. package/dist/components/MixIdCallbackPage.d.ts.map +1 -0
  17. package/dist/components/MixIdCallbackPage.js +38 -0
  18. package/dist/components/MixIdConnection.d.ts +18 -0
  19. package/dist/components/MixIdConnection.d.ts.map +1 -0
  20. package/dist/components/MixIdConnection.js +197 -0
  21. package/dist/components/index.d.ts +5 -0
  22. package/dist/components/index.d.ts.map +1 -0
  23. package/dist/components/index.js +2 -0
  24. package/dist/hooks/index.d.ts +5 -0
  25. package/dist/hooks/index.d.ts.map +1 -0
  26. package/dist/hooks/index.js +4 -0
  27. package/dist/hooks/useMixIdSession.d.ts +19 -0
  28. package/dist/hooks/useMixIdSession.d.ts.map +1 -0
  29. package/dist/hooks/useMixIdSession.js +124 -0
  30. package/dist/hooks/useMixIdStatus.d.ts +9 -0
  31. package/dist/hooks/useMixIdStatus.d.ts.map +1 -0
  32. package/dist/hooks/useMixIdStatus.js +81 -0
  33. package/dist/hooks/useMixIdSync.d.ts +16 -0
  34. package/dist/hooks/useMixIdSync.d.ts.map +1 -0
  35. package/dist/hooks/useMixIdSync.js +263 -0
  36. package/dist/hooks/useNotifications.d.ts +17 -0
  37. package/dist/hooks/useNotifications.d.ts.map +1 -0
  38. package/dist/hooks/useNotifications.js +144 -0
  39. package/dist/index.d.ts +5 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +8 -0
  42. package/dist/ui/Button.d.ts +5 -0
  43. package/dist/ui/Button.d.ts.map +1 -0
  44. package/dist/ui/Button.js +7 -0
  45. package/dist/ui/Card.d.ts +5 -0
  46. package/dist/ui/Card.d.ts.map +1 -0
  47. package/dist/ui/Card.js +7 -0
  48. package/dist/ui/index.d.ts +3 -0
  49. package/dist/ui/index.d.ts.map +1 -0
  50. package/dist/ui/index.js +2 -0
  51. package/package.json +69 -0
@@ -0,0 +1,201 @@
1
+ import { mixIdApi } from './mixIdApi';
2
+ class WebSocketClient {
3
+ constructor() {
4
+ Object.defineProperty(this, "ws", {
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true,
8
+ value: null
9
+ });
10
+ Object.defineProperty(this, "reconnectAttempts", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: 0
15
+ });
16
+ Object.defineProperty(this, "maxReconnectAttempts", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: 10
21
+ });
22
+ Object.defineProperty(this, "reconnectDelay", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: 1000
27
+ });
28
+ Object.defineProperty(this, "isConnecting", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: false
33
+ });
34
+ Object.defineProperty(this, "eventHandlers", {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: new Map()
39
+ });
40
+ Object.defineProperty(this, "messageQueue", {
41
+ enumerable: true,
42
+ configurable: true,
43
+ writable: true,
44
+ value: []
45
+ });
46
+ Object.defineProperty(this, "isOnline", {
47
+ enumerable: true,
48
+ configurable: true,
49
+ writable: true,
50
+ value: typeof navigator !== 'undefined' ? navigator.onLine : true
51
+ });
52
+ if (typeof window !== 'undefined') {
53
+ // Listen to online/offline events
54
+ window.addEventListener('online', () => {
55
+ this.isOnline = true;
56
+ if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
57
+ this.connect();
58
+ }
59
+ });
60
+ window.addEventListener('offline', () => {
61
+ this.isOnline = false;
62
+ });
63
+ }
64
+ }
65
+ connect() {
66
+ if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
67
+ return;
68
+ }
69
+ const config = mixIdApi.getConfig();
70
+ if (!config || !config.accessToken) {
71
+ return;
72
+ }
73
+ this.isConnecting = true;
74
+ try {
75
+ const apiBase = config.apiBase || (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
76
+ ? (import.meta.env?.VITE_MIX_ID_API_BASE || 'http://localhost:3000/api')
77
+ : 'http://localhost:3000/api';
78
+ const wsUrl = apiBase.replace(/^http/, 'ws').replace(/\/api$/, '') + '/ws';
79
+ const ws = new WebSocket(`${wsUrl}?token=${config.accessToken}`);
80
+ ws.onopen = () => {
81
+ this.ws = ws;
82
+ this.isConnecting = false;
83
+ this.reconnectAttempts = 0;
84
+ console.log('WebSocket connected');
85
+ // Dispatch custom event for connection status update
86
+ if (typeof window !== 'undefined') {
87
+ window.dispatchEvent(new Event('mixid-ws-status-changed'));
88
+ }
89
+ // Send queued messages
90
+ this.flushMessageQueue();
91
+ };
92
+ ws.onmessage = (event) => {
93
+ try {
94
+ const message = JSON.parse(event.data);
95
+ this.handleMessage(message);
96
+ }
97
+ catch (error) {
98
+ console.error('Error parsing WebSocket message:', error);
99
+ }
100
+ };
101
+ ws.onerror = (error) => {
102
+ console.error('WebSocket error:', error);
103
+ this.isConnecting = false;
104
+ };
105
+ ws.onclose = () => {
106
+ this.ws = null;
107
+ this.isConnecting = false;
108
+ // Dispatch custom event for connection status update
109
+ if (typeof window !== 'undefined') {
110
+ window.dispatchEvent(new Event('mixid-ws-status-changed'));
111
+ }
112
+ this.attemptReconnect();
113
+ };
114
+ }
115
+ catch (error) {
116
+ console.error('WebSocket connection error:', error);
117
+ this.isConnecting = false;
118
+ this.attemptReconnect();
119
+ }
120
+ }
121
+ attemptReconnect() {
122
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
123
+ console.log('Max reconnection attempts reached');
124
+ return;
125
+ }
126
+ if (!this.isOnline) {
127
+ // Wait for online event
128
+ return;
129
+ }
130
+ this.reconnectAttempts++;
131
+ const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000);
132
+ setTimeout(() => {
133
+ if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
134
+ this.connect();
135
+ }
136
+ }, delay);
137
+ }
138
+ handleMessage(message) {
139
+ // Handle ping/pong
140
+ if (message.type === 'ping') {
141
+ this.send({ type: 'pong' });
142
+ return;
143
+ }
144
+ // Call registered event handlers
145
+ const handlers = this.eventHandlers.get(message.type);
146
+ if (handlers) {
147
+ handlers.forEach((handler) => handler(message));
148
+ }
149
+ // Also call wildcard handlers
150
+ const wildcardHandlers = this.eventHandlers.get('*');
151
+ if (wildcardHandlers) {
152
+ wildcardHandlers.forEach((handler) => handler(message));
153
+ }
154
+ }
155
+ send(message) {
156
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
157
+ this.ws.send(JSON.stringify(message));
158
+ }
159
+ else {
160
+ // Queue message for later
161
+ this.messageQueue.push(message);
162
+ }
163
+ }
164
+ flushMessageQueue() {
165
+ while (this.messageQueue.length > 0) {
166
+ const message = this.messageQueue.shift();
167
+ if (message) {
168
+ this.send(message);
169
+ }
170
+ }
171
+ }
172
+ on(eventType, handler) {
173
+ if (!this.eventHandlers.has(eventType)) {
174
+ this.eventHandlers.set(eventType, new Set());
175
+ }
176
+ this.eventHandlers.get(eventType).add(handler);
177
+ }
178
+ off(eventType, handler) {
179
+ const handlers = this.eventHandlers.get(eventType);
180
+ if (handlers) {
181
+ handlers.delete(handler);
182
+ }
183
+ }
184
+ disconnect() {
185
+ if (this.ws) {
186
+ this.ws.close();
187
+ this.ws = null;
188
+ }
189
+ this.eventHandlers.clear();
190
+ this.messageQueue = [];
191
+ this.reconnectAttempts = 0;
192
+ // Dispatch custom event for connection status update
193
+ if (typeof window !== 'undefined') {
194
+ window.dispatchEvent(new Event('mixid-ws-status-changed'));
195
+ }
196
+ }
197
+ isConnected() {
198
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
199
+ }
200
+ }
201
+ export const wsClient = new WebSocketClient();
@@ -0,0 +1,6 @@
1
+ export interface MixIdCallbackPageProps {
2
+ onCallback?: (code: string, state: string | null) => void;
3
+ redirectTo?: string;
4
+ }
5
+ export default function MixIdCallbackPage({ onCallback, redirectTo }?: MixIdCallbackPageProps): import("react/jsx-runtime").JSX.Element;
6
+ //# sourceMappingURL=MixIdCallbackPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MixIdCallbackPage.d.ts","sourceRoot":"","sources":["../../src/components/MixIdCallbackPage.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,UAAU,EAAE,UAAwB,EAAE,GAAE,sBAA2B,2CA2C9G"}
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from 'react';
3
+ import { Center, Loader, Text, Stack } from '@mantine/core';
4
+ export default function MixIdCallbackPage({ onCallback, redirectTo = '/settings' } = {}) {
5
+ useEffect(() => {
6
+ if (typeof window === 'undefined')
7
+ return;
8
+ const urlParams = new URLSearchParams(window.location.search);
9
+ const code = urlParams.get('code');
10
+ const state = urlParams.get('state');
11
+ if (code) {
12
+ // Send message to parent window
13
+ if (window.opener) {
14
+ window.opener.postMessage({
15
+ type: 'mixid-oauth-callback',
16
+ code,
17
+ state,
18
+ }, window.location.origin);
19
+ window.close();
20
+ }
21
+ else {
22
+ // If no opener, call callback or redirect
23
+ if (onCallback) {
24
+ onCallback(code, state);
25
+ }
26
+ else if (redirectTo && typeof window !== 'undefined') {
27
+ window.location.href = redirectTo;
28
+ }
29
+ }
30
+ }
31
+ else {
32
+ if (redirectTo && typeof window !== 'undefined') {
33
+ window.location.href = redirectTo;
34
+ }
35
+ }
36
+ }, [onCallback, redirectTo]);
37
+ return (_jsx(Center, { h: "100vh", children: _jsxs(Stack, { align: "center", gap: "md", children: [_jsx(Loader, {}), _jsx(Text, { children: "\u041E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0430 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438 MIX ID..." })] }) }));
38
+ }
@@ -0,0 +1,18 @@
1
+ export interface MixIdConnectionProps {
2
+ onConnected?: () => void;
3
+ onDisconnected?: () => void;
4
+ showSyncSettings?: boolean;
5
+ showSyncData?: boolean;
6
+ apiBase?: string;
7
+ clientId?: string;
8
+ clientSecret?: string;
9
+ notifications?: {
10
+ show: (options: {
11
+ title: string;
12
+ message: string;
13
+ color?: string;
14
+ }) => void;
15
+ };
16
+ }
17
+ export default function MixIdConnection({ onConnected, onDisconnected, showSyncSettings, showSyncData, apiBase, clientId, clientSecret, notifications, }: MixIdConnectionProps): import("react/jsx-runtime").JSX.Element;
18
+ //# sourceMappingURL=MixIdConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MixIdConnection.d.ts","sourceRoot":"","sources":["../../src/components/MixIdConnection.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE;QACd,IAAI,EAAE,CAAC,OAAO,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAA;KAC5E,CAAA;CACF;AAED,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EACtC,WAAW,EACX,cAAc,EACd,gBAAuB,EACvB,YAAmB,EACnB,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,aAAa,GACd,EAAE,oBAAoB,2CAwStB"}
@@ -0,0 +1,197 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Paper, Group, Button, Text, Badge, Modal, Stack, Switch, Alert, Loader, } from '@mantine/core';
4
+ import { IconPlug, IconSettings, IconLogout, IconX } from '@tabler/icons-react';
5
+ import { useDisclosure } from '@mantine/hooks';
6
+ import { mixIdApi } from '../api/mixIdApi';
7
+ import { useMixIdStatus } from '../hooks/useMixIdStatus';
8
+ export default function MixIdConnection({ onConnected, onDisconnected, showSyncSettings = true, showSyncData = true, apiBase, clientId, clientSecret, notifications, }) {
9
+ const { isConnected, syncStatus, hasConfig } = useMixIdStatus();
10
+ const [loading, setLoading] = useState(true);
11
+ const [syncStatusData, setSyncStatusData] = useState(null);
12
+ const [settingsModalOpened, { open: openSettings, close: closeSettings }] = useDisclosure(false);
13
+ const [syncSettings, setSyncSettings] = useState(false);
14
+ const [syncData, setSyncData] = useState(false);
15
+ useEffect(() => {
16
+ checkConnection();
17
+ }, []);
18
+ const checkConnection = async () => {
19
+ try {
20
+ const config = mixIdApi.getConfig();
21
+ if (!config || !config.accessToken) {
22
+ setSyncStatusData(null);
23
+ return;
24
+ }
25
+ const status = await mixIdApi.getSyncStatus();
26
+ setSyncStatusData(status);
27
+ setSyncSettings(status.syncSettings);
28
+ setSyncData(status.syncData);
29
+ }
30
+ catch (error) {
31
+ setSyncStatusData(null);
32
+ }
33
+ finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+ const handleConnect = async () => {
38
+ try {
39
+ // Get config from props or environment
40
+ const finalApiBase = apiBase ||
41
+ (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
42
+ ? (import.meta.env?.VITE_MIX_ID_API_BASE || 'http://localhost:3000/api')
43
+ : 'http://localhost:3000/api';
44
+ const finalClientId = clientId ||
45
+ (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_CLIENT_ID)
46
+ ? (import.meta.env?.VITE_MIX_ID_CLIENT_ID || '')
47
+ : '';
48
+ const finalClientSecret = clientSecret ||
49
+ (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_CLIENT_SECRET)
50
+ ? (import.meta.env?.VITE_MIX_ID_CLIENT_SECRET || '')
51
+ : '';
52
+ if (!finalClientId || !finalClientSecret) {
53
+ const message = 'MIX ID не настроен. Укажите VITE_MIX_ID_CLIENT_ID и VITE_MIX_ID_CLIENT_SECRET';
54
+ if (notifications) {
55
+ notifications.show({
56
+ title: 'Ошибка',
57
+ message,
58
+ color: 'red',
59
+ });
60
+ }
61
+ else {
62
+ alert(message);
63
+ }
64
+ return;
65
+ }
66
+ mixIdApi.setConfig({
67
+ apiBase: finalApiBase || 'http://localhost:3000/api',
68
+ clientId: finalClientId,
69
+ clientSecret: finalClientSecret
70
+ });
71
+ // Initiate OAuth flow
72
+ const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/mixid-callback' : '';
73
+ const { authorizationUrl, code } = await mixIdApi.initiateOAuth(redirectUri);
74
+ // Open OAuth window
75
+ if (typeof window === 'undefined')
76
+ return;
77
+ const width = 600;
78
+ const height = 700;
79
+ const left = window.screenX + (window.outerWidth - width) / 2;
80
+ const top = window.screenY + (window.outerHeight - height) / 2;
81
+ const oauthWindow = window.open(authorizationUrl, 'MIX ID Authorization', `width=${width},height=${height},left=${left},top=${top}`);
82
+ // Listen for OAuth callback
83
+ const handleMessage = async (event) => {
84
+ if (event.origin !== window.location.origin)
85
+ return;
86
+ if (event.data.type === 'mixid-oauth-callback') {
87
+ window.removeEventListener('message', handleMessage);
88
+ oauthWindow?.close();
89
+ try {
90
+ const { code: callbackCode } = event.data;
91
+ await mixIdApi.exchangeCodeForToken(callbackCode || code, redirectUri);
92
+ // Dispatch event to trigger WebSocket connection and status update
93
+ window.dispatchEvent(new Event('mixid-config-changed'));
94
+ await checkConnection();
95
+ if (notifications) {
96
+ notifications.show({
97
+ title: 'Успешно',
98
+ message: 'MIX ID подключен',
99
+ color: 'green',
100
+ });
101
+ }
102
+ onConnected?.();
103
+ openSettings();
104
+ }
105
+ catch (error) {
106
+ const message = error instanceof Error ? error.message : 'Не удалось подключить MIX ID';
107
+ if (notifications) {
108
+ notifications.show({
109
+ title: 'Ошибка',
110
+ message,
111
+ color: 'red',
112
+ });
113
+ }
114
+ else {
115
+ alert(message);
116
+ }
117
+ }
118
+ }
119
+ };
120
+ window.addEventListener('message', handleMessage);
121
+ // Fallback: check if window was closed manually
122
+ const checkClosed = setInterval(() => {
123
+ if (oauthWindow?.closed) {
124
+ clearInterval(checkClosed);
125
+ window.removeEventListener('message', handleMessage);
126
+ }
127
+ }, 1000);
128
+ }
129
+ catch (error) {
130
+ const message = error instanceof Error ? error.message : 'Не удалось инициировать подключение';
131
+ if (notifications) {
132
+ notifications.show({
133
+ title: 'Ошибка',
134
+ message,
135
+ color: 'red',
136
+ });
137
+ }
138
+ else {
139
+ alert(message);
140
+ }
141
+ }
142
+ };
143
+ const handleDisconnect = async () => {
144
+ if (typeof window === 'undefined')
145
+ return;
146
+ if (!confirm('Вы уверены, что хотите отключить MIX ID?'))
147
+ return;
148
+ mixIdApi.clearConfig();
149
+ // Dispatch event to trigger WebSocket disconnection and status update
150
+ window.dispatchEvent(new Event('mixid-config-changed'));
151
+ setSyncStatusData(null);
152
+ if (notifications) {
153
+ notifications.show({
154
+ title: 'Успешно',
155
+ message: 'MIX ID отключен',
156
+ color: 'blue',
157
+ });
158
+ }
159
+ onDisconnected?.();
160
+ };
161
+ const handleSaveSettings = async () => {
162
+ try {
163
+ await mixIdApi.updateSyncPreferences(syncSettings, syncData);
164
+ if (notifications) {
165
+ notifications.show({
166
+ title: 'Успешно',
167
+ message: 'Настройки синхронизации сохранены',
168
+ color: 'green',
169
+ });
170
+ }
171
+ closeSettings();
172
+ await checkConnection();
173
+ }
174
+ catch (error) {
175
+ const message = error instanceof Error ? error.message : 'Не удалось сохранить настройки';
176
+ if (notifications) {
177
+ notifications.show({
178
+ title: 'Ошибка',
179
+ message,
180
+ color: 'red',
181
+ });
182
+ }
183
+ else {
184
+ alert(message);
185
+ }
186
+ }
187
+ };
188
+ if (loading) {
189
+ return (_jsx(Paper, { p: "md", withBorder: true, children: _jsx(Loader, { size: "sm" }) }));
190
+ }
191
+ return (_jsxs(_Fragment, { children: [_jsx(Paper, { p: "md", withBorder: true, children: _jsxs(Group, { justify: "space-between", children: [_jsxs(Group, { children: [_jsx(IconPlug, { size: 24 }), _jsxs("div", { children: [_jsx(Text, { fw: 500, children: "MIX ID" }), _jsx(Text, { size: "sm", c: "dimmed", children: "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044F \u0434\u0430\u043D\u043D\u044B\u0445 \u0447\u0435\u0440\u0435\u0437 Zorin Projects" })] })] }), isConnected ? (_jsxs(Group, { children: [syncStatusData && (_jsxs(Group, { gap: "xs", children: [showSyncSettings && (_jsx(Badge, { color: syncStatusData.syncSettings ? 'green' : 'gray', children: "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438" })), showSyncData && (_jsx(Badge, { color: syncStatusData.syncData ? 'green' : 'gray', children: "\u0414\u0430\u043D\u043D\u044B\u0435" }))] })), _jsx(Button, { leftSection: _jsx(IconSettings, { size: 16 }), variant: "light", onClick: openSettings, children: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B" }), _jsx(Button, { leftSection: _jsx(IconLogout, { size: 16 }), variant: "subtle", onClick: handleDisconnect, children: "\u0412\u044B\u0439\u0442\u0438" })] })) : (_jsx(Button, { leftSection: _jsx(IconPlug, { size: 16 }), onClick: handleConnect, children: "\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C" }))] }) }), _jsx(Modal, { opened: settingsModalOpened, onClose: closeSettings, title: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u0438 MIX ID", children: _jsxs(Stack, { gap: "md", children: [_jsx(Alert, { children: _jsx(Text, { size: "sm", children: "MIX ID \u043F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0430\u0448\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0438 \u0434\u0430\u043D\u043D\u044B\u0435 \u043C\u0435\u0436\u0434\u0443 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0430\u043C\u0438. \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0432\u044B\u0431\u0440\u0430\u0442\u044C, \u0447\u0442\u043E \u0438\u043C\u0435\u043D\u043D\u043E \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C." }) }), showSyncSettings && (_jsx(Switch, { label: "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438", description: "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0431\u0443\u0434\u0443\u0442 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C", checked: syncSettings, onChange: (e) => {
192
+ setSyncSettings(e.currentTarget.checked);
193
+ if (!e.currentTarget.checked) {
194
+ setSyncData(false);
195
+ }
196
+ } })), showSyncData && (_jsx(Switch, { label: "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435", description: "\u0414\u0430\u043D\u043D\u044B\u0435 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0431\u0443\u0434\u0443\u0442 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C", checked: syncData, onChange: (e) => setSyncData(e.currentTarget.checked), disabled: showSyncSettings && !syncSettings })), showSyncSettings && !syncSettings && (_jsx(Alert, { color: "yellow", icon: _jsx(IconX, { size: 16 }), children: "\u0414\u043B\u044F \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u0438 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044E \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043A" })), syncStatusData?.lastSyncAt && (_jsxs(Text, { size: "sm", c: "dimmed", children: ["\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044F: ", new Date(syncStatusData.lastSyncAt).toLocaleString('ru-RU')] })), _jsxs(Group, { justify: "flex-end", children: [_jsx(Button, { variant: "subtle", onClick: closeSettings, children: "\u041E\u0442\u043C\u0435\u043D\u0430" }), _jsx(Button, { onClick: handleSaveSettings, children: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C" })] })] }) })] }));
197
+ }
@@ -0,0 +1,5 @@
1
+ export { default as MixIdConnection } from './MixIdConnection';
2
+ export { default as MixIdCallbackPage } from './MixIdCallbackPage';
3
+ export type { MixIdConnectionProps } from './MixIdConnection';
4
+ export type { MixIdCallbackPageProps } from './MixIdCallbackPage';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAClE,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { default as MixIdConnection } from './MixIdConnection';
2
+ export { default as MixIdCallbackPage } from './MixIdCallbackPage';
@@ -0,0 +1,5 @@
1
+ export * from './useMixIdStatus';
2
+ export * from './useMixIdSync';
3
+ export * from './useNotifications';
4
+ export * from './useMixIdSession';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA"}
@@ -0,0 +1,4 @@
1
+ export * from './useMixIdStatus';
2
+ export * from './useMixIdSync';
3
+ export * from './useNotifications';
4
+ export * from './useMixIdSession';
@@ -0,0 +1,19 @@
1
+ export interface Session {
2
+ id: string;
3
+ deviceInfo: any;
4
+ lastActivityAt: string;
5
+ createdAt: string;
6
+ }
7
+ export interface UseMixIdSessionOptions {
8
+ onSessionDeleted?: () => void;
9
+ onSessionExpired?: () => void;
10
+ onSessionInvalid?: () => void;
11
+ heartbeatInterval?: number;
12
+ checkSessionOnMount?: boolean;
13
+ }
14
+ export declare function useMixIdSession(options?: UseMixIdSessionOptions): {
15
+ sendHeartbeat: () => Promise<void>;
16
+ checkSession: () => Promise<void>;
17
+ currentSessionId: string | null;
18
+ };
19
+ //# sourceMappingURL=useMixIdSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMixIdSession.d.ts","sourceRoot":"","sources":["../../src/hooks/useMixIdSession.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,GAAG,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B;;;;EAwInE"}
@@ -0,0 +1,124 @@
1
+ import { useEffect, useCallback, useState } from 'react';
2
+ import { mixIdApi } from '../api/mixIdApi';
3
+ import { wsClient } from '../api/websocket';
4
+ export function useMixIdSession(options = {}) {
5
+ const { onSessionDeleted, onSessionExpired, onSessionInvalid, heartbeatInterval = 30 * 1000, // 30 seconds
6
+ checkSessionOnMount = true, } = options;
7
+ const [currentSessionId, setCurrentSessionId] = useState(null);
8
+ const sendHeartbeat = useCallback(async () => {
9
+ try {
10
+ const config = mixIdApi.getConfig();
11
+ if (!config || !config.accessToken) {
12
+ return;
13
+ }
14
+ const result = await mixIdApi.heartbeat({
15
+ platform: typeof navigator !== 'undefined' ? navigator.platform : 'unknown',
16
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
17
+ timestamp: new Date().toISOString(),
18
+ });
19
+ // Store session ID if returned
20
+ if (result && result.sessionId) {
21
+ setCurrentSessionId(result.sessionId);
22
+ }
23
+ }
24
+ catch (error) {
25
+ console.error('Heartbeat failed:', error);
26
+ // If heartbeat fails with 401/403, session might be deleted or expired
27
+ if (error instanceof Error) {
28
+ if (error.message.includes('401') || error.message.includes('403')) {
29
+ mixIdApi.clearConfig();
30
+ wsClient.disconnect();
31
+ onSessionExpired?.();
32
+ }
33
+ else if (error.message.includes('404')) {
34
+ // Session not found - might have been deleted
35
+ mixIdApi.clearConfig();
36
+ wsClient.disconnect();
37
+ onSessionDeleted?.();
38
+ }
39
+ else {
40
+ onSessionInvalid?.();
41
+ }
42
+ }
43
+ }
44
+ }, [onSessionDeleted, onSessionExpired, onSessionInvalid]);
45
+ const checkSession = useCallback(async () => {
46
+ try {
47
+ const config = mixIdApi.getConfig();
48
+ if (!config || !config.accessToken) {
49
+ return;
50
+ }
51
+ // Try to get sessions to verify current session exists
52
+ const sessions = await mixIdApi.getSessions();
53
+ // If we have a current session ID, check if it still exists
54
+ if (currentSessionId) {
55
+ const sessionExists = sessions.some(s => s.id === currentSessionId);
56
+ if (!sessionExists) {
57
+ // Session was deleted
58
+ mixIdApi.clearConfig();
59
+ wsClient.disconnect();
60
+ onSessionDeleted?.();
61
+ }
62
+ }
63
+ }
64
+ catch (error) {
65
+ console.error('Failed to check session:', error);
66
+ if (error instanceof Error && (error.message.includes('401') || error.message.includes('403'))) {
67
+ mixIdApi.clearConfig();
68
+ wsClient.disconnect();
69
+ onSessionExpired?.();
70
+ }
71
+ }
72
+ }, [currentSessionId, onSessionDeleted, onSessionExpired]);
73
+ useEffect(() => {
74
+ const config = mixIdApi.getConfig();
75
+ if (!config || !config.accessToken) {
76
+ return;
77
+ }
78
+ // Check session on mount if enabled
79
+ if (checkSessionOnMount) {
80
+ checkSession();
81
+ }
82
+ // Set up WebSocket handlers for session events
83
+ const handleSessionDeleted = (message) => {
84
+ // If message contains sessionId, check if it's our session
85
+ if (message.sessionId) {
86
+ if (currentSessionId && message.sessionId === currentSessionId) {
87
+ // Our session was deleted
88
+ mixIdApi.clearConfig();
89
+ wsClient.disconnect();
90
+ onSessionDeleted?.();
91
+ }
92
+ }
93
+ else {
94
+ // Session deletion event without specific ID - might be ours
95
+ // Check by making a request
96
+ checkSession();
97
+ }
98
+ };
99
+ const handleSessionExpired = () => {
100
+ mixIdApi.clearConfig();
101
+ wsClient.disconnect();
102
+ onSessionExpired?.();
103
+ };
104
+ wsClient.on('session:deleted', handleSessionDeleted);
105
+ wsClient.on('session:expired', handleSessionExpired);
106
+ // Start heartbeat
107
+ const heartbeatIntervalId = setInterval(sendHeartbeat, heartbeatInterval);
108
+ // Send initial heartbeat
109
+ sendHeartbeat();
110
+ // Check session periodically (every 5 minutes)
111
+ const sessionCheckInterval = setInterval(checkSession, 5 * 60 * 1000);
112
+ return () => {
113
+ wsClient.off('session:deleted', handleSessionDeleted);
114
+ wsClient.off('session:expired', handleSessionExpired);
115
+ clearInterval(heartbeatIntervalId);
116
+ clearInterval(sessionCheckInterval);
117
+ };
118
+ }, [sendHeartbeat, checkSession, onSessionDeleted, onSessionExpired, heartbeatInterval, checkSessionOnMount, currentSessionId]);
119
+ return {
120
+ sendHeartbeat,
121
+ checkSession,
122
+ currentSessionId,
123
+ };
124
+ }
@@ -0,0 +1,9 @@
1
+ export type MixIdSyncStatus = 'connected-ws' | 'connected-rest' | 'disconnected' | 'checking';
2
+ export interface UseMixIdStatusReturn {
3
+ isConnected: boolean;
4
+ syncStatus: MixIdSyncStatus;
5
+ hasConfig: boolean;
6
+ refresh: () => void;
7
+ }
8
+ export declare function useMixIdStatus(): UseMixIdStatusReturn;
9
+ //# sourceMappingURL=useMixIdStatus.d.ts.map