SHiNE-server/SHiNE-browser-plugin-wallet/popup.js

367 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { formatPairingShortCode } from './js/lib/device-pairing.js';
const els = {
serverLoginInfo: document.querySelector('#server-login-info'),
serverAddress: document.querySelector('#server-address'),
loginInput: document.querySelector('#login-input'),
usePassword: document.querySelector('#use-password'),
passwordField: document.querySelector('#password-field'),
passwordInput: document.querySelector('#password-input'),
startBtn: document.querySelector('#start-btn'),
connectCard: document.querySelector('#connect-card'),
pairingCard: document.querySelector('#pairing-card'),
shortCode: document.querySelector('#short-code'),
pairingHint: document.querySelector('#pairing-hint'),
pairingExpire: document.querySelector('#pairing-expire'),
cancelBtn: document.querySelector('#cancel-btn'),
status: document.querySelector('#status'),
sessionCard: document.querySelector('#session-card'),
sessionLogin: document.querySelector('#session-login'),
sessionId: document.querySelector('#session-id'),
sessionType: document.querySelector('#session-type'),
deviceKeyShort: document.querySelector('#device-key-short'),
resumeBtn: document.querySelector('#resume-btn'),
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
disconnectBtn: document.querySelector('#disconnect-btn'),
walletCard: document.querySelector('#wallet-card'),
deviceSelect: document.querySelector('#device-select'),
homeserverList: document.querySelector('#homeserver-list'),
requestWalletBtn: document.querySelector('#request-wallet-btn'),
walletResultCard: document.querySelector('#wallet-result-card'),
walletType: document.querySelector('#wallet-type'),
walletPubkey: document.querySelector('#wallet-pubkey'),
walletVerify: document.querySelector('#wallet-verify'),
copyWalletBtn: document.querySelector('#copy-wallet-btn'),
connectionPill: document.querySelector('#connection-pill'),
};
let state = {
settings: {
serverLogin: 'shineupme',
serverHttp: 'https://shineup.me',
login: '',
},
pairing: {
active: false,
expiresAtMs: 0,
shortCode: '',
},
session: null,
walletProfile: null,
signing: {
selectedDeviceName: '',
},
currentWallet: null,
status: {
text: '',
kind: 'info',
},
};
let refreshTimer = 0;
let saveSettingsTimer = 0;
function setStatus(message, kind = 'info') {
els.status.textContent = String(message || '');
els.status.className = `status ${kind === 'error' ? 'error' : 'info'}`;
els.status.classList.toggle('hidden', !message);
}
function setConnectedPill(connected) {
els.connectionPill.textContent = connected ? 'подключено' : 'не подключено';
els.connectionPill.className = connected ? 'pill pill-online' : 'pill pill-offline';
}
function formatRemaining(ms) {
const safe = Math.max(0, Math.floor(Number(ms || 0) / 1000));
const minutes = Math.floor(safe / 60);
const seconds = safe % 60;
return `${minutes} мин ${seconds} сек`;
}
function shortKey(value, size = 10) {
const raw = String(value || '').trim();
return raw ? `${raw.slice(0, size)}...` : '—';
}
function renderHomeserverList(items = []) {
els.homeserverList.innerHTML = '';
if (!items.length) {
const empty = document.createElement('p');
empty.className = 'muted small';
empty.textContent = 'В PDA пока нет опубликованных homeserver-сессий.';
els.homeserverList.append(empty);
return;
}
items.forEach((item) => {
const row = document.createElement('div');
row.className = 'summary-row device-row';
const label = document.createElement('span');
label.textContent = `${item.sessionName} (${shortKey(item.sessionPubKeyBase58, 8)})`;
const badge = document.createElement('strong');
const stateValue = String(item.onlineState || 'unknown');
badge.textContent = stateValue;
badge.className = `device-state device-state-${stateValue}`;
row.append(label, badge);
els.homeserverList.append(row);
});
}
function applyState(nextState) {
state = nextState || state;
const loginValue = String(state?.settings?.login || '');
const resolvedServerLogin = String(state?.settings?.serverLogin || '').trim();
const resolvedServerAddress = String(state?.settings?.serverHttp || '').trim();
els.serverLoginInfo.textContent = resolvedServerLogin ? `Сервер SHiNE: ${resolvedServerLogin}` : 'Сервер SHiNE: —';
els.serverAddress.textContent = resolvedServerAddress ? `Адрес: ${resolvedServerAddress}` : 'Адрес: —';
if (document.activeElement !== els.loginInput) {
els.loginInput.value = loginValue;
}
setConnectedPill(!!state?.session);
setStatus(state?.status?.text || '', state?.status?.kind || 'info');
const session = state?.session;
const walletProfile = state?.walletProfile;
const signing = state?.signing || {};
const currentWallet = state?.currentWallet || null;
els.connectCard.classList.toggle('hidden', !!session);
els.sessionCard.classList.toggle('hidden', !session);
els.walletCard.classList.toggle('hidden', !session);
if (session) {
els.sessionLogin.textContent = session.login || '—';
els.sessionId.textContent = session.sessionId || '—';
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
els.deviceKeyShort.textContent = shortKey(walletProfile?.publicKeys?.deviceKeyBase58 || '');
}
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
els.deviceSelect.innerHTML = '';
homeservers.forEach((item) => {
const option = document.createElement('option');
option.value = item.sessionName;
option.textContent = `${item.sessionName} [${item.onlineState || 'offline'}]`;
option.selected = item.sessionName === signing.selectedDeviceName;
els.deviceSelect.append(option);
});
renderHomeserverList(homeservers);
els.requestWalletBtn.disabled = !session || !signing.selectedDeviceName;
if (currentWallet?.publicKeyBase58) {
els.walletResultCard.classList.remove('hidden');
els.walletType.textContent = currentWallet.type || '—';
els.walletPubkey.textContent = currentWallet.publicKeyBase58 || '—';
els.walletVerify.textContent = currentWallet.verificationText || '—';
} else {
els.walletResultCard.classList.add('hidden');
els.walletType.textContent = '—';
els.walletPubkey.textContent = '—';
els.walletVerify.textContent = '—';
}
const pairing = state?.pairing || {};
if (pairing.active) {
els.pairingCard.classList.remove('hidden');
els.shortCode.textContent = formatPairingShortCode(String(pairing.shortCode || ''));
const leftMs = Number(pairing.expiresAtMs || 0) - Date.now();
els.pairingExpire.textContent = leftMs > 0 ? `Код действителен ещё ${formatRemaining(leftMs)}.` : 'Время ожидания истекло.';
els.startBtn.disabled = true;
} else {
els.pairingCard.classList.add('hidden');
els.shortCode.textContent = formatPairingShortCode('');
els.pairingExpire.textContent = '';
els.startBtn.disabled = false;
}
}
function normalizeError(response, fallback) {
return response?.error || fallback || 'Unknown error';
}
function sendMessage(type, payload = {}) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type, payload }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message || 'Runtime message failed'));
return;
}
if (!response?.ok) {
reject(new Error(normalizeError(response, 'Wallet operation failed')));
return;
}
if (response?.state) applyState(response.state);
resolve(response);
});
});
}
async function refreshState() {
const response = await sendMessage('wallet:getState');
applyState(response.state);
}
async function saveSettings() {
await sendMessage('wallet:saveSettings', {
login: String(els.loginInput.value || '').trim(),
});
}
async function resolveServerInfo() {
const login = String(els.loginInput.value || '').trim();
if (!login) {
await sendMessage('wallet:saveSettings', { login: '' });
return;
}
try {
await sendMessage('wallet:resolveServerInfo', { login });
} catch (error) {
setStatus(error.message || 'Не удалось определить сервер SHiNE по PDA.', 'error');
}
}
function scheduleSaveSettings() {
if (saveSettingsTimer) {
window.clearTimeout(saveSettingsTimer);
}
saveSettingsTimer = window.setTimeout(() => {
saveSettingsTimer = 0;
void saveSettings();
}, 250);
}
async function startPairing() {
const login = String(els.loginInput.value || '').trim();
if (!login) {
setStatus('Введите логин.', 'error');
return;
}
setStatus('Создаём wallet-session заявку...', 'info');
try {
await sendMessage('wallet:startPairing', {
login,
usePassword: !!els.usePassword.checked,
password: String(els.passwordInput.value || ''),
});
} catch (error) {
setStatus(error.message || 'Не удалось начать pairing.', 'error');
}
}
async function cancelPairing() {
try {
await sendMessage('wallet:cancelPairing');
} catch (error) {
setStatus(error.message || 'Не удалось отменить pairing.', 'error');
}
}
async function resumeSession() {
setStatus('Проверяем сохранённую wallet-session...', 'info');
try {
await sendMessage('wallet:resumeSession');
} catch (error) {
setStatus(error.message || 'Не удалось восстановить session.', 'error');
}
}
async function disconnectSession() {
try {
await sendMessage('wallet:disconnectSession');
} catch (error) {
setStatus(error.message || 'Не удалось удалить session.', 'error');
}
}
async function refreshDevices() {
setStatus('Обновляем trusted homeserver-устройства...', 'info');
try {
await sendMessage('wallet:refreshWalletDevices');
} catch (error) {
setStatus(error.message || 'Не удалось обновить список устройств.', 'error');
}
}
async function updateDeviceSelection() {
try {
await sendMessage('wallet:updateSigningSelection', {
selectedDeviceName: String(els.deviceSelect.value || '').trim(),
});
} catch (error) {
setStatus(error.message || 'Не удалось обновить выбор homeserver.', 'error');
}
}
async function requestCurrentWallet() {
setStatus('Запрашиваем текущий кошелёк с ESP32...', 'info');
try {
await sendMessage('wallet:requestCurrentWallet');
} catch (error) {
setStatus(error.message || 'Не удалось получить кошелёк с ESP32.', 'error');
}
}
async function copyWalletKey() {
const value = String(els.walletPubkey.textContent || '').trim();
if (!value || value === '—') return;
try {
await navigator.clipboard.writeText(value);
setStatus('Публичный ключ скопирован.', 'info');
} catch (error) {
setStatus(error.message || 'Не удалось скопировать ключ.', 'error');
}
}
function startUiRefreshLoop() {
stopUiRefreshLoop();
refreshTimer = window.setInterval(() => {
void refreshState();
}, 1000);
}
function stopUiRefreshLoop() {
if (refreshTimer) {
window.clearInterval(refreshTimer);
refreshTimer = 0;
}
}
function bindUi() {
els.usePassword.addEventListener('change', () => {
els.passwordField.classList.toggle('hidden', !els.usePassword.checked);
if (!els.usePassword.checked) {
els.passwordInput.value = '';
}
});
els.loginInput.addEventListener('input', () => { scheduleSaveSettings(); });
els.loginInput.addEventListener('change', () => {
void saveSettings();
void resolveServerInfo();
});
els.startBtn.addEventListener('click', () => { void startPairing(); });
els.cancelBtn.addEventListener('click', () => { void cancelPairing(); });
els.resumeBtn.addEventListener('click', () => { void resumeSession(); });
els.refreshDevicesBtn.addEventListener('click', () => { void refreshDevices(); });
els.disconnectBtn.addEventListener('click', () => { void disconnectSession(); });
els.deviceSelect.addEventListener('change', () => { void updateDeviceSelection(); });
els.requestWalletBtn.addEventListener('click', () => { void requestCurrentWallet(); });
els.copyWalletBtn.addEventListener('click', () => { void copyWalletKey(); });
}
async function init() {
bindUi();
await refreshState();
startUiRefreshLoop();
}
window.addEventListener('beforeunload', () => {
stopUiRefreshLoop();
if (saveSettingsTimer) {
window.clearTimeout(saveSettingsTimer);
saveSettingsTimer = 0;
}
});
void init();