**Motivations :** - Simplify user experience with single authentication flow - Remove confusing mode selection interface - Hide 4 words display (will be used later in interface) - Direct WebAuthn authentication for both new and existing pairings **Modifications :** - Replaced mode selection with single pairing interface - Added logic to detect existing credentials vs new pairing - Removed 4 words display from pairing process - Simplified HTML structure with single main interface - Updated JavaScript logic for direct WebAuthn flow **Pages affectées :** - src/pages/home/home.html - Simplified to single interface - src/pages/home/home.ts - Added direct WebAuthn flow logic - src/utils/sp-address.utils.ts - Removed 4 words display
600 lines
19 KiB
TypeScript
Executable File
600 lines
19 KiB
TypeScript
Executable File
import Routing from '../../services/modal.service';
|
|
import Services from '../../services/service';
|
|
import { addSubscription } from '../../utils/subscription.utils';
|
|
import { displayEmojis, generateCreateBtn, addressToEmoji, prepareAndSendPairingTx } from '../../utils/sp-address.utils';
|
|
import { getCorrectDOM } from '../../utils/html.utils';
|
|
// import { navigate, registerAllListeners } from '../../router'; // Unused imports
|
|
import { IframePairingComponent } from '../../components/iframe-pairing/iframe-pairing';
|
|
|
|
// Extend WindowEventMap to include custom events
|
|
declare global {
|
|
interface WindowEventMap {
|
|
'pairing-words-generated': CustomEvent;
|
|
'pairing-status-update': CustomEvent;
|
|
'pairing-success': CustomEvent;
|
|
'pairing-error': CustomEvent;
|
|
}
|
|
}
|
|
|
|
// Home page loading spinner functions
|
|
function showHomeLoadingSpinner(message: string = 'Loading...') {
|
|
// Remove existing spinner if any
|
|
hideHomeLoadingSpinner();
|
|
|
|
// Create spinner overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.id = 'home-loading-overlay';
|
|
overlay.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 9998;
|
|
backdrop-filter: blur(3px);
|
|
`;
|
|
|
|
// Create spinner content
|
|
const spinnerContent = document.createElement('div');
|
|
spinnerContent.style.cssText = `
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
text-align: center;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
max-width: 350px;
|
|
width: 90%;
|
|
`;
|
|
|
|
// Create spinner
|
|
const spinner = document.createElement('div');
|
|
spinner.style.cssText = `
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 3px solid #f3f3f3;
|
|
border-top: 3px solid #3a506b;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 15px auto;
|
|
`;
|
|
|
|
// Create message
|
|
const messageEl = document.createElement('div');
|
|
messageEl.textContent = message;
|
|
messageEl.style.cssText = `
|
|
font-size: 14px;
|
|
color: #3a506b;
|
|
font-weight: 500;
|
|
`;
|
|
|
|
// Add CSS animation if not already present
|
|
if (!document.getElementById('home-spinner-styles')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'home-spinner-styles';
|
|
style.textContent = `
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
// Assemble spinner
|
|
spinnerContent.appendChild(spinner);
|
|
spinnerContent.appendChild(messageEl);
|
|
overlay.appendChild(spinnerContent);
|
|
|
|
// Add to document
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
function hideHomeLoadingSpinner() {
|
|
const overlay = document.getElementById('home-loading-overlay');
|
|
if (overlay) {
|
|
overlay.remove();
|
|
}
|
|
}
|
|
|
|
export async function initHomePage(): Promise<void> {
|
|
console.log('INIT-HOME');
|
|
|
|
// Show loading spinner during home page initialization
|
|
showHomeLoadingSpinner('Initializing pairing interface...');
|
|
|
|
// Initialize iframe pairing, content menu, and communication only if in iframe
|
|
if (window.parent !== window) {
|
|
initIframePairing();
|
|
initContentMenu();
|
|
initIframeCommunication();
|
|
}
|
|
|
|
// Set up iframe pairing button listeners
|
|
setupIframePairingButtons();
|
|
|
|
// Set up main pairing interface
|
|
setupMainPairing();
|
|
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
container.querySelectorAll('.tab').forEach(tab => {
|
|
addSubscription(tab, 'click', () => {
|
|
container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
|
|
container
|
|
.querySelectorAll('.tab-content')
|
|
.forEach(content => content.classList.remove('active'));
|
|
container
|
|
.querySelector(`#${tab.getAttribute('data-tab') as string}`)
|
|
?.classList.add('active');
|
|
});
|
|
});
|
|
|
|
try {
|
|
console.log('🔧 Getting services instance...');
|
|
const service = await Services.getInstance();
|
|
console.log('🔧 Getting device address...');
|
|
const spAddress = await service.getDeviceAddress();
|
|
console.log('🔧 Generating create button...');
|
|
generateCreateBtn();
|
|
console.log('🔧 Displaying emojis...');
|
|
displayEmojis(spAddress);
|
|
|
|
// Hide loading spinner after initialization
|
|
console.log('🔧 Hiding loading spinner...');
|
|
hideHomeLoadingSpinner();
|
|
console.log('✅ Home page initialization completed');
|
|
} catch (error) {
|
|
console.error('❌ Error initializing home page:', error);
|
|
hideHomeLoadingSpinner();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
//// Modal
|
|
export async function openModal(myAddress: string, receiverAddress: string) {
|
|
const router = await Routing.getInstance();
|
|
router.openLoginModal(myAddress, receiverAddress);
|
|
}
|
|
|
|
// const service = await Services.getInstance()
|
|
// service.setNotification()
|
|
|
|
function scanDevice() {
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
const scannerImg = container.querySelector('#scanner') as HTMLElement;
|
|
if (scannerImg) scannerImg.style.display = 'none';
|
|
const scannerQrCode = container.querySelector('.qr-code-scanner') as HTMLElement;
|
|
if (scannerQrCode) scannerQrCode.style.display = 'block';
|
|
const scanButton = container?.querySelector('#scan-btn') as HTMLElement;
|
|
if (scanButton) scanButton.style.display = 'none';
|
|
// QR scanner functionality removed
|
|
}
|
|
|
|
async function populateMemberSelect() {
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
|
|
|
|
if (!memberSelect) {
|
|
console.error('Could not find memberSelect element');
|
|
return;
|
|
}
|
|
|
|
const service = await Services.getInstance();
|
|
const members = await service.getAllMembersSorted();
|
|
|
|
for (const [processId, member] of Object.entries(members)) {
|
|
// Use member variable
|
|
console.log('Processing member:', member);
|
|
const process = await service.getProcess(processId);
|
|
let memberPublicName;
|
|
|
|
if (process) {
|
|
const publicMemberData = service.getPublicData(process);
|
|
if (publicMemberData) {
|
|
const extractedName = publicMemberData['memberPublicName'];
|
|
if (extractedName !== undefined && extractedName !== null) {
|
|
memberPublicName = extractedName;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!memberPublicName) {
|
|
memberPublicName = 'Unnamed Member';
|
|
}
|
|
|
|
// Récupérer les emojis pour ce processId
|
|
const emojis = await addressToEmoji(processId);
|
|
|
|
const option = document.createElement('option');
|
|
option.value = processId;
|
|
option.textContent = `${memberPublicName} (${emojis})`;
|
|
memberSelect.appendChild(option);
|
|
}
|
|
}
|
|
|
|
(window as any).populateMemberSelect = populateMemberSelect;
|
|
|
|
(window as any).scanDevice = scanDevice;
|
|
|
|
// Initialize iframe pairing component
|
|
let iframePairing: IframePairingComponent | null = null;
|
|
|
|
export function initIframePairing() {
|
|
if (!iframePairing) {
|
|
iframePairing = new IframePairingComponent();
|
|
iframePairing.createHiddenIframe();
|
|
|
|
// Listen for pairing events
|
|
window.addEventListener('pairing-words-generated', (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
console.log('✅ 4 words generated via iframe:', customEvent.detail.words);
|
|
// Update the UI with the generated words
|
|
const creatorWordsElement = document.querySelector('#creator-words');
|
|
if (creatorWordsElement) {
|
|
creatorWordsElement.textContent = customEvent.detail.words;
|
|
creatorWordsElement.className = 'words-content active';
|
|
}
|
|
|
|
// Send message to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'PAIRING_4WORDS_WORDS_GENERATED',
|
|
data: { words: customEvent.detail.words },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('pairing-status-update', (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
console.log('📊 Pairing status update:', customEvent.detail.status);
|
|
// Update status indicators
|
|
const statusElement = document.querySelector(`#${customEvent.detail.type}-status span`);
|
|
if (statusElement) {
|
|
statusElement.textContent = customEvent.detail.status;
|
|
}
|
|
|
|
// Send message to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'PAIRING_4WORDS_STATUS_UPDATE',
|
|
data: { status: customEvent.detail.status, type: customEvent.detail.type },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('pairing-success', (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
console.log('✅ Pairing successful:', customEvent.detail.message);
|
|
|
|
// Send message to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'PAIRING_4WORDS_SUCCESS',
|
|
data: { message: customEvent.detail.message },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
|
|
// Handle successful pairing
|
|
setTimeout(() => {
|
|
window.location.href = '/account';
|
|
}, 2000);
|
|
});
|
|
|
|
window.addEventListener('pairing-error', (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
console.error('❌ Pairing error:', customEvent.detail.error);
|
|
|
|
// Send message to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'PAIRING_4WORDS_ERROR',
|
|
data: { error: customEvent.detail.error },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
|
|
// Handle pairing error
|
|
alert(`Pairing error: ${customEvent.detail.error}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize content menu (only in iframe mode)
|
|
export function initContentMenu() {
|
|
// Only add menu buttons if we're in an iframe
|
|
if (window.parent !== window) {
|
|
// Add iframe mode class to body
|
|
document.body.classList.add('iframe-mode');
|
|
|
|
// Add menu buttons to title container
|
|
const titleContainer = document.querySelector('.title-container');
|
|
if (titleContainer) {
|
|
const menuHtml = `
|
|
<div class="content-menu">
|
|
<button class="menu-btn active" data-page="home">🏠 Home</button>
|
|
<button class="menu-btn" data-page="account">👤 Account</button>
|
|
<button class="menu-btn" data-page="settings">⚙️ Settings</button>
|
|
<button class="menu-btn" data-page="help">❓ Help</button>
|
|
</div>
|
|
`;
|
|
titleContainer.insertAdjacentHTML('beforeend', menuHtml);
|
|
}
|
|
|
|
const menuButtons = document.querySelectorAll('.menu-btn');
|
|
|
|
menuButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
// Remove active class from all buttons
|
|
menuButtons.forEach(btn => btn.classList.remove('active'));
|
|
// Add active class to clicked button
|
|
button.classList.add('active');
|
|
|
|
const page = button.getAttribute('data-page');
|
|
console.log(`Menu clicked: ${page}`);
|
|
|
|
// Send message to parent window
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'MENU_NAVIGATION',
|
|
data: { page },
|
|
},
|
|
'*'
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize iframe communication
|
|
export function initIframeCommunication() {
|
|
// Listen for messages from parent window
|
|
window.addEventListener('message', event => {
|
|
// Filter out browser extension messages first
|
|
if (
|
|
event.data.source === 'react-devtools-content-script' ||
|
|
event.data.hello === true ||
|
|
!event.data.type ||
|
|
event.data.type.startsWith('Pass::') ||
|
|
event.data.type === 'PassClientScriptReady'
|
|
) {
|
|
return; // Ignore browser extension messages
|
|
}
|
|
|
|
// Security check - in production, verify event.origin
|
|
console.log('📨 Received message from parent:', event.data);
|
|
|
|
const { type, data } = event.data;
|
|
|
|
switch (type) {
|
|
case 'TEST_MESSAGE':
|
|
console.log('🧪 Test message received:', data.message);
|
|
// Send response back to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'TEST_RESPONSE',
|
|
data: { response: 'Hello from 4NK iframe!' },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 'PAIRING_4WORDS_CREATE':
|
|
console.log('🔐 Parent requested pairing creation');
|
|
createPairingViaIframe();
|
|
break;
|
|
|
|
case 'PAIRING_4WORDS_JOIN':
|
|
console.log('🔗 Parent requested pairing join with words:', data.words);
|
|
joinPairingViaIframe(data.words);
|
|
break;
|
|
|
|
case 'LISTENING':
|
|
console.log('👂 Parent is listening for messages');
|
|
break;
|
|
|
|
case 'IFRAME_READY':
|
|
console.log('✅ Iframe is ready and initialized');
|
|
break;
|
|
|
|
default:
|
|
console.log('❓ Unknown message type from parent:', type);
|
|
}
|
|
});
|
|
|
|
// Notify parent that iframe is ready
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'IFRAME_READY',
|
|
data: { service: '4nk-pairing' },
|
|
},
|
|
'*'
|
|
);
|
|
console.log('📡 Notified parent that iframe is ready');
|
|
}
|
|
}
|
|
|
|
// Enhanced pairing functions using iframe
|
|
export async function createPairingViaIframe() {
|
|
if (!iframePairing) {
|
|
initIframePairing();
|
|
}
|
|
|
|
try {
|
|
await iframePairing!.createPairing();
|
|
} catch (error) {
|
|
console.error('Error creating pairing via iframe:', error);
|
|
alert(`Error creating pairing: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
export async function joinPairingViaIframe(words: string) {
|
|
if (!iframePairing) {
|
|
initIframePairing();
|
|
}
|
|
|
|
try {
|
|
await iframePairing!.joinPairing(words);
|
|
} catch (error) {
|
|
console.error('Error joining pairing via iframe:', error);
|
|
alert(`Error joining pairing: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
// Set up button listeners for iframe pairing
|
|
export function setupIframePairingButtons() {
|
|
// Create button listener
|
|
const createButton = document.getElementById('createButton');
|
|
if (createButton) {
|
|
createButton.addEventListener('click', async () => {
|
|
console.log('🔐 Create button clicked - using iframe pairing');
|
|
await createPairingViaIframe();
|
|
});
|
|
}
|
|
|
|
// Join button listener
|
|
const joinButton = document.getElementById('joinButton');
|
|
const wordsInput = document.getElementById('wordsInput') as HTMLInputElement;
|
|
|
|
if (joinButton && wordsInput) {
|
|
// Enable join button when words are entered
|
|
wordsInput.addEventListener('input', () => {
|
|
const words = wordsInput.value.trim();
|
|
(joinButton as HTMLButtonElement).disabled = !words;
|
|
});
|
|
|
|
joinButton.addEventListener('click', async () => {
|
|
const words = wordsInput.value.trim();
|
|
if (words) {
|
|
console.log('🔗 Join button clicked - using iframe pairing with words:', words);
|
|
await joinPairingViaIframe(words);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Copy words button listener
|
|
const copyWordsBtn = document.getElementById('copyWordsBtn');
|
|
if (copyWordsBtn) {
|
|
copyWordsBtn.addEventListener('click', () => {
|
|
const creatorWordsElement = document.querySelector('#creator-words');
|
|
if (creatorWordsElement && creatorWordsElement.textContent) {
|
|
navigator.clipboard
|
|
.writeText(creatorWordsElement.textContent)
|
|
.then(() => {
|
|
console.log('✅ Words copied to clipboard');
|
|
// Show feedback
|
|
const originalText = copyWordsBtn.textContent;
|
|
copyWordsBtn.textContent = '✅ Copied!';
|
|
setTimeout(() => {
|
|
copyWordsBtn.textContent = originalText;
|
|
}, 2000);
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to copy words:', err);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Main Pairing Interface
|
|
export function setupMainPairing(): void {
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
|
|
const mainPairingButton = container.querySelector('#mainPairingButton') as HTMLButtonElement;
|
|
const mainStatus = container.querySelector('#main-status') as HTMLElement;
|
|
|
|
if (mainPairingButton) {
|
|
mainPairingButton.addEventListener('click', async () => {
|
|
await handleMainPairing();
|
|
});
|
|
}
|
|
}
|
|
|
|
async function handleMainPairing(): Promise<void> {
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
const mainStatus = container.querySelector('#main-status') as HTMLElement;
|
|
const mainPairingButton = container.querySelector('#mainPairingButton') as HTMLButtonElement;
|
|
|
|
try {
|
|
// Update UI
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>Authenticating with browser...</span>';
|
|
}
|
|
if (mainPairingButton) {
|
|
mainPairingButton.disabled = true;
|
|
mainPairingButton.textContent = 'Authenticating...';
|
|
}
|
|
|
|
// Check if we have existing credentials
|
|
const service = await Services.getInstance();
|
|
const { secureCredentialsService } = await import('../../services/secure-credentials.service');
|
|
const hasCredentials = await secureCredentialsService.hasCredentials();
|
|
|
|
if (hasCredentials) {
|
|
// Existing pairing - decrypt credentials
|
|
console.log('🔓 Existing credentials found, decrypting...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>Decrypting existing credentials...</span>';
|
|
}
|
|
|
|
await secureCredentialsService.retrieveCredentials(''); // Empty password for WebAuthn
|
|
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Credentials decrypted successfully</span>';
|
|
}
|
|
} else {
|
|
// No existing pairing - create new one
|
|
console.log('🔐 No existing credentials, creating new pairing...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>Creating new secure pairing...</span>';
|
|
}
|
|
|
|
// This will trigger the WebAuthn flow and create new credentials
|
|
await prepareAndSendPairingTx();
|
|
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ New pairing created successfully</span>';
|
|
}
|
|
}
|
|
|
|
// Re-enable button
|
|
if (mainPairingButton) {
|
|
mainPairingButton.disabled = false;
|
|
mainPairingButton.textContent = 'Authenticate with Browser';
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Pairing failed:', error);
|
|
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Authentication failed</span>';
|
|
}
|
|
|
|
if (mainPairingButton) {
|
|
mainPairingButton.disabled = false;
|
|
mainPairingButton.textContent = 'Authenticate with Browser';
|
|
}
|
|
}
|
|
}
|