From 95cceceeca00fad5d5970394dc8ba14a18a1db43 Mon Sep 17 00:00:00 2001 From: Tommaso Ascani Date: Mon, 8 Sep 2025 09:12:14 +0200 Subject: [PATCH 01/20] feat: add OTP input component --- .../pageComponents/login/OTPInput.tsx | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 src/renderer/src/components/pageComponents/login/OTPInput.tsx diff --git a/src/renderer/src/components/pageComponents/login/OTPInput.tsx b/src/renderer/src/components/pageComponents/login/OTPInput.tsx new file mode 100644 index 00000000..17d0d192 --- /dev/null +++ b/src/renderer/src/components/pageComponents/login/OTPInput.tsx @@ -0,0 +1,193 @@ +// Copyright (C) 2025 Nethesis S.r.l. +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { + useRef, + useEffect, + KeyboardEvent, + ClipboardEvent, + forwardRef, + useImperativeHandle, +} from 'react' + +interface OTPInputProps { + value: string + onChange: (value: string) => void + length?: number + disabled?: boolean + className?: string + error?: boolean +} + +export interface OTPInputRef { + focus: () => void + clear: () => void +} + +export const OTPInput = forwardRef( + ({ value, onChange, length = 6, disabled = false, className = '', error = false }, ref) => { + const inputRefs = useRef<(HTMLInputElement | null)[]>([]) + + // Initialize input refs array + useEffect(() => { + inputRefs.current = inputRefs.current.slice(0, length) + }, [length]) + + // Expose methods to parent component + useImperativeHandle(ref, () => ({ + focus: () => { + const firstEmptyIndex = value.length < length ? value.length : 0 + inputRefs.current[firstEmptyIndex]?.focus() + }, + clear: () => { + onChange('') + inputRefs.current[0]?.focus() + }, + })) + + // Auto-focus first input on mount + useEffect(() => { + if (!disabled) { + inputRefs.current[0]?.focus() + } + }, [disabled]) + + const focusInput = (index: number) => { + if (inputRefs.current[index]) { + inputRefs.current[index]?.focus() + } + } + + const focusNextInput = (index: number) => { + if (index < length - 1) { + focusInput(index + 1) + } + } + + const focusPrevInput = (index: number) => { + if (index > 0) { + focusInput(index - 1) + } + } + + const handleChange = (index: number, inputValue: string) => { + // Remove any non-digit characters + const digit = inputValue.replace(/\D/g, '') + + if (digit.length <= 1) { + const newValue = value.split('') + newValue[index] = digit + const updatedValue = newValue.join('').slice(0, length) + onChange(updatedValue) + + // Auto-focus next input if a digit was entered + if (digit && index < length - 1) { + focusNextInput(index) + } + } + } + + const handleKeyDown = (index: number, event: KeyboardEvent) => { + const { key } = event + + if (key === 'Backspace') { + if (value[index]) { + // Clear current input + const newValue = value.split('') + newValue[index] = '' + onChange(newValue.join('')) + } else if (index > 0) { + // Move to previous input and clear it + const newValue = value.split('') + newValue[index - 1] = '' + onChange(newValue.join('')) + focusPrevInput(index) + } + } else if (key === 'ArrowLeft') { + event.preventDefault() + focusPrevInput(index) + } else if (key === 'ArrowRight') { + event.preventDefault() + focusNextInput(index) + } else if (key === 'Delete') { + event.preventDefault() + const newValue = value.split('') + newValue[index] = '' + onChange(newValue.join('')) + } else if (/^[0-9]$/.test(key)) { + // Handle direct digit input + event.preventDefault() + handleChange(index, key) + } + } + + const handlePaste = (event: ClipboardEvent) => { + event.preventDefault() + const pasteData = event.clipboardData.getData('text') + const digits = pasteData.replace(/\D/g, '').slice(0, length) + onChange(digits) + + // Focus the next empty input or the last input + const nextFocusIndex = Math.min(digits.length, length - 1) + setTimeout(() => focusInput(nextFocusIndex), 0) + } + + const handleFocus = (index: number) => { + // Select all text when focusing + inputRefs.current[index]?.select() + } + + return ( +
+ {Array.from({ length }, (_, index) => ( + (inputRefs.current[index] = el)} + type='text' + inputMode='numeric' + pattern='[0-9]*' + maxLength={1} + value={value[index] || ''} + onChange={(e) => handleChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={handlePaste} + onFocus={() => handleFocus(index)} + disabled={disabled} + className={` + w-12 h-12 text-center text-lg font-semibold + border-2 rounded-lg + focus:outline-none transition-colors duration-200 + ${ + error + ? 'border-red-500 dark:border-red-400 focus:ring-2 focus:ring-red-500 focus:border-red-500 dark:focus:ring-red-400 dark:focus:border-red-400' + : 'focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primaryDark dark:focus:border-primaryDark' + } + ${ + !error && value[index] + ? 'border-primary dark:border-primaryDark bg-primary/5 dark:bg-primaryDark/5' + : !error && !value[index] + ? 'border-gray-300 dark:border-gray-600' + : '' + } + ${ + error && value[index] + ? 'bg-red-50 dark:bg-red-900/20' + : error && !value[index] + ? 'bg-red-50 dark:bg-red-900/20' + : '' + } + ${ + disabled + ? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500 cursor-not-allowed' + : 'bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 hover:border-gray-400 dark:hover:border-gray-500' + } + `} + aria-label={`Digit ${index + 1}`} + /> + ))} +
+ ) + }, +) + +OTPInput.displayName = 'OTPInput' From e1ecbe06f8e7097dc589bae1890f0b4e82b841f7 Mon Sep 17 00:00:00 2001 From: Tommaso Ascani Date: Tue, 9 Sep 2025 17:14:19 +0200 Subject: [PATCH 02/20] feat: Implement Two-Factor Authentication (2FA) support in login process - Added OTP input component for 2FA verification in LoginForm. - Updated DisplayedAccountLogin to handle delete click functionality. - Enhanced LoginForm to manage 2FA state and verification flow. - Integrated JWT token handling for authentication, including 2FA requirement checks. - Refactored useNethVoiceAPI to support JWT-based login and 2FA verification. - Introduced JWT utility functions for decoding and validation. - Updated LoginPage to manage UI state for 2FA and account selection. - Adjusted store to include showTwoFactor state for managing login UI. - Improved error handling for login and 2FA processes. --- public/locales/en/translations.json | 11 +- public/locales/it/translations.json | 11 +- .../classes/controllers/AccountController.ts | 16 +- .../public/locales/en/translations.json | 11 +- .../public/locales/it/translations.json | 11 +- .../login/DisplayedAccountLogin.tsx | 52 +-- .../pageComponents/login/LoginForm.tsx | 351 ++++++++++++++---- .../components/pageComponents/login/index.ts | 1 + src/renderer/src/pages/LoginPage.tsx | 124 +++++-- src/renderer/src/store.ts | 3 +- src/shared/types.ts | 2 + src/shared/useNethVoiceAPI.ts | 204 ++++++---- src/shared/useNetwork.ts | 2 +- src/shared/utils/jwt.ts | 75 ++++ 14 files changed, 660 insertions(+), 214 deletions(-) create mode 100644 src/shared/utils/jwt.ts diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 921078c8..ef745fed 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -109,7 +109,16 @@ "Account List title": "Account list", "Account List description": "Choose an account to continue to NethLink.", "Delete account": "Are you sure you want to delete {{username}}?", - "Back": "Back" + "Back": "Back", + "User not authorized for NethLink": "User not authorized for NethLink", + "Generic error": "Generic error", + "2FA": { + "OTP code": "OTP code", + "OTP invalid": "The code you entered is invalid or has expired. Please check your authenticator app and try again.", + "OTP verification failed": "OTP verification failed", + "Two-Factor Authentication": "Two-Factor Authentication", + "Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.": "Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code." + } }, "SplashScreen": { "Description": "Welcome to NethLink, a desktop solution for seamless communication. Make and receive calls, save contacts to you phonebook and much more.", diff --git a/public/locales/it/translations.json b/public/locales/it/translations.json index daa6c0f4..866dea3d 100644 --- a/public/locales/it/translations.json +++ b/public/locales/it/translations.json @@ -109,7 +109,16 @@ "Account List title": "Account disponibili", "Account List description": "Scegliete un account per proseguire con NethLink.", "Delete account": "Sei sicuro di voler eliminare {{username}}?", - "Back": "Indietro" + "Back": "Indietro", + "User not authorized for NethLink": "Utente non autorizzato per NethLink", + "Generic error": "Errore generico", + "2FA": { + "OTP code": "Codice OTP", + "OTP invalid": "Il codice inserito non è valido o è scaduto. Controlla la tua app di autenticazione e riprova.", + "OTP verification failed": "Verifica OTP fallita", + "Two-Factor Authentication": "Autenticazione a Due Fattori", + "Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.": "Inserisci il codice a 6 cifre (codice OTP) dalla tua app di autenticazione. Se non puoi accedere all'app, puoi utilizzare un codice OTP di recupero." + } }, "SplashScreen": { "Description": "Benvenuti in NethLink, la soluzione desktop per comunicazioni senza confini. Effettua e ricevi chiamate, salva i contatti nella tua rubrica e molto altro ancora.", diff --git a/src/main/classes/controllers/AccountController.ts b/src/main/classes/controllers/AccountController.ts index 35a2f07c..cf275e49 100644 --- a/src/main/classes/controllers/AccountController.ts +++ b/src/main/classes/controllers/AccountController.ts @@ -5,7 +5,8 @@ import { store } from '@/lib/mainStore' import { useNethVoiceAPI } from '@shared/useNethVoiceAPI' import { useLogin } from '@shared/useLogin' import { NetworkController } from './NetworkController' -import { delay, getAccountUID } from '@shared/utils/utils' +import { getAccountUID } from '@shared/utils/utils' +import { requires2FA } from '@shared/utils/jwt' const defaultConfig: ConfigFile = { lastUser: undefined, @@ -77,6 +78,19 @@ export class AccountController { const _accountData = JSON.parse(decryptString) const password = _accountData.password const tempLoggedAccount = await this.NethVoiceAPI.Authentication.login(lastLoggedAccount.host, lastLoggedAccount.username, password) + + // Check if 2FA is required - auto-login should fail in this case + if (tempLoggedAccount.jwtToken && requires2FA(tempLoggedAccount.jwtToken)) { + Log.info('auto login failed: 2FA required, user interaction needed') + return false + } + + // Auto-login only works with JWT tokens (no legacy support) + if (!tempLoggedAccount.jwtToken) { + Log.info('auto login failed: no JWT token received') + return false + } + let loggedAccount: Account = { ...lastLoggedAccount, ...tempLoggedAccount, diff --git a/src/renderer/public/locales/en/translations.json b/src/renderer/public/locales/en/translations.json index 921078c8..ef745fed 100644 --- a/src/renderer/public/locales/en/translations.json +++ b/src/renderer/public/locales/en/translations.json @@ -109,7 +109,16 @@ "Account List title": "Account list", "Account List description": "Choose an account to continue to NethLink.", "Delete account": "Are you sure you want to delete {{username}}?", - "Back": "Back" + "Back": "Back", + "User not authorized for NethLink": "User not authorized for NethLink", + "Generic error": "Generic error", + "2FA": { + "OTP code": "OTP code", + "OTP invalid": "The code you entered is invalid or has expired. Please check your authenticator app and try again.", + "OTP verification failed": "OTP verification failed", + "Two-Factor Authentication": "Two-Factor Authentication", + "Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.": "Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code." + } }, "SplashScreen": { "Description": "Welcome to NethLink, a desktop solution for seamless communication. Make and receive calls, save contacts to you phonebook and much more.", diff --git a/src/renderer/public/locales/it/translations.json b/src/renderer/public/locales/it/translations.json index daa6c0f4..866dea3d 100644 --- a/src/renderer/public/locales/it/translations.json +++ b/src/renderer/public/locales/it/translations.json @@ -109,7 +109,16 @@ "Account List title": "Account disponibili", "Account List description": "Scegliete un account per proseguire con NethLink.", "Delete account": "Sei sicuro di voler eliminare {{username}}?", - "Back": "Indietro" + "Back": "Indietro", + "User not authorized for NethLink": "Utente non autorizzato per NethLink", + "Generic error": "Errore generico", + "2FA": { + "OTP code": "Codice OTP", + "OTP invalid": "Il codice inserito non è valido o è scaduto. Controlla la tua app di autenticazione e riprova.", + "OTP verification failed": "Verifica OTP fallita", + "Two-Factor Authentication": "Autenticazione a Due Fattori", + "Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.": "Inserisci il codice a 6 cifre (codice OTP) dalla tua app di autenticazione. Se non puoi accedere all'app, puoi utilizzare un codice OTP di recupero." + } }, "SplashScreen": { "Description": "Benvenuti in NethLink, la soluzione desktop per comunicazioni senza confini. Effettua e ricevi chiamate, salva i contatti nella tua rubrica e molto altro ancora.", diff --git a/src/renderer/src/components/pageComponents/login/DisplayedAccountLogin.tsx b/src/renderer/src/components/pageComponents/login/DisplayedAccountLogin.tsx index 6f30649d..1fe9b018 100644 --- a/src/renderer/src/components/pageComponents/login/DisplayedAccountLogin.tsx +++ b/src/renderer/src/components/pageComponents/login/DisplayedAccountLogin.tsx @@ -17,41 +17,47 @@ export function DisplayedAccountLogin({ account, imageSrc, handleClick, - handleDeleteClick + handleDeleteClick, }: DisplayedAccountLoginProps) { return (
handleClick?.()} className={classNames( 'w-full flex flex-row gap-7 items-center justify-start bg-transparent h-20 rounded-lg text-titleLight dark:text-titleDark cursor-pointer', - handleClick ? 'hover:bg-hoverLight dark:hover:bg-hoverDark' : '' + handleClick ? 'hover:bg-hoverLight dark:hover:bg-hoverDark' : '', )} > -
- +
+
-

- {account - ? - - {account.data?.name} - - {

{account.data?.endpoints.mainextension[0].id} - {account.host}
} +

+ {account ? ( + + {account.data?.name} + { + + {account.data?.endpoints.mainextension[0].id} - {account.host} + + } - : t('Login.Use Another Account')} + ) : ( + t('Login.Use Another Account') + )}

- { - handleDeleteClick && { - e.preventDefault() - e.stopPropagation() - handleDeleteClick() - }} /> - } + {handleDeleteClick && ( + { + e.preventDefault() + e.stopPropagation() + handleDeleteClick() + }} + /> + )}
) diff --git a/src/renderer/src/components/pageComponents/login/LoginForm.tsx b/src/renderer/src/components/pageComponents/login/LoginForm.tsx index 1146a38e..a68e0b20 100644 --- a/src/renderer/src/components/pageComponents/login/LoginForm.tsx +++ b/src/renderer/src/components/pageComponents/login/LoginForm.tsx @@ -5,20 +5,22 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faEye as EyeIcon, faEyeSlash as EyeSlashIcon, - faXmarkCircle as ErrorIcon, faWarning as AlertIcon, + faCircleNotch, } from '@fortawesome/free-solid-svg-icons' import { t } from 'i18next' import { useEffect, useRef, useState } from 'react' import { Button, TextInput } from '@renderer/components/Nethesis' import { Account, LoginData } from '@shared/types' import { DisplayedAccountLogin } from './DisplayedAccountLogin' +import { OTPInput, OTPInputRef } from './OTPInput' import { useLoginPageData, useSharedState } from '@renderer/store' import { useNethVoiceAPI } from '@shared/useNethVoiceAPI' import { IPC_EVENTS, NEW_ACCOUNT } from '@shared/constants' import { Log } from '@shared/utils/logger' import { getAccountUID } from '@shared/utils/utils' import { InlineNotification } from '@renderer/components/Nethesis/InlineNotification' +import { requires2FA } from '@shared/utils/jwt' export interface LoginFormProps { onError: ( @@ -36,7 +38,13 @@ export const LoginForm = ({ onError, handleRefreshConnection }) => { const [isLoading, setIsLoading] = useLoginPageData('isLoading') const [connection] = useSharedState('connection') const [error, setError] = useState(undefined) + const [otpCode, setOtpCode] = useState('') + const [onOTPError, setOnOTPError] = useState(false) + const [showTwoFactor, setShowTwoFactor] = useLoginPageData('showTwoFactor') + const [tempAccount, setTempAccount] = useState(undefined) + const [otpDisabled, setOtpDisabled] = useState(false) const passwordRef = useRef() + const otpInputRef = useRef() as React.MutableRefObject const schema: z.ZodType = z.object({ host: z @@ -71,24 +79,42 @@ export const LoginForm = ({ onError, handleRefreshConnection }) => { useEffect(() => { setIsLoading(false) + setTempAccount(undefined) + setOnOTPError(false) if (auth?.availableAccounts) { if (selectedAccount) { if (selectedAccount === NEW_ACCOUNT) { + setShowTwoFactor(false) // Reset 2FA when switching to new account reset() focus('host') } else { + setShowTwoFactor(false) // Reset 2FA when switching to existing account reset() setValue('host', selectedAccount.host) setValue('username', selectedAccount.username) focus('password') } } else { + setShowTwoFactor(false) // Reset 2FA when going back to account list setError(undefined) focus('host') } } }, [auth, selectedAccount]) + // Handle 2FA reset when back button is pressed from LoginPage + useEffect(() => { + if (!showTwoFactor && tempAccount) { + // Reset 2FA state when going back from OTP verification + setTempAccount(undefined) + setOnOTPError(false) + setIsLoading(false) + setOtpDisabled(false) + setOtpCode('') + setError(undefined) + } + }, [showTwoFactor]) + async function handleLogin(data: LoginData) { if (!isLoading) { let e: Error | undefined = undefined @@ -106,6 +132,18 @@ export const LoginForm = ({ onError, handleRefreshConnection }) => { data.password, ) Log.info('LOGIN successfully logged in with credential') + + // Check if 2FA is required + if (loggedAccount.jwtToken && requires2FA(loggedAccount.jwtToken)) { + Log.info('LOGIN 2FA required, showing 2FA form') + setTempAccount(loggedAccount) + passwordRef.current = data.password + setShowTwoFactor(true) + setIsLoading(false) + return + } + + // Complete login flow window.electron.receive( IPC_EVENTS.SET_NETHVOICE_CONFIG, (account: Account) => { @@ -133,12 +171,22 @@ export const LoginForm = ({ onError, handleRefreshConnection }) => { }) setError(() => undefined) } catch (error: any) { + console.error('LOGIN error during login', error) setIsLoading(false) - if (error.message === 'Unauthorized') + if (error.message === 'Wrong username or password') { + setError(() => new Error(t('Login.Wrong username or password')!)) + } else if (error.message === 'Network connection lost') { + setError(() => new Error(t('Login.Network connection is lost')!)) + } else if (error.message === 'Unauthorized') { setError( () => new Error(t('Login.Wrong host or username or password')!), ) - else { + } else if (error.message === 'User not authorized for NethLink') { + Log.info('LOGIN user not authorized for NethLink') + setError( + () => new Error(t('Login.User not authorized for NethLink')!), + ) + } else { setError(() => error) } } @@ -152,6 +200,82 @@ export const LoginForm = ({ onError, handleRefreshConnection }) => { } } + async function handle2FAVerification(e?: React.FormEvent) { + if (e) { + e.preventDefault() + } + + if (!tempAccount) { + setOnOTPError(true) + return + } + + setIsLoading(true) + setOnOTPError(false) + + try { + Log.info('LOGIN verifying 2FA code') + // const { NethVoiceAPI: TempAPI } = useNethVoiceAPI(tempAccount) + const verifiedAccount = await NethVoiceAPI.Authentication.verify2FA( + otpCode, + tempAccount, + ) + Log.info('LOGIN 2FA verification successful') + + // Complete login flow + window.electron.receive( + IPC_EVENTS.SET_NETHVOICE_CONFIG, + (account: Account) => { + Log.info( + 'LOGIN received account server configuration after 2FA', + account, + ) + const previousLoggedAccount = + auth?.availableAccounts[getAccountUID(account)] + account.theme = previousLoggedAccount + ? previousLoggedAccount.theme + : 'system' + Log.info('LOGIN send login event to the backend after 2FA', account) + window.electron.send(IPC_EVENTS.LOGIN, { + password: passwordRef.current, + account, + }) + }, + ) + Log.info('LOGIN get account server configuration after 2FA') + window.electron.send(IPC_EVENTS.GET_NETHVOICE_CONFIG, verifiedAccount) + + setTempAccount(undefined) + setError(() => undefined) + } catch (error: any) { + setIsLoading(false) + + if (error.message === 'OTP invalid') { + setOnOTPError(true) + setError(() => new Error(t('Login.2FA.OTP invalid') as string)) + } else if (error.message === 'User not authorized for NethLink') { + setError( + () => + new Error(t('Login.User not authorized for NethLink') as string), + ) + setOtpDisabled(true) + } else { + console.error('LOGIN error during 2FA verification', error) + setError(() => new Error(t('Login.Generic error') as string)) + } + } + } + + function handleBack2FA() { + setShowTwoFactor(false) + setTempAccount(undefined) + setOnOTPError(false) + setIsLoading(false) + setOtpDisabled(false) + setOtpCode('') + setError(undefined) + } + const onSubmitForm: SubmitHandler = (data) => { handleLogin(data) } @@ -198,52 +322,142 @@ export const LoginForm = ({ onError, handleRefreshConnection }) => { return (
-

- {selectedAccount - ? t('Login.Account List title') - : t('Login.New Account title')} -

-

- {selectedAccount - ? t('Login.Account List description') - : t('Login.New Account description')} -

- {error && ( - - {error.message} - - )} - {selectedAccount && selectedAccount !== NEW_ACCOUNT && ( - - )} - {connection ? ( -
-
- {!(selectedAccount && selectedAccount !== NEW_ACCOUNT) && ( - <> - { - if (e.key === 'Enter') { - e.preventDefault() - submitButtonRef.current?.focus() - handleSubmit(onSubmitForm)(e) - } - }} + {showTwoFactor ? ( + <> + {/* OTP input section */} +
+
+

+ {t('Login.2FA.Two-Factor Authentication')} +

+

+ {t( + 'Login.2FA.Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.', + )} +

+
+ {error && ( + + {error.message} + + )} + handle2FAVerification(e)} + className='space-y-6 mt-6' + > +
+

+ {t('Login.2FA.OTP code')} +

+ +
+ +
+ +
+ +
+ + ) : ( + <> +

+ {selectedAccount + ? t('Login.Account List title') + : t('Login.New Account title')} +

+

+ {selectedAccount + ? t('Login.Account List description') + : t('Login.New Account description')} +

+ {error && ( + + {error.message} + + )} + {selectedAccount && selectedAccount !== NEW_ACCOUNT && ( + + )} + {connection ? ( +
+
+ {!(selectedAccount && selectedAccount !== NEW_ACCOUNT) && ( + <> + { + if (e.key === 'Enter') { + e.preventDefault() + submitButtonRef.current?.focus() + handleSubmit(onSubmitForm)(e) + } + }} + /> + { + if (e.key === 'Enter') { + e.preventDefault() + submitButtonRef.current?.focus() + handleSubmit(onSubmitForm)(e) + } + }} + /> + + )} setPwdVisible(!pwdVisible)} + trailingIcon={true} + helper={errors.password?.message || undefined} + error={!!errors.password?.message} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() @@ -252,36 +466,19 @@ export const LoginForm = ({ onError, handleRefreshConnection }) => { } }} /> - - )} - setPwdVisible(!pwdVisible)} - trailingIcon={true} - helper={errors.password?.message || undefined} - error={!!errors.password?.message} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - submitButtonRef.current?.focus() - handleSubmit(onSubmitForm)(e) - } - }} - /> - -
-
- ) : ( -
- -
+ +
+ + ) : ( +
+ +
+ )} + )}
) diff --git a/src/renderer/src/components/pageComponents/login/index.ts b/src/renderer/src/components/pageComponents/login/index.ts index 493ad306..085a2255 100644 --- a/src/renderer/src/components/pageComponents/login/index.ts +++ b/src/renderer/src/components/pageComponents/login/index.ts @@ -2,3 +2,4 @@ export * from './AvailableAccountList' export * from './LoginForm' export * from './AvailableAccountDeleteDialog' export * from './DisplayedAccountLogin' +export * from './OTPInput' diff --git a/src/renderer/src/pages/LoginPage.tsx b/src/renderer/src/pages/LoginPage.tsx index e2e54a50..3930a61f 100644 --- a/src/renderer/src/pages/LoginPage.tsx +++ b/src/renderer/src/pages/LoginPage.tsx @@ -5,28 +5,30 @@ import spinner from '../assets/loginPageSpinner.svg' import darkHeader from '../assets/nethlinkDarkHeader.svg' import lightHeader from '../assets/nethlinkLightHeader.svg' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { - faArrowLeft as ArrowIcon, -} from '@fortawesome/free-solid-svg-icons' +import { faArrowLeft as ArrowIcon } from '@fortawesome/free-solid-svg-icons' import { t } from 'i18next' import { Button } from '@renderer/components/Nethesis' import './LoginPage.css' import { useLoginPageData, useSharedState } from '@renderer/store' -import { AvailableAccountList, LoginForm } from '@renderer/components/pageComponents' +import { + AvailableAccountList, + LoginForm, +} from '@renderer/components/pageComponents' import { IPC_EVENTS, LoginPageSize, NEW_ACCOUNT } from '@shared/constants' import { Log } from '@shared/utils/logger' import { FieldErrors } from 'react-hook-form' import { AvailableAccountDeleteDialog } from '@renderer/components/pageComponents/login/AvailableAccountDeleteDialog' export interface LoginPageProps { - themeMode: string, + themeMode: string handleRefreshConnection: () => void } enum LoginSizes { BASE = 550, ACCOUNT_FORM = 488, + TWO_FACTOR_AUTH = 420, BACK_BUTTON = 60, INPUT_ERROR = 22, LOGIN_FAILURE = 104, @@ -39,25 +41,30 @@ enum LoginSizes { } type ErrorsData = { - formErrors: FieldErrors, - generalError: Error | undefined, + formErrors: FieldErrors + generalError: Error | undefined } -export function LoginPage({ themeMode, handleRefreshConnection }: LoginPageProps) { - - +export function LoginPage({ + themeMode, + handleRefreshConnection, +}: LoginPageProps) { const loginWindowRef = useRef() as MutableRefObject const [auth] = useSharedState('auth') - const [isLoading, setIsLoading] = useLoginPageData('isLoading') - const [selectedAccount, setSelectedAccount] = useLoginPageData('selectedAccount') + const [isLoading] = useLoginPageData('isLoading') + const [selectedAccount, setSelectedAccount] = + useLoginPageData('selectedAccount') const [windowHeight, setWindowHeight] = useLoginPageData('windowHeight') + const [showTwoFactor, setShowTwoFactor] = useLoginPageData('showTwoFactor') const [connection] = useSharedState('connection') const [errorsData, setErrorsData] = useState() const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) - const [deleteDialogAccount, setDeleteDialogAccount] = useState(undefined) + const [deleteDialogAccount, setDeleteDialogAccount] = useState< + Account | undefined + >(undefined) useEffect(() => { calculateHeight() - }, [selectedAccount, auth, errorsData, connection]) + }, [selectedAccount, auth, errorsData, connection, showTwoFactor]) useEffect(() => { if (windowHeight) { @@ -66,15 +73,24 @@ export function LoginPage({ themeMode, handleRefreshConnection }: LoginPageProps }, [windowHeight]) const goBack = () => { - setSelectedAccount(undefined) - setErrorsData({ formErrors: {}, generalError: undefined }) + if (showTwoFactor) { + // If we're in OTP verification, go back to login form + setShowTwoFactor(false) + // Keep selectedAccount, stay in the login form + } else { + // If we're in normal login form, go back to account selection + setSelectedAccount(undefined) + setErrorsData({ formErrors: {}, generalError: undefined }) + } } - - const onFormErrors = (formErrors: FieldErrors, generalError: Error | undefined) => { + const onFormErrors = ( + formErrors: FieldErrors, + generalError: Error | undefined, + ) => { setErrorsData({ formErrors, - generalError + generalError, }) } @@ -96,13 +112,22 @@ export function LoginPage({ themeMode, handleRefreshConnection }: LoginPageProps function calculateHeight() { let loginWindowHeight = 0 const accounts = Object.keys(auth?.availableAccounts || {}) - const errorCount = Object.values(errorsData?.formErrors || {}).filter((v) => v.message).length - //Login form is shown - if (selectedAccount) { + const errorCount = Object.values(errorsData?.formErrors || {}).filter( + (v) => v.message, + ).length + + // If Two Factor Authentication is shown, use specific height + if (showTwoFactor) { + loginWindowHeight = LoginSizes.TWO_FACTOR_AUTH + if (!auth?.isFirstStart) { + loginWindowHeight += LoginSizes.BACK_BUTTON - 24 + } + } + // Login form is shown + else if (selectedAccount) { if (selectedAccount === NEW_ACCOUNT) { loginWindowHeight = LoginSizes.BASE - if (!connection) - loginWindowHeight = LoginSizes.CONNECTION_FAILURE_BASE + if (!connection) loginWindowHeight = LoginSizes.CONNECTION_FAILURE_BASE if (!auth?.isFirstStart) { loginWindowHeight += LoginSizes.BACK_BUTTON - 24 } @@ -115,11 +140,15 @@ export function LoginPage({ themeMode, handleRefreshConnection }: LoginPageProps if (auth?.isFirstStart) { loginWindowHeight = LoginSizes.BASE } else { - //List of account is shown + // Account list is shown switch (accounts.length) { case 0: loginWindowHeight = LoginSizes.BASE - if (auth && !auth.isFirstStart && Object.keys(auth.availableAccounts).length > 0) { + if ( + auth && + !auth.isFirstStart && + Object.keys(auth.availableAccounts).length > 0 + ) { loginWindowHeight += LoginSizes.BACK_BUTTON } break @@ -145,38 +174,51 @@ export function LoginPage({ themeMode, handleRefreshConnection }: LoginPageProps return (
-
- +
+
- { - auth && <> - { - Object.keys(auth.availableAccounts).length > 0 && selectedAccount && ( + {auth && ( + <> + {Object.keys(auth.availableAccounts).length > 0 && + selectedAccount && ( )} - {(auth.isFirstStart || selectedAccount || Object.keys(auth.availableAccounts).length === 0) ? : } + {auth.isFirstStart || + selectedAccount || + showTwoFactor || + Object.keys(auth.availableAccounts).length === 0 ? ( + + ) : ( + + )} - } + )}
{isLoading && ( -
- +
+
)} = [(T | undefined), (value: T | undefined) => void] export type Account = { username: string accessToken?: string + jwtToken?: string // New JWT token field lastAccess?: string host: string theme: AvailableThemes @@ -410,6 +411,7 @@ export type LoginPageData = { selectedAccount?: Account | typeof NEW_ACCOUNT isLoading: boolean windowHeight?: number + showTwoFactor: boolean } export type AuthAppData = { diff --git a/src/shared/useNethVoiceAPI.ts b/src/shared/useNethVoiceAPI.ts index b2050189..519beb14 100644 --- a/src/shared/useNethVoiceAPI.ts +++ b/src/shared/useNethVoiceAPI.ts @@ -1,6 +1,5 @@ import moment from 'moment' -import hmacSHA1 from 'crypto-js/hmac-sha1' import { Account, NewContactType, @@ -17,6 +16,10 @@ import { import { Log } from '@shared/utils/logger' import { useNetwork } from './useNetwork' import { SpeeddialTypes } from './constants' +import { requires2FA } from '@shared/utils/jwt' + +// Base path for API endpoints +const API_BASE_PATH = '/api' export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) => { const { GET, POST } = useNetwork() @@ -28,20 +31,24 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) return path } - function _toHash(username: string, password: string, nonce: string) { - const token = nonce ? hmacSHA1(`${username}:${password}:${nonce}`, password).toString() : '' - return token - } - function _getHeaders(hasAuth = true) { if (hasAuth && !account) throw new Error('no token') - return { - headers: { - 'Content-Type': 'application/json', - ...(hasAuth && { Authorization: account!.username + ':' + account!.accessToken }) + + const headers: { 'Content-Type': string; Authorization?: string } = { + 'Content-Type': 'application/json', + } + + if (hasAuth) { + // Use JWT Bearer token only + if (account!.jwtToken) { + headers.Authorization = `Bearer ${account!.jwtToken}` + } else { + throw new Error('No JWT token available for authentication') } } + + return { headers } } async function _GET(path: string, hasAuth = true): Promise { @@ -57,18 +64,18 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) try { return (await POST(_joinUrl(path), data, _getHeaders(hasAuth))) } catch (e) { - if (!path.includes('login')) + if (!path.includes('login') && !path.includes('2fa/verify-otp')) console.error(e) throw e } } const AstProxy = { - groups: async () => await _GET('/webrest/astproxy/opgroups'), - extensions: async (): Promise => await _GET('/webrest/astproxy/extensions'), - getQueues: async () => await _GET('/webrest/astproxy/queues'), - getParkings: async () => await _GET('/webrest/astproxy/parkings'), - pickupParking: async (parkInformation: any) => await _POST('/webrest/astproxy/pickup_parking', parkInformation) + groups: async () => await _GET(`${API_BASE_PATH}/astproxy/opgroups`), + extensions: async (): Promise => await _GET(`${API_BASE_PATH}/astproxy/extensions`), + getQueues: async () => await _GET(`${API_BASE_PATH}/astproxy/queues`), + getParkings: async () => await _GET(`${API_BASE_PATH}/astproxy/parkings`), + pickupParking: async (parkInformation: any) => await _POST(`${API_BASE_PATH}/astproxy/pickup_parking`, parkInformation) } @@ -83,43 +90,107 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) username, theme: 'system' } - return new Promise((resolve, reject) => { - _POST('/webrest/authentication/login', data, false).catch(async (reason) => { - try { - if (reason.response?.status === 401 && reason.response?.headers['www-authenticate']) { - const digest = reason.response.headers['www-authenticate'] - const nonce = digest.split(' ')[1] - if (nonce) { - const accessToken = _toHash(username, password, nonce) - account = { - ...account, - accessToken, - lastAccess: moment().toISOString() - } as Account - const me = await User.me() - account.data = me - const nethlinkExtension = account.data!.endpoints.extension.find((el) => el.type === 'nethlink') - if (!nethlinkExtension) - reject(new Error("Questo utente non è abilitato all'uso del NethLink")) - else { - resolve(account) - } - } - } else { - console.error('undefined nonce response') - reject(new Error('Unauthorized')) + + try { + // Try JWT login + const response = await _POST(`${API_BASE_PATH}/login`, data, false) + + if (response.token) { + // JWT authentication successful + account = { + ...account, + jwtToken: response.token, + lastAccess: moment().toISOString() + } as Account + + // Check if 2FA is required + if (requires2FA(response.token)) { + // Return account with JWT token but mark as requiring 2FA + return account + } else { + // Complete login process + const me = await User.me() + account.data = me + const nethlinkExtension = account.data!.endpoints.extension.find((el) => el.type === 'nethlink') + if (!nethlinkExtension) { + throw new Error('User not authorized for NethLink') } - } catch (e) { - reject(e) + return account } - }) - }) + } else { + throw new Error('No token received') + } + } catch (reason: any) { + // Handle specific error cases + if (reason.response?.status === 401) { + throw new Error('Wrong username or password') + } else if (reason.response?.status === 404) { + throw new Error('Network connection lost') + } else if (reason.message === 'User not authorized for NethLink') { + throw reason + } else { + console.error('Login error:', reason) + throw new Error('Unauthorized') + } + } }, + + verify2FA: async (otp: string, tempAccount: Account | undefined): Promise => { + account = tempAccount + + if (!account || !account.jwtToken) { + throw new Error('No active login session') + } + + try { + const response = await _POST(`${API_BASE_PATH}/2fa/verify-otp`, { + otp, + username: account.username + }, true) + + if (response.data.token) { + // Update account with new JWT token + account = { + ...account, + jwtToken: response.data.token, + lastAccess: moment().toISOString() + } as Account + + // Complete login process + const me = await User.me() + account.data = me + const nethlinkExtension = account.data!.endpoints.extension.find((el) => el.type === 'nethlink') + if (!nethlinkExtension) { + // Clean up backend token and clear account state + try { + await Authentication.logout() + } catch (logoutError) { + Log.warning("Error during logout after unauthorized access:", logoutError) + } + account = undefined + throw new Error('User not authorized for NethLink') + } + return account + } else { + throw new Error('No token received after 2FA verification') + } + } catch (reason: any) { + if (reason.response?.status === 400) { + throw new Error('OTP invalid') + } else if (reason.message === 'User not authorized for NethLink') { + throw reason + } else { + console.error('2FA verification error:', reason) + throw new Error('Verification failed') + } + } + }, + logout: async () => { isFirstHeartbeat = false return new Promise(async (resolve) => { try { - await _POST('/webrest/authentication/logout', {}) + await _POST(`${API_BASE_PATH}/authentication/logout`, {}) } catch (e) { Log.warning("error during logout:", e) } finally { @@ -127,8 +198,9 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) } }) }, + phoneIslandTokenLogin: async (): Promise<{ username: string, token: string }> => - await _POST('/webrest/authentication/phone_island_token_login', { subtype: 'nethlink'}), + await _POST(`${API_BASE_PATH}/authentication/phone_island_token_login`, { subtype: 'nethlink' }), } const CustCard = {} @@ -143,7 +215,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) try { if (account) { const res = await _GET( - `/webrest/historycall/interval/user/${account.username}/${from}/${to}?offset=0&limit=15&sort=time%20desc&removeLostCalls=undefined` + `${API_BASE_PATH}/historycall/interval/user/${account.username}/${from}/${to}?offset=0&limit=15&sort=time%20desc&removeLostCalls=undefined` ) return res } else { @@ -166,12 +238,12 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) view: 'all' | 'company' | 'person' = 'all' ) => { const s = await _GET( - `/webrest/phonebook/search/${search.trim()}?offset=${offset}&limit=${pageSize}&view=${view}` + `${API_BASE_PATH}/phonebook/search/${search.trim()}?offset=${offset}&limit=${pageSize}&view=${view}` ) return s }, getSpeeddials: async () => { - return await _GET('/webrest/phonebook/speeddials') + return await _GET(`${API_BASE_PATH}/phonebook/speeddials`) }, ///SPEEDDIALS createSpeeddial: async (create: NewContactType) => { @@ -186,7 +258,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) notes: SpeeddialTypes.BASIC } try { - await _POST(`/webrest/phonebook/create`, newSpeedDial) + await _POST(`${API_BASE_PATH}/phonebook/create`, newSpeedDial) return newSpeedDial } catch (e) { Log.warning('error during createSpeeddial', e) @@ -205,7 +277,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) notes: SpeeddialTypes.FAVOURITES } try { - await _POST(`/webrest/phonebook/create`, newSpeedDial) + await _POST(`${API_BASE_PATH}/phonebook/create`, newSpeedDial) return newSpeedDial } catch (e) { Log.warning('error during createFavourite', e) @@ -216,7 +288,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) const editedSpeedDial = Object.assign({}, updatedContact) editedSpeedDial.id = editedSpeedDial.id?.toString() try { - await _POST(`/webrest/phonebook/modify_cticontact`, editedSpeedDial) + await _POST(`${API_BASE_PATH}/phonebook/modify_cticontact`, editedSpeedDial) return editedSpeedDial } catch (e) { Log.warning('error during updateSpeeddialBy', e) @@ -229,12 +301,12 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) editedSpeedDial.speeddial_num = edit.speeddial_num editedSpeedDial.name = edit.name editedSpeedDial.id = editedSpeedDial.id?.toString() - await _POST(`/webrest/phonebook/modify_cticontact`, editedSpeedDial) + await _POST(`${API_BASE_PATH}/phonebook/modify_cticontact`, editedSpeedDial) return editedSpeedDial } }, deleteSpeeddial: async (obj: { id: string }) => { - await _POST(`/webrest/phonebook/delete_cticontact`, { id: '' + obj.id }) + await _POST(`${API_BASE_PATH}/phonebook/delete_cticontact`, { id: '' + obj.id }) return obj }, //CONTACTS @@ -254,7 +326,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) selectedPrefNum: 'extension', kind: 'person' } - await _POST(`/webrest/phonebook/create`, newContact) + await _POST(`${API_BASE_PATH}/phonebook/create`, newContact) return newContact }, updateContact: async (edit: NewContactType, current: ContactType) => { @@ -263,18 +335,18 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) newSpeedDial.speeddial_num = edit.speeddial_num newSpeedDial.name = edit.name newSpeedDial.id = newSpeedDial.id?.toString() - await _POST(`/webrest/phonebook/modify_cticontact`, newSpeedDial) + await _POST(`${API_BASE_PATH}/phonebook/modify_cticontact`, newSpeedDial) return current } }, deleteContact: async (obj: { id: string }) => { - await _POST(`/webrest/phonebook/delete_cticontact`, obj) + await _POST(`${API_BASE_PATH}/phonebook/delete_cticontact`, obj) } } const Profiling = { all: async () => { - return await _GET(`/webrest/profiling/all`) + return await _GET(`${API_BASE_PATH}/profiling/all`) } } @@ -282,7 +354,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) const User = { me: async (): Promise => { - const data: AccountData = await _GET('/webrest/user/me') + const data: AccountData = await _GET(`${API_BASE_PATH}/user/me`) data.mainextension = data!.endpoints.mainextension[0].id const ext = data.endpoints.extension.find((e) => e.type === 'nethlink') //the !loggedAccount flag allow to reduce the invocation only to the backend module and only at the first login @@ -293,14 +365,14 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) } return data }, - all: async () => await _GET('/webrest/user/all'), - all_avatars: async () => await _GET('/webrest/user/all_avatars'), - all_endpoints: async () => await _GET('/webrest/user/endpoints/all'), - heartbeat: async (extension: string, username: string) => await _POST('/webrest/user/nethlink', { extension, username }), + all: async () => await _GET(`${API_BASE_PATH}/user/all`), + all_avatars: async () => await _GET(`${API_BASE_PATH}/user/all_avatars`), + all_endpoints: async () => await _GET(`${API_BASE_PATH}/user/endpoints/all`), + heartbeat: async (extension: string, username: string) => await _POST(`${API_BASE_PATH}/user/nethlink`, { extension, username }), default_device: async (deviceIdInformation: Extension, force = false): Promise => { try { if (account?.data?.default_device.type !== 'physical' || force) { - await _POST('/webrest/user/default_device', { id: deviceIdInformation.id }) + await _POST(`${API_BASE_PATH}/user/default_device`, { id: deviceIdInformation.id }) return true } } catch (e) { @@ -308,7 +380,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) } return false; }, - setPresence: async (status: StatusTypes, to?: string) => await _POST('/webrest/user/presence', { status, ...(to ? { to } : {}) }) + setPresence: async (status: StatusTypes, to?: string) => await _POST(`${API_BASE_PATH}/user/presence`, { status, ...(to ? { to } : {}) }) } const Voicemail = {} diff --git a/src/shared/useNetwork.ts b/src/shared/useNetwork.ts index 235146f5..b5e32488 100644 --- a/src/shared/useNetwork.ts +++ b/src/shared/useNetwork.ts @@ -10,7 +10,7 @@ export const useNetwork = () => { return response.data } catch (e: any) { const err: AxiosError = e - if (!path.includes('login')) + if (!path.includes('login') && !path.includes('2fa/verify-otp')) Log.error('during fetch POST', err.name, err.code, err.message, path, config, data) throw e } diff --git a/src/shared/utils/jwt.ts b/src/shared/utils/jwt.ts new file mode 100644 index 00000000..35033546 --- /dev/null +++ b/src/shared/utils/jwt.ts @@ -0,0 +1,75 @@ +/** + * JWT utilities for token decoding and validation + */ + +export interface JWTPayload { + username: string + '2fa'?: boolean + exp?: number + iat?: number + [key: string]: any +} + +/** + * Base64 decode that works in both browser and Node.js + */ +function base64Decode(str: string): string { + if (typeof window !== 'undefined' && typeof window.atob === 'function') { + // Browser environment + return window.atob(str) + } else { + // Node.js environment + return Buffer.from(str, 'base64').toString('utf-8') + } +} + +/** + * Decode JWT token (client-side only) + * @param token JWT token string + * @returns Decoded payload or null if invalid + */ +export function decodeJWT(token: string): JWTPayload | null { + try { + // Split the token into parts + const parts = token.split('.') + if (parts.length !== 3) { + return null + } + + // Decode the payload (second part) + const payload = parts[1] + // Add padding if needed + const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4) + + const decodedPayload = base64Decode(paddedPayload) + return JSON.parse(decodedPayload) as JWTPayload + } catch (error) { + console.error('Error decoding JWT:', error) + return null + } +} + +/** + * Check if JWT token is expired + * @param token JWT token string + * @returns true if expired, false if valid + */ +export function isJWTExpired(token: string): boolean { + const payload = decodeJWT(token) + if (!payload || !payload.exp) { + return true + } + + const now = Math.floor(Date.now() / 1000) + return payload.exp < now +} + +/** + * Check if 2FA is required from JWT token + * @param token JWT token string + * @returns true if 2FA is required + */ +export function requires2FA(token: string): boolean { + const payload = decodeJWT(token) + return payload?.['2fa'] === true +} From 9e1727480f9ffedc11ee46ea6890976fc17faff7 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Fri, 19 Sep 2025 13:01:22 +0200 Subject: [PATCH 03/20] fix: use /webrest as fallback with no middleware --- src/shared/types.ts | 1 + src/shared/useNethVoiceAPI.ts | 329 +++++++++++++++++++++++++++------- 2 files changed, 262 insertions(+), 68 deletions(-) diff --git a/src/shared/types.ts b/src/shared/types.ts index b885476f..395502e5 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -32,6 +32,7 @@ export type Account = { data?: AccountData, shortcut?: string preferredDevices?: PreferredDevices + apiBasePath?: string // Store which API path works for this account } export type PreferredDevices = { diff --git a/src/shared/useNethVoiceAPI.ts b/src/shared/useNethVoiceAPI.ts index 519beb14..1556ecc6 100644 --- a/src/shared/useNethVoiceAPI.ts +++ b/src/shared/useNethVoiceAPI.ts @@ -1,5 +1,6 @@ import moment from 'moment' +import hmacSHA1 from 'crypto-js/hmac-sha1' import { Account, NewContactType, @@ -18,13 +19,51 @@ import { useNetwork } from './useNetwork' import { SpeeddialTypes } from './constants' import { requires2FA } from '@shared/utils/jwt' -// Base path for API endpoints -const API_BASE_PATH = '/api' +// Base paths for API endpoints (fallback from /api to /webrest) +const PRIMARY_API_BASE_PATH = '/api' +const FALLBACK_API_BASE_PATH = '/webrest' export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) => { const { GET, POST } = useNetwork() let isFirstHeartbeat = true let account: Account | undefined = loggedAccount || undefined + // Use account's stored API path preference, or default to primary + let currentApiBasePath = account?.apiBasePath || PRIMARY_API_BASE_PATH + + if (account?.apiBasePath) { + Log.debug(`Using stored API path for ${account.username}: ${account.apiBasePath}`) + } else { + Log.debug(`Using default API path: ${PRIMARY_API_BASE_PATH}`) + } + + function buildApiPath(endpoint: string): string { + const result = (() => { + if (currentApiBasePath === FALLBACK_API_BASE_PATH) { + // Special mapping for webrest endpoints + if (endpoint === '/login') { + return `${FALLBACK_API_BASE_PATH}/authentication/login` + } + if (endpoint === '/authentication/logout') { + return `${FALLBACK_API_BASE_PATH}/authentication/logout` + } + if (endpoint === '/authentication/phone_island_token_login') { + return `${FALLBACK_API_BASE_PATH}/authentication/phone_island_token_login` + } + // For other endpoints, use webrest format + return `${FALLBACK_API_BASE_PATH}${endpoint}` + } + // Primary API path + return `${currentApiBasePath}${endpoint}` + })() + + Log.debug(`buildApiPath(${endpoint}) -> ${result} (currentApiBasePath: ${currentApiBasePath})`) + return result + } + + function _toHash(username: string, password: string, nonce: string) { + const token = nonce ? hmacSHA1(`${username}:${password}:${nonce}`, password).toString() : '' + return token + } function _joinUrl(url: string) { const path = `https://${account!.host}${url}` @@ -40,11 +79,14 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) } if (hasAuth) { - // Use JWT Bearer token only if (account!.jwtToken) { + // JWT Bearer token for /api headers.Authorization = `Bearer ${account!.jwtToken}` + } else if (account!.accessToken) { + // Hash-based token for /webrest + headers.Authorization = `${account!.username}:${account!.accessToken}` } else { - throw new Error('No JWT token available for authentication') + throw new Error('No authentication token available') } } @@ -55,27 +97,120 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) try { return (await GET(_joinUrl(path), _getHeaders(hasAuth))) } catch (e) { + // Check if we should try fallback path for critical endpoints + if (shouldTryFallback(path, e)) { + return await _GETWithFallback(path, hasAuth) + } console.error(e) throw e } } + async function _GETWithFallback(path: string, hasAuth = true): Promise { + const originalPath = path + + // Switch to fallback path and rebuild the correct path + currentApiBasePath = FALLBACK_API_BASE_PATH + if (account) { + account.apiBasePath = FALLBACK_API_BASE_PATH + } + + // Extract the endpoint from the original path and rebuild with fallback + const endpoint = path.replace(PRIMARY_API_BASE_PATH, '') + const fallbackPath = buildApiPath(endpoint) + + try { + Log.debug(`Trying fallback path: ${fallbackPath}`) + const result = await GET(_joinUrl(fallbackPath), _getHeaders(hasAuth)) + + Log.info('Switched to fallback API path: /webrest') + return result + } catch (fallbackError) { + Log.warning('Fallback also failed:', fallbackError) + console.error(fallbackError) + throw fallbackError + } + } + async function _POST(path: string, data?: object, hasAuth = true): Promise { try { return (await POST(_joinUrl(path), data, _getHeaders(hasAuth))) } catch (e) { + // Check if we should try fallback path for critical endpoints + if (shouldTryFallback(path, e)) { + return await _POSTWithFallback(path, data, hasAuth) + } + if (!path.includes('login') && !path.includes('2fa/verify-otp')) console.error(e) throw e } } + function shouldTryFallback(path: string, error: any): boolean { + // Only try fallback if we're using primary path + if (currentApiBasePath !== PRIMARY_API_BASE_PATH) { + return false + } + + // Try fallback for connection errors (404, 503, or network failures) + const isConnectionError = error?.response?.status === 404 || error?.response?.status === 503 || !error?.response + + // For auth endpoints, always try fallback on connection errors + const isCriticalAuthEndpoint = path.includes('login') || path.includes('2fa/verify-otp') + if (isCriticalAuthEndpoint && isConnectionError) { + return true + } + + // For other endpoints, try fallback only on 404 (endpoint not found) + // This indicates the API structure is different (middleware vs webrest) + if (error?.response?.status === 404) { + return true + } + + return false + } + + async function _POSTWithFallback(path: string, data?: object, hasAuth = true): Promise { + const originalPath = path + + // Switch to fallback path + currentApiBasePath = FALLBACK_API_BASE_PATH + if (account) { + account.apiBasePath = FALLBACK_API_BASE_PATH + } + Log.info('Switched to fallback API path: /webrest') + + // For login endpoint, we need special handling for webrest authentication + if (originalPath.includes('/login')) { + Log.debug('Login fallback: switching to hash-based authentication') + // The login function will now use webrest logic since currentApiBasePath is changed + // We need to throw the original error to let the login function handle the retry + throw new Error('FALLBACK_TO_WEBREST') + } + + // For other endpoints, try the direct fallback + const endpoint = path.replace(PRIMARY_API_BASE_PATH, '') + const fallbackPath = buildApiPath(endpoint) + + try { + Log.debug(`Trying fallback path: ${fallbackPath}`) + const result = await POST(_joinUrl(fallbackPath), data, _getHeaders(hasAuth)) + return result + } catch (fallbackError) { + Log.warning('Fallback also failed:', fallbackError) + if (!originalPath.includes('login') && !originalPath.includes('2fa/verify-otp')) + console.error(fallbackError) + throw fallbackError + } + } + const AstProxy = { - groups: async () => await _GET(`${API_BASE_PATH}/astproxy/opgroups`), - extensions: async (): Promise => await _GET(`${API_BASE_PATH}/astproxy/extensions`), - getQueues: async () => await _GET(`${API_BASE_PATH}/astproxy/queues`), - getParkings: async () => await _GET(`${API_BASE_PATH}/astproxy/parkings`), - pickupParking: async (parkInformation: any) => await _POST(`${API_BASE_PATH}/astproxy/pickup_parking`, parkInformation) + groups: async () => await _GET(buildApiPath('/astproxy/opgroups')), + extensions: async (): Promise => await _GET(buildApiPath('/astproxy/extensions')), + getQueues: async () => await _GET(buildApiPath('/astproxy/queues')), + getParkings: async () => await _GET(buildApiPath('/astproxy/parkings')), + pickupParking: async (parkInformation: any) => await _POST(buildApiPath('/astproxy/pickup_parking'), parkInformation) } @@ -91,48 +226,101 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) theme: 'system' } - try { - // Try JWT login - const response = await _POST(`${API_BASE_PATH}/login`, data, false) - - if (response.token) { - // JWT authentication successful - account = { - ...account, - jwtToken: response.token, - lastAccess: moment().toISOString() - } as Account - - // Check if 2FA is required - if (requires2FA(response.token)) { - // Return account with JWT token but mark as requiring 2FA - return account + // Try JWT authentication first (for /api) + if (currentApiBasePath === PRIMARY_API_BASE_PATH) { + try { + const response = await _POST(buildApiPath('/login'), data, false) + + if (response.token) { + // JWT authentication successful + account = { + ...account, + jwtToken: response.token, + lastAccess: moment().toISOString(), + apiBasePath: PRIMARY_API_BASE_PATH + } as Account + + // Check if 2FA is required + if (requires2FA(response.token)) { + // Return account with JWT token but mark as requiring 2FA + return account + } else { + // Complete login process + const me = await User.me() + account.data = me + const nethlinkExtension = account.data!.endpoints.extension.find((el) => el.type === 'nethlink') + if (!nethlinkExtension) { + throw new Error('User not authorized for NethLink') + } + return account + } + } else { + throw new Error('No token received') + } + } catch (reason: any) { + // Check if this is a fallback trigger + if (reason.message === 'FALLBACK_TO_WEBREST') { + Log.debug('Retrying login with webrest authentication') + // Fallback has set currentApiBasePath to FALLBACK_API_BASE_PATH + // Continue to webrest authentication below } else { - // Complete login process - const me = await User.me() - account.data = me - const nethlinkExtension = account.data!.endpoints.extension.find((el) => el.type === 'nethlink') - if (!nethlinkExtension) { - throw new Error('User not authorized for NethLink') + // Handle other specific error cases + if (reason.response?.status === 401) { + throw new Error('Wrong username or password') + } else if (reason.response?.status === 404) { + throw new Error('Network connection lost') + } else if (reason.message === 'User not authorized for NethLink') { + throw reason + } else { + console.error('Login error:', reason) + throw new Error('Unauthorized') } - return account } - } else { - throw new Error('No token received') - } - } catch (reason: any) { - // Handle specific error cases - if (reason.response?.status === 401) { - throw new Error('Wrong username or password') - } else if (reason.response?.status === 404) { - throw new Error('Network connection lost') - } else if (reason.message === 'User not authorized for NethLink') { - throw reason - } else { - console.error('Login error:', reason) - throw new Error('Unauthorized') } } + + // Hash-based authentication for /webrest (either direct or after fallback) + if (currentApiBasePath === FALLBACK_API_BASE_PATH) { + // Hash-based authentication for /webrest + return new Promise((resolve, reject) => { + _POST(buildApiPath('/login'), data, false).catch(async (reason) => { + try { + if (reason.response?.status === 401 && reason.response?.headers['www-authenticate']) { + const digest = reason.response.headers['www-authenticate'] + const nonce = digest.split(' ')[1] + if (nonce) { + const accessToken = _toHash(username, password, nonce) + account = { + ...account, + accessToken, + lastAccess: moment().toISOString(), + apiBasePath: FALLBACK_API_BASE_PATH + } as Account + const me = await User.me() + account.data = me + const nethlinkExtension = account.data!.endpoints.extension.find((el) => el.type === 'nethlink') + if (!nethlinkExtension) { + reject(new Error('User not authorized for NethLink')) + } else { + resolve(account) + } + } else { + console.error('undefined nonce response') + reject(new Error('Unauthorized')) + } + } else { + console.error('Login error:', reason) + reject(new Error('Unauthorized')) + } + } catch (e) { + reject(e) + } + }) + }) + } + + // This should never be reached + throw new Error('No authentication method available') }, verify2FA: async (otp: string, tempAccount: Account | undefined): Promise => { @@ -143,7 +331,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) } try { - const response = await _POST(`${API_BASE_PATH}/2fa/verify-otp`, { + const response = await _POST(buildApiPath('/2fa/verify-otp'), { otp, username: account.username }, true) @@ -190,17 +378,22 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) isFirstHeartbeat = false return new Promise(async (resolve) => { try { - await _POST(`${API_BASE_PATH}/authentication/logout`, {}) + await _POST(buildApiPath('/authentication/logout'), {}) } catch (e) { Log.warning("error during logout:", e) } finally { + // Reset to primary API path for next login attempt + currentApiBasePath = PRIMARY_API_BASE_PATH + if (account) { + account.apiBasePath = PRIMARY_API_BASE_PATH + } resolve() } }) }, phoneIslandTokenLogin: async (): Promise<{ username: string, token: string }> => - await _POST(`${API_BASE_PATH}/authentication/phone_island_token_login`, { subtype: 'nethlink' }), + await _POST(buildApiPath('/authentication/phone_island_token_login'), { subtype: 'nethlink' }), } const CustCard = {} @@ -215,7 +408,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) try { if (account) { const res = await _GET( - `${API_BASE_PATH}/historycall/interval/user/${account.username}/${from}/${to}?offset=0&limit=15&sort=time%20desc&removeLostCalls=undefined` + buildApiPath(`/historycall/interval/user/${account.username}/${from}/${to}?offset=0&limit=15&sort=time%20desc&removeLostCalls=undefined`) ) return res } else { @@ -238,12 +431,12 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) view: 'all' | 'company' | 'person' = 'all' ) => { const s = await _GET( - `${API_BASE_PATH}/phonebook/search/${search.trim()}?offset=${offset}&limit=${pageSize}&view=${view}` + buildApiPath(`/phonebook/search/${search.trim()}?offset=${offset}&limit=${pageSize}&view=${view}`) ) return s }, getSpeeddials: async () => { - return await _GET(`${API_BASE_PATH}/phonebook/speeddials`) + return await _GET(buildApiPath('/phonebook/speeddials')) }, ///SPEEDDIALS createSpeeddial: async (create: NewContactType) => { @@ -258,7 +451,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) notes: SpeeddialTypes.BASIC } try { - await _POST(`${API_BASE_PATH}/phonebook/create`, newSpeedDial) + await _POST(buildApiPath('/phonebook/create'), newSpeedDial) return newSpeedDial } catch (e) { Log.warning('error during createSpeeddial', e) @@ -277,7 +470,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) notes: SpeeddialTypes.FAVOURITES } try { - await _POST(`${API_BASE_PATH}/phonebook/create`, newSpeedDial) + await _POST(buildApiPath('/phonebook/create'), newSpeedDial) return newSpeedDial } catch (e) { Log.warning('error during createFavourite', e) @@ -288,7 +481,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) const editedSpeedDial = Object.assign({}, updatedContact) editedSpeedDial.id = editedSpeedDial.id?.toString() try { - await _POST(`${API_BASE_PATH}/phonebook/modify_cticontact`, editedSpeedDial) + await _POST(buildApiPath('/phonebook/modify_cticontact'), editedSpeedDial) return editedSpeedDial } catch (e) { Log.warning('error during updateSpeeddialBy', e) @@ -301,12 +494,12 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) editedSpeedDial.speeddial_num = edit.speeddial_num editedSpeedDial.name = edit.name editedSpeedDial.id = editedSpeedDial.id?.toString() - await _POST(`${API_BASE_PATH}/phonebook/modify_cticontact`, editedSpeedDial) + await _POST(`${currentApiBasePath}/phonebook/modify_cticontact`, editedSpeedDial) return editedSpeedDial } }, deleteSpeeddial: async (obj: { id: string }) => { - await _POST(`${API_BASE_PATH}/phonebook/delete_cticontact`, { id: '' + obj.id }) + await _POST(buildApiPath('/phonebook/delete_cticontact'), { id: '' + obj.id }) return obj }, //CONTACTS @@ -326,7 +519,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) selectedPrefNum: 'extension', kind: 'person' } - await _POST(`${API_BASE_PATH}/phonebook/create`, newContact) + await _POST(`${currentApiBasePath}/phonebook/create`, newContact) return newContact }, updateContact: async (edit: NewContactType, current: ContactType) => { @@ -335,18 +528,18 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) newSpeedDial.speeddial_num = edit.speeddial_num newSpeedDial.name = edit.name newSpeedDial.id = newSpeedDial.id?.toString() - await _POST(`${API_BASE_PATH}/phonebook/modify_cticontact`, newSpeedDial) + await _POST(`${currentApiBasePath}/phonebook/modify_cticontact`, newSpeedDial) return current } }, deleteContact: async (obj: { id: string }) => { - await _POST(`${API_BASE_PATH}/phonebook/delete_cticontact`, obj) + await _POST(buildApiPath('/phonebook/delete_cticontact'), obj) } } const Profiling = { all: async () => { - return await _GET(`${API_BASE_PATH}/profiling/all`) + return await _GET(buildApiPath('/profiling/all')) } } @@ -354,7 +547,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) const User = { me: async (): Promise => { - const data: AccountData = await _GET(`${API_BASE_PATH}/user/me`) + const data: AccountData = await _GET(buildApiPath('/user/me')) data.mainextension = data!.endpoints.mainextension[0].id const ext = data.endpoints.extension.find((e) => e.type === 'nethlink') //the !loggedAccount flag allow to reduce the invocation only to the backend module and only at the first login @@ -365,14 +558,14 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) } return data }, - all: async () => await _GET(`${API_BASE_PATH}/user/all`), - all_avatars: async () => await _GET(`${API_BASE_PATH}/user/all_avatars`), - all_endpoints: async () => await _GET(`${API_BASE_PATH}/user/endpoints/all`), - heartbeat: async (extension: string, username: string) => await _POST(`${API_BASE_PATH}/user/nethlink`, { extension, username }), + all: async () => await _GET(buildApiPath('/user/all')), + all_avatars: async () => await _GET(buildApiPath('/user/all_avatars')), + all_endpoints: async () => await _GET(buildApiPath('/user/endpoints/all')), + heartbeat: async (extension: string, username: string) => await _POST(buildApiPath('/user/nethlink'), { extension, username }), default_device: async (deviceIdInformation: Extension, force = false): Promise => { try { if (account?.data?.default_device.type !== 'physical' || force) { - await _POST(`${API_BASE_PATH}/user/default_device`, { id: deviceIdInformation.id }) + await _POST(buildApiPath('/user/default_device'), { id: deviceIdInformation.id }) return true } } catch (e) { @@ -380,7 +573,7 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) } return false; }, - setPresence: async (status: StatusTypes, to?: string) => await _POST(`${API_BASE_PATH}/user/presence`, { status, ...(to ? { to } : {}) }) + setPresence: async (status: StatusTypes, to?: string) => await _POST(buildApiPath('/user/presence'), { status, ...(to ? { to } : {}) }) } const Voicemail = {} From b2ab3f28c9382c7ca3dc1e76ab646660acd4e336 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Mon, 22 Sep 2025 09:51:12 +0200 Subject: [PATCH 04/20] chore: update phone-island --- package-lock.json | 144 ++-------------------------------------------- package.json | 2 +- 2 files changed, 5 insertions(+), 141 deletions(-) diff --git a/package-lock.json b/package-lock.json index dd0b217c..f0066bcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.15.11", + "@nethesis/phone-island": "^0.16.0", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -3177,72 +3177,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.1.tgz", - "integrity": "sha512-YfASnrhJ+ve6Q43ZiDwmpBgYgi2u0bYjeAVi2tDfN7YWAKO8X9EEOuPGtqbJpPLM6TfAHimghICjWe2eaJ8BAg==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@emotion/is-prop-valid": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", @@ -5639,9 +5573,9 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.15.11", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.15.11.tgz", - "integrity": "sha512-+Jso0npfbdOjC9+pPSsZmZYpNepbWRQ7dagRDIpFsi1bVAk0S/wh7+0KbUW5OVeaHbRICjLagl7jLAGP4/Q4MQ==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.16.0.tgz", + "integrity": "sha512-tpIwWiDI0jxtqpxLXkwGUBnEzew2LsuopCTaEK0fvGvV3n+JjUXknVDsQIA97UqqirTPBrFfLuGDfEnc8c+VfQ==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { @@ -6045,37 +5979,6 @@ "node": ">= 8" } }, - "node_modules/@nethesis/phone-island/node_modules/type-fest": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.29.0.tgz", - "integrity": "sha512-RPYt6dKyemXJe7I6oNstcH24myUGSReicxcHTvCLgzm4e0n8y05dGvcGB15/SoPRBmhlMthWQ9pvKyL81ko8nQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "peer": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nethesis/phone-island/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@nethesis/phone-island/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -11456,15 +11359,6 @@ "sha.js": "^2.4.8" } }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -23740,36 +23634,6 @@ "dev": true, "license": "MIT" }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 1c511cf9..84fa9cee 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.15.11", + "@nethesis/phone-island": "^0.16.0", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", From ebdc4e888c2960630a39e4dfdd05ce082993cf0b Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Mon, 22 Sep 2025 15:28:39 +0200 Subject: [PATCH 05/20] fix: removed useless inline notification --- .../SettingsSettings/SettingsDevicesDialog.tsx | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx index 99fac417..f603e834 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx @@ -169,10 +169,6 @@ export function SettingsDeviceDialog() { videoInput: t('TopBar.Camera'), } - const isDeviceUnavailable = - account?.data?.default_device?.type == 'webrtc' || - account?.data?.mainPresence !== 'online' - return ( <> {/* Background color */} @@ -276,23 +272,12 @@ export function SettingsDeviceDialog() {
))} - {/* Inline notification */} - {isDeviceUnavailable && ( - -

{t('Devices.Inline warning message devices')}

-
- )} {/* Action buttons */}
@@ -313,4 +298,4 @@ export function SettingsDeviceDialog() {
) -} +} \ No newline at end of file From 80b6a3b2d84757182a60df591055536702c8edbb Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 29 Sep 2025 10:08:16 +0200 Subject: [PATCH 06/20] fix: added missing toLowerCase for textIput inside login modal --- .../src/components/pageComponents/login/LoginForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/pageComponents/login/LoginForm.tsx b/src/renderer/src/components/pageComponents/login/LoginForm.tsx index a68e0b20..a4408e4a 100644 --- a/src/renderer/src/components/pageComponents/login/LoginForm.tsx +++ b/src/renderer/src/components/pageComponents/login/LoginForm.tsx @@ -434,7 +434,9 @@ export const LoginForm = ({ onError, handleRefreshConnection }) => { }} /> value?.toLowerCase() || '', + })} type='text' label={t('Login.Username') as string} helper={errors.username?.message || undefined} From 09c1a729df7f5c881b607ee964ea26932db278e7 Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 29 Sep 2025 10:09:18 +0200 Subject: [PATCH 07/20] chore(dep): update phone-island --- package-lock.json | 41 ++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0066bcb..87dd508b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.16.0", + "@nethesis/phone-island": "^0.16.1", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -5573,15 +5573,15 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.16.0.tgz", - "integrity": "sha512-tpIwWiDI0jxtqpxLXkwGUBnEzew2LsuopCTaEK0fvGvV3n+JjUXknVDsQIA97UqqirTPBrFfLuGDfEnc8c+VfQ==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.16.1.tgz", + "integrity": "sha512-haspjOhG8pHeZrysQWjcJmw7NRSVZkLe9aE/ge5RmzU8DodESP/P5B2EeLSutVCzGeDCx5iLTJTeiAm05i2ZyA==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { "@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", - "@headlessui/react": "^2.2.0", + "@headlessui/react": "^2.2.8", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", "@rematch/core": "^2.2.0", @@ -5979,6 +5979,37 @@ "node": ">= 8" } }, + "node_modules/@nethesis/phone-island/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "peer": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nethesis/phone-island/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@nethesis/phone-island/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index 84fa9cee..3229631f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.16.0", + "@nethesis/phone-island": "^0.16.1", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", From 8aeae1c4c57f5b8dab804d40fc8bdabd979eb257 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 1 Oct 2025 10:57:58 +0200 Subject: [PATCH 08/20] chore: update phone-island --- package-lock.json | 100 +++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 59 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87dd508b..610c40c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.16.1", + "@nethesis/phone-island": "^0.16.2", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -147,6 +147,7 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -878,6 +879,7 @@ "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, @@ -1801,6 +1803,7 @@ "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-module-imports": "^7.25.9", @@ -3183,6 +3186,7 @@ "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -3789,6 +3793,7 @@ "integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.1" }, @@ -4960,6 +4965,7 @@ "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/core": "^0.22.12" } @@ -4996,6 +5002,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5008,6 +5015,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5032,6 +5040,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12", "tinycolor2": "^1.6.0" @@ -5075,6 +5084,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5198,6 +5208,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5210,6 +5221,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5225,6 +5237,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5573,9 +5586,9 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.16.1.tgz", - "integrity": "sha512-haspjOhG8pHeZrysQWjcJmw7NRSVZkLe9aE/ge5RmzU8DodESP/P5B2EeLSutVCzGeDCx5iLTJTeiAm05i2ZyA==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.16.2.tgz", + "integrity": "sha512-qR/4tssLeAcbRGYOM0UmPBVWxG19VWs7RcZe47Zz3YN8mtfYXYsVyjYKMQ4qNVmeFbFWDGV8UiIyQ81ZnxbczQ==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { @@ -5664,6 +5677,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5784,6 +5798,7 @@ "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5979,37 +5994,6 @@ "node": ">= 8" } }, - "node_modules/@nethesis/phone-island/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "peer": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nethesis/phone-island/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@nethesis/phone-island/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -6489,6 +6473,7 @@ "integrity": "sha512-Sj3nC/2X+bOBZeOf4jdJ00nhCcx9wLbVK9SOs6eFR4Y1qKXqRY0hGigbQgfTpCdjRFlwTHHfN3m41MlNvMhDgw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -7286,8 +7271,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -7405,6 +7389,7 @@ "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -7646,6 +7631,7 @@ "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -7737,6 +7723,7 @@ "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" } @@ -8085,6 +8072,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -8517,6 +8505,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8638,6 +8627,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -10030,6 +10020,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -11565,6 +11556,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12005,6 +11997,7 @@ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -12397,6 +12390,7 @@ "integrity": "sha512-OBa6xbQwFAm6gbbClkuGrxnOLbrPauv3yaugnGtIHsn7BvFSmMhZzhmcJQMrAGzDW2M3n/RmG/5mgOYUagqoeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.13", "builder-util": "26.0.13", @@ -12688,6 +12682,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^18.11.18", @@ -12925,7 +12920,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -12946,7 +12940,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -13410,6 +13403,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -13466,6 +13460,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -13508,6 +13503,7 @@ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -13543,6 +13539,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -14327,6 +14324,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16382,6 +16380,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.6" } @@ -17459,6 +17458,7 @@ "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -20247,7 +20247,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -20609,6 +20608,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -20943,6 +20943,7 @@ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -22215,6 +22216,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -23478,6 +23480,7 @@ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -23681,6 +23684,7 @@ "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -23850,6 +23854,7 @@ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -24098,6 +24103,7 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -24193,6 +24199,7 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -24214,6 +24221,7 @@ "integrity": "sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -26570,6 +26578,7 @@ "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.4.5", @@ -26981,6 +26990,7 @@ "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -27085,7 +27095,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -27160,7 +27169,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -27755,6 +27763,7 @@ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -28133,6 +28142,7 @@ "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -28290,6 +28300,7 @@ "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -28361,6 +28372,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -28418,6 +28430,7 @@ "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -28478,6 +28491,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29000,6 +29014,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29096,6 +29111,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/package.json b/package.json index 3229631f..12bc4120 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.16.1", + "@nethesis/phone-island": "^0.16.2", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", From d66bfa31f2744d19d403d242c3acc6992f049857 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 1 Oct 2025 11:53:03 +0200 Subject: [PATCH 09/20] feat: added warm-up for microphone on windows --- src/main/lib/ipcEvents.ts | 8 ++++++++ src/renderer/src/pages/PhoneIslandPage.tsx | 18 ++++++++++++++++++ src/shared/constants.ts | 1 + 3 files changed, 27 insertions(+) diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index 29417770..d152f648 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -268,6 +268,14 @@ export function registerIpcEvents() { Log.info('PhoneIsland is ready to receive events') const account = store.get('account') as Account + // Warm-up audio devices on Windows to prevent muted first call after reboot + if (process.platform === 'win32') { + setTimeout(() => { + Log.info('Sending WARMUP_AUDIO_DEVICES event on Windows') + PhoneIslandController.instance.window.emit(IPC_EVENTS.WARMUP_AUDIO_DEVICES) + }, 100) + } + setTimeout(() => { Log.info('Send CHANGE_PREFERRED_DEVICES event with', account.preferredDevices) AccountController.instance.updatePreferredDevice(account.preferredDevices) diff --git a/src/renderer/src/pages/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index 2bce7a30..5a40cd7c 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -81,6 +81,24 @@ export function PhoneIslandPage() { eventDispatch(PHONE_ISLAND_EVENTS['phone-island-audio-output-change'], { deviceId: devices.audioOutput }) }) + window.electron.receive(IPC_EVENTS.WARMUP_AUDIO_DEVICES, async () => { + Log.info('Warming up audio devices on Windows...') + try { + // Request microphone access to "wake up" the audio device on Windows + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: false + }) + + // Immediately stop the stream - we just needed to initialize the device + stream.getTracks().forEach(track => track.stop()) + + Log.info('Audio devices warm-up completed successfully') + } catch (error) { + Log.warning('Audio devices warm-up failed (this might be expected if permissions are denied):', error) + } + }) + window.electron.receive(IPC_EVENTS.TRANSFER_CALL, (to: string) => { eventDispatch(PHONE_ISLAND_EVENTS['phone-island-call-transfer'], { to diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 8a606aa3..e244f583 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -94,6 +94,7 @@ export enum IPC_EVENTS { COPY_TO_CLIPBOARD = "COPY_TO_CLIPBOARD", CHANGE_PREFERRED_DEVICES = "CHANGE_PREFERRED_DEVICES", PHONE_ISLAND_READY = "PHONE_ISLAND_READY", + WARMUP_AUDIO_DEVICES = "WARMUP_AUDIO_DEVICES", URL_OPEN = "URL_OPEN", } From ce75a42f855dc9ea32d7cd111e20109012311e1e Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 16 Oct 2025 10:11:19 +0200 Subject: [PATCH 10/20] chore(dep): update phone-island --- package-lock.json | 100 +++++++++++++++++++--------------------------- package.json | 2 +- 2 files changed, 43 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 610c40c2..5ea428d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.16.2", + "@nethesis/phone-island": "^0.17.1", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -147,7 +147,6 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -879,7 +878,6 @@ "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, @@ -1803,7 +1801,6 @@ "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-module-imports": "^7.25.9", @@ -3186,7 +3183,6 @@ "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -3793,7 +3789,6 @@ "integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.1" }, @@ -4965,7 +4960,6 @@ "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/core": "^0.22.12" } @@ -5002,7 +4996,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5015,7 +5008,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5040,7 +5032,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12", "tinycolor2": "^1.6.0" @@ -5084,7 +5075,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5208,7 +5198,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5221,7 +5210,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5237,7 +5225,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -5586,9 +5573,9 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.16.2.tgz", - "integrity": "sha512-qR/4tssLeAcbRGYOM0UmPBVWxG19VWs7RcZe47Zz3YN8mtfYXYsVyjYKMQ4qNVmeFbFWDGV8UiIyQ81ZnxbczQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.17.1.tgz", + "integrity": "sha512-pt8iLnD+cHMvq7+thGp6HeQ1+xhaOqyxDBnCZ4qrPdpXdDWodW+LfdeVZx4cqM87GbMb7UOC8fqS27azaFWN3Q==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { @@ -5677,7 +5664,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5798,7 +5784,6 @@ "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5994,6 +5979,37 @@ "node": ">= 8" } }, + "node_modules/@nethesis/phone-island/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "peer": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nethesis/phone-island/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@nethesis/phone-island/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -6473,7 +6489,6 @@ "integrity": "sha512-Sj3nC/2X+bOBZeOf4jdJ00nhCcx9wLbVK9SOs6eFR4Y1qKXqRY0hGigbQgfTpCdjRFlwTHHfN3m41MlNvMhDgw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -7271,7 +7286,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -7389,7 +7405,6 @@ "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -7631,7 +7646,6 @@ "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -7723,7 +7737,6 @@ "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" } @@ -8072,7 +8085,6 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -8505,7 +8517,6 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8627,7 +8638,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -10020,7 +10030,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -11556,7 +11565,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11997,7 +12005,6 @@ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -12390,7 +12397,6 @@ "integrity": "sha512-OBa6xbQwFAm6gbbClkuGrxnOLbrPauv3yaugnGtIHsn7BvFSmMhZzhmcJQMrAGzDW2M3n/RmG/5mgOYUagqoeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.0.13", "builder-util": "26.0.13", @@ -12682,7 +12688,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^18.11.18", @@ -12920,6 +12925,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -12940,6 +12946,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -13403,7 +13410,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -13460,7 +13466,6 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -13503,7 +13508,6 @@ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -13539,7 +13543,6 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -14324,7 +14327,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16380,7 +16382,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.6" } @@ -17458,7 +17459,6 @@ "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -20247,6 +20247,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -20608,7 +20609,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -20943,7 +20943,6 @@ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "*" } @@ -22216,7 +22215,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -23480,7 +23478,6 @@ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -23684,7 +23681,6 @@ "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -23854,7 +23850,6 @@ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -24103,7 +24098,6 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -24199,7 +24193,6 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -24221,7 +24214,6 @@ "integrity": "sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -26578,7 +26570,6 @@ "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.4.5", @@ -26990,7 +26981,6 @@ "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -27095,6 +27085,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -27169,6 +27160,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -27763,7 +27755,6 @@ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -28142,7 +28133,6 @@ "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -28300,7 +28290,6 @@ "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -28372,7 +28361,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -28430,7 +28418,6 @@ "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -28491,7 +28478,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29014,7 +29000,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29111,7 +29096,6 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/package.json b/package.json index 12bc4120..79bd51f8 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.16.2", + "@nethesis/phone-island": "^0.17.1", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", From 873bcbe9567640cb44e91d3fe235e4781ec7baf1 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Thu, 16 Oct 2025 11:59:10 +0200 Subject: [PATCH 11/20] fix: warm-up devices with echo test --- .../controllers/PhoneIslandController.ts | 58 ++++++++++++++++- src/main/lib/ipcEvents.ts | 63 ++++++++++++++++--- src/preload/index.ts | 2 + src/renderer/src/pages/PhoneIslandPage.tsx | 38 +++++------ 4 files changed, 133 insertions(+), 28 deletions(-) diff --git a/src/main/classes/controllers/PhoneIslandController.ts b/src/main/classes/controllers/PhoneIslandController.ts index 46f9600f..8ad4c4f8 100644 --- a/src/main/classes/controllers/PhoneIslandController.ts +++ b/src/main/classes/controllers/PhoneIslandController.ts @@ -10,6 +10,7 @@ import { Extension, Size } from '@shared/types' export class PhoneIslandController { static instance: PhoneIslandController window: PhoneIslandWindow + private isWarmingUp: boolean = false constructor() { PhoneIslandController.instance = this @@ -30,7 +31,8 @@ export class PhoneIslandController { if (h === 0 && w === 0) { window.hide() } else { - if (!window.isVisible()) { + // Don't show window during warm-up + if (!window.isVisible() && !this.isWarmingUp) { window.show() window.setAlwaysOnTop(true) } @@ -148,6 +150,60 @@ export class PhoneIslandController { } } + muteAudio() { + try { + const window = this.window.getWindow() + if (window && window.webContents) { + window.webContents.setAudioMuted(true) + Log.info('PhoneIsland audio muted') + } + } catch (e) { + Log.warning('error during muting PhoneIsland audio:', e) + } + } + + unmuteAudio() { + try { + const window = this.window.getWindow() + if (window && window.webContents) { + window.webContents.setAudioMuted(false) + Log.info('PhoneIsland audio unmuted') + } + } catch (e) { + Log.warning('error during unmuting PhoneIsland audio:', e) + } + } + + forceHide() { + try { + const window = this.window.getWindow() + if (window) { + this.isWarmingUp = true + window.hide() + Log.info('PhoneIsland window hidden') + } + } catch (e) { + Log.warning('error during force hiding PhoneIsland:', e) + } + } + + forceShow() { + try { + const window = this.window.getWindow() + if (window) { + this.isWarmingUp = false + // Only show if there's actually content (size > 0) + const bounds = window.getBounds() + if (bounds.width > 0 && bounds.height > 0) { + window.show() + window.setAlwaysOnTop(true) + Log.info('PhoneIsland window shown') + } + } + } catch (e) { + Log.warning('error during force showing PhoneIsland:', e) + } + } async safeQuit() { await this.logout() diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index d152f648..7d4d2507 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -18,6 +18,9 @@ import os from 'os' const { keyboard, Key } = require("@nut-tree-fork/nut-js"); +// Global flag to ensure audio warm-up runs only once per app session +let hasRunAudioWarmup = false + function onSyncEmitter( channel: IPC_EVENTS, asyncCallback: (...args: any[]) => Promise @@ -268,14 +271,6 @@ export function registerIpcEvents() { Log.info('PhoneIsland is ready to receive events') const account = store.get('account') as Account - // Warm-up audio devices on Windows to prevent muted first call after reboot - if (process.platform === 'win32') { - setTimeout(() => { - Log.info('Sending WARMUP_AUDIO_DEVICES event on Windows') - PhoneIslandController.instance.window.emit(IPC_EVENTS.WARMUP_AUDIO_DEVICES) - }, 100) - } - setTimeout(() => { Log.info('Send CHANGE_PREFERRED_DEVICES event with', account.preferredDevices) AccountController.instance.updatePreferredDevice(account.preferredDevices) @@ -283,6 +278,58 @@ export function registerIpcEvents() { }, 250) }) + // Handler for warm-up audio devices request from renderer + ipcMain.on(IPC_EVENTS.WARMUP_AUDIO_DEVICES, async () => { + // Check if warm-up has already been executed + if (hasRunAudioWarmup) { + Log.info('[WARMUP] Audio warm-up already executed, skipping...') + return + } + + // Mark as executed + hasRunAudioWarmup = true + + try { + Log.info('[WARMUP] Starting silent echo test to warm up audio devices...') + + // Hide the PhoneIsland window to prevent it from showing during warm-up + PhoneIslandController.instance.forceHide() + + // Mute the PhoneIsland window audio + PhoneIslandController.instance.muteAudio() + + // Wait a bit to ensure mute and hide are applied + await new Promise(resolve => setTimeout(resolve, 100)) + + // Start echo test call to *43 + Log.info('[WARMUP] Starting call to *43') + PhoneIslandController.instance.call('*43') + + // Keep the call active for 5 seconds to warm up devices + await new Promise(resolve => setTimeout(resolve, 1500)) + + // End the call + Log.info('[WARMUP] Ending echo test call') + PhoneIslandController.instance.window.emit(IPC_EVENTS.END_CALL) + + // Wait a bit before unmuting and showing + await new Promise(resolve => setTimeout(resolve, 250)) + + // Unmute the PhoneIsland window audio + PhoneIslandController.instance.unmuteAudio() + + // Show the window again (only if it has content) + PhoneIslandController.instance.forceShow() + + Log.info('[WARMUP] Audio warm-up completed successfully') + } catch (error) { + Log.error('[WARMUP] Error during audio warm-up:', error) + // Make sure to unmute and show even if there's an error + PhoneIslandController.instance.unmuteAudio() + PhoneIslandController.instance.forceShow() + } + }) + ipcMain.on(IPC_EVENTS.CHANGE_PREFERRED_DEVICES, (_, devices) => { Log.info('Received CHANGE_PREFERRED_DEVICES in ipcEvents:', devices) AccountController.instance.updatePreferredDevice(devices) diff --git a/src/preload/index.ts b/src/preload/index.ts index af4031cc..9b5f98bf 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,6 +11,7 @@ import { preloadBindings } from 'i18next-electron-fs-backend' export interface IElectronAPI { env: NodeJS.ProcessEnv, appVersion: string, + platform: string, // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise @@ -82,6 +83,7 @@ function setEmitter(event) { const api: IElectronAPI = { env: process.env, appVersion: process.env['APP_VERSION'] || '0.0.1', + platform: process.platform, i18nextElectronBackend: preloadBindings(ipcRenderer, process), //SYNC EMITTERS - expect response login: setEmitterSync(IPC_EVENTS.LOGIN), diff --git a/src/renderer/src/pages/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index 5a40cd7c..8cc2d361 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -32,6 +32,7 @@ export function PhoneIslandPage() { const isUrlOpening = useRef(false) const urlOpenAttempts = useRef(0) const urlOpenListenerRegistered = useRef(false) + const hasRunWarmup = useRef(false) useEffect(() => { resize(phoneIsalndSizes) @@ -76,26 +77,25 @@ export function PhoneIslandPage() { window.electron.receive(IPC_EVENTS.CHANGE_PREFERRED_DEVICES, (devices: PreferredDevices) => { Log.info('Received CHANGE_PREFERRED_DEVICES in PhoneIslandPage:', devices) - eventDispatch(PHONE_ISLAND_EVENTS['phone-island-audio-input-change'], { deviceId: devices.audioInput }) - eventDispatch(PHONE_ISLAND_EVENTS['phone-island-video-input-change'], { deviceId: devices.videoInput }) - eventDispatch(PHONE_ISLAND_EVENTS['phone-island-audio-output-change'], { deviceId: devices.audioOutput }) - }) - window.electron.receive(IPC_EVENTS.WARMUP_AUDIO_DEVICES, async () => { - Log.info('Warming up audio devices on Windows...') - try { - // Request microphone access to "wake up" the audio device on Windows - const stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: false - }) - - // Immediately stop the stream - we just needed to initialize the device - stream.getTracks().forEach(track => track.stop()) - - Log.info('Audio devices warm-up completed successfully') - } catch (error) { - Log.warning('Audio devices warm-up failed (this might be expected if permissions are denied):', error) + // Run audio warm-up first, only once after PhoneIsland is fully initialized + // Only on Windows/macOS where the issue occurs + if (!hasRunWarmup.current && (window.api?.platform === 'win32' || window.api?.platform === 'darwin')) { + hasRunWarmup.current = true + Log.info('Requesting audio warm-up from main process...') + window.electron.send(IPC_EVENTS.WARMUP_AUDIO_DEVICES) + + // Dispatch device changes after warm-up completes (after ~5 seconds) + setTimeout(() => { + eventDispatch(PHONE_ISLAND_EVENTS['phone-island-audio-input-change'], { deviceId: devices.audioInput }) + eventDispatch(PHONE_ISLAND_EVENTS['phone-island-video-input-change'], { deviceId: devices.videoInput }) + eventDispatch(PHONE_ISLAND_EVENTS['phone-island-audio-output-change'], { deviceId: devices.audioOutput }) + }, 5000) + } else { + // If warm-up already done or not needed, dispatch immediately + eventDispatch(PHONE_ISLAND_EVENTS['phone-island-audio-input-change'], { deviceId: devices.audioInput }) + eventDispatch(PHONE_ISLAND_EVENTS['phone-island-video-input-change'], { deviceId: devices.videoInput }) + eventDispatch(PHONE_ISLAND_EVENTS['phone-island-audio-output-change'], { deviceId: devices.audioOutput }) } }) From 1092d2d10a2579f469b3ac6cdb29482c46aa37e6 Mon Sep 17 00:00:00 2001 From: Antonio Date: Fri, 17 Oct 2025 15:55:05 +0200 Subject: [PATCH 12/20] fix: better transcription view management --- .../src/components/Nethesis/dropdown/index.tsx | 1 + src/renderer/src/pages/PhoneIslandPage.tsx | 13 +++++++------ src/shared/types.ts | 1 + 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/Nethesis/dropdown/index.tsx b/src/renderer/src/components/Nethesis/dropdown/index.tsx index 9aaef3de..ee450102 100644 --- a/src/renderer/src/components/Nethesis/dropdown/index.tsx +++ b/src/renderer/src/components/Nethesis/dropdown/index.tsx @@ -24,6 +24,7 @@ export interface DropdownProps extends ComponentProps<'div'> { | 'topVoicemail' | 'bottomVoicemail' | 'oneVoicemail' + | 'bottomTranscription' size?: 'full' } diff --git a/src/renderer/src/pages/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index 8cc2d361..ad0a6566 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -168,34 +168,35 @@ export function PhoneIslandPage() { const resize = (phoneIsalndSize: PhoneIslandSizes) => { if (!isOnLogout.current) { - const { width, height, top, bottom, left, right } = phoneIsalndSize.sizes + const { width, height, top, bottom, left, right, bottomTranscription } = phoneIsalndSize.sizes const w = Number(width.replace('px', '')) const h = Number(height.replace('px', '')) const r = Number((right ?? '0px').replace('px', '')) + const transcription = Number((bottomTranscription ?? '0px').replace('px', '')) const t = Number((top ?? '0px').replace('px', '')) const l = Number((left ?? '0px').replace('px', '')) const b = Number((bottom ?? '0px').replace('px', '')) const data = { width, height, - bottom: bottom ?? '0px', top: top ?? '0px', right: right ?? '0px', left: left ?? '0px', + transcription: bottomTranscription ?? '0px', } phoneIslandContainer.current?.setAttribute('style', ` width: calc(100vw + ${data.right} + ${data.left}); - height: calc(100vh + ${data.top} + ${data.bottom}); + height: calc(100vh + ${data.top} + ${data.bottom} + ${data.transcription}); `) - innerPIContainer.current?.setAttribute('style', ` margin-left: calc(${data.left} - ${data.right}); - `) //calc(${data.top} - ${data.bottom}) + margin-top: calc(${data.transcription} * -1); + `) window.api.resizePhoneIsland({ w: w + r + l, - h: h + t + b + h: h + t + b + transcription , }) } } diff --git a/src/shared/types.ts b/src/shared/types.ts index 395502e5..0debed3f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -493,6 +493,7 @@ export type sizeInformationType = { bottom?: string left?: string right?: string + bottomTranscription?: string } export type PhoneIslandSizes = { From 8f4f113c18955dd31f7551c2327242b0bcdcfafe Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 20 Oct 2025 14:20:58 +0200 Subject: [PATCH 13/20] chore(dep): update phone-island --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ea428d9..d09504cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.17.1", + "@nethesis/phone-island": "^0.17.2", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -5573,9 +5573,9 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.17.1.tgz", - "integrity": "sha512-pt8iLnD+cHMvq7+thGp6HeQ1+xhaOqyxDBnCZ4qrPdpXdDWodW+LfdeVZx4cqM87GbMb7UOC8fqS27azaFWN3Q==", + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.17.2.tgz", + "integrity": "sha512-b+9O6Wg2IfBLHkDe3nuXTVZK4VXnHxTcu9GcPMzXcns/T/e9K05hyaFBD4+xU8UpK5PtH8HWhyEMR0eBwsBrDg==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 79bd51f8..73637583 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.17.1", + "@nethesis/phone-island": "^0.17.2", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", From 7ccab2f1d0f12f7104b8daadfcb9d104ec27c457 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Mon, 20 Oct 2025 16:56:28 +0200 Subject: [PATCH 14/20] fix: await lockscreen after run application --- src/main/main.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/main.ts b/src/main/main.ts index 93ba78e5..c2f24f35 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -205,6 +205,20 @@ function attachOnReadyProcess() { //I display the splashscreen when the splashscreen component is correctly loaded. SplashScreenController.instance.window.addOnBuildListener(() => { + // On Windows, check if system is locked before starting + if (process.platform === 'win32') { + const idleState = powerMonitor.getSystemIdleState(1) + if (idleState === 'locked') { + Log.info('Windows is locked, waiting for unlock before starting app...') + // Wait for unlock-screen event before starting + powerMonitor.once('unlock-screen', () => { + Log.info('Windows unlocked, starting app now...') + setTimeout(startApp, 1000) + }) + return + } + } + // Normal flow: start after 1 second setTimeout(startApp, 1000) }) await attachProtocolListeners() From 27482b40cc96a1b0a7acd79babef52a00b00ca94 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 21 Oct 2025 15:22:23 +0200 Subject: [PATCH 15/20] fix: reuse valid JWT token with 2FA --- .../classes/controllers/AccountController.ts | 59 ++++++++++++++++++- src/shared/utils/jwt.ts | 8 ++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/main/classes/controllers/AccountController.ts b/src/main/classes/controllers/AccountController.ts index cf275e49..d82c1f22 100644 --- a/src/main/classes/controllers/AccountController.ts +++ b/src/main/classes/controllers/AccountController.ts @@ -6,7 +6,7 @@ import { useNethVoiceAPI } from '@shared/useNethVoiceAPI' import { useLogin } from '@shared/useLogin' import { NetworkController } from './NetworkController' import { getAccountUID } from '@shared/utils/utils' -import { requires2FA } from '@shared/utils/jwt' +import { requires2FA, isJWTExpired } from '@shared/utils/jwt' const defaultConfig: ConfigFile = { lastUser: undefined, @@ -77,8 +77,63 @@ export class AccountController { const decryptString = safeStorage.decryptString(psw) const _accountData = JSON.parse(decryptString) const password = _accountData.password + + // Check if saved token is still valid and doesn't require 2FA + if (lastLoggedAccount.jwtToken) { + if (!isJWTExpired(lastLoggedAccount.jwtToken)) { + // Token is still valid locally, check if it requires 2FA + if (requires2FA(lastLoggedAccount.jwtToken)) { + Log.info('auto login failed: 2FA required, user interaction needed') + return false + } + + // Token looks valid locally, but we need to verify with server + // The token might have been invalidated (e.g., 2FA disabled/enabled) + Log.info('auto login: validating saved token with server...') + + try { + // Make a simple API call to verify the token is still accepted by the server + // Use the /api/user/me endpoint to validate the token + const testUrl = `https://${lastLoggedAccount.host}/api/user/me` + const response = await NetworkController.instance.get(testUrl, { + headers: { + 'Authorization': `Bearer ${lastLoggedAccount.jwtToken}` + } + } as any) + + // If we get here, the token is valid on the server + Log.info('auto login: token validated with server, using saved token') + } catch (error: any) { + // Token was rejected by server (401/403) or network error + Log.info('auto login failed: saved token rejected by server', error?.response?.status || error?.message) + return false + } + + // Update store with the saved account (don't do a new login!) + // IMPORTANT: Preserve auth.lastUser and auth.lastUserCryptPsw so they are saved to disk + // IMPORTANT: Set connection: true to prevent "No internet connection" banner + store.updateStore({ + account: lastLoggedAccount, + theme: lastLoggedAccount.theme, + connection: true, + accountStatus: store.store.accountStatus || 'offline', + isCallsEnabled: store.store.isCallsEnabled || false, + auth: { + ...authAppData, + lastUser: authAppData.lastUser, + lastUserCryptPsw: authAppData.lastUserCryptPsw + } + }, 'autoLogin') + + return true + } else { + Log.info('auto login: saved token expired, need to re-login') + } + } + + // Token is expired or doesn't exist, do a new login const tempLoggedAccount = await this.NethVoiceAPI.Authentication.login(lastLoggedAccount.host, lastLoggedAccount.username, password) - + // Check if 2FA is required - auto-login should fail in this case if (tempLoggedAccount.jwtToken && requires2FA(tempLoggedAccount.jwtToken)) { Log.info('auto login failed: 2FA required, user interaction needed') diff --git a/src/shared/utils/jwt.ts b/src/shared/utils/jwt.ts index 35033546..e5f3ebca 100644 --- a/src/shared/utils/jwt.ts +++ b/src/shared/utils/jwt.ts @@ -67,9 +67,13 @@ export function isJWTExpired(token: string): boolean { /** * Check if 2FA is required from JWT token * @param token JWT token string - * @returns true if 2FA is required + * @returns true if 2FA is required (2FA enabled but OTP not yet verified) */ export function requires2FA(token: string): boolean { const payload = decodeJWT(token) - return payload?.['2fa'] === true + const has2FA = payload?.['2fa'] === true + const otpVerified = payload?.['otp_verified'] === true + + // 2FA is required only if it's enabled AND the OTP hasn't been verified yet + return has2FA && !otpVerified } From 8398e40ecf94ecf9c0df9436e62b484ea0d64514 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 22 Oct 2025 09:13:17 +0200 Subject: [PATCH 16/20] fix: higher timeout on echo test --- src/main/lib/ipcEvents.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index 7d4d2507..1e07e494 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -299,7 +299,7 @@ export function registerIpcEvents() { PhoneIslandController.instance.muteAudio() // Wait a bit to ensure mute and hide are applied - await new Promise(resolve => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 550)) // Start echo test call to *43 Log.info('[WARMUP] Starting call to *43') @@ -313,7 +313,7 @@ export function registerIpcEvents() { PhoneIslandController.instance.window.emit(IPC_EVENTS.END_CALL) // Wait a bit before unmuting and showing - await new Promise(resolve => setTimeout(resolve, 250)) + await new Promise(resolve => setTimeout(resolve, 550)) // Unmute the PhoneIsland window audio PhoneIslandController.instance.unmuteAudio() From d1125c7ea6e246c92a458f382029df989d04b798 Mon Sep 17 00:00:00 2001 From: Antonio Date: Fri, 7 Nov 2025 10:12:46 +0100 Subject: [PATCH 17/20] fix: call new init audio event --- package-lock.json | 8 ++-- package.json | 2 +- src/main/lib/ipcEvents.ts | 52 ---------------------- src/renderer/src/pages/PhoneIslandPage.tsx | 4 +- src/shared/constants.ts | 3 +- 5 files changed, 9 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index d09504cc..21b6cd13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.17.2", + "@nethesis/phone-island": "^0.17.3", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -5573,9 +5573,9 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.17.2.tgz", - "integrity": "sha512-b+9O6Wg2IfBLHkDe3nuXTVZK4VXnHxTcu9GcPMzXcns/T/e9K05hyaFBD4+xU8UpK5PtH8HWhyEMR0eBwsBrDg==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.17.3.tgz", + "integrity": "sha512-vwuNV9c8BmCBUMlmcIVQEBTqBDl0oREDh3D4xKQo1ApdHlHqSf1udGE8F4ILhTCQbunuinT/vAuOaEezpWOGvw==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 73637583..6a4dd2bd 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.17.2", + "@nethesis/phone-island": "^0.17.3", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index 1e07e494..82a5013f 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -278,58 +278,6 @@ export function registerIpcEvents() { }, 250) }) - // Handler for warm-up audio devices request from renderer - ipcMain.on(IPC_EVENTS.WARMUP_AUDIO_DEVICES, async () => { - // Check if warm-up has already been executed - if (hasRunAudioWarmup) { - Log.info('[WARMUP] Audio warm-up already executed, skipping...') - return - } - - // Mark as executed - hasRunAudioWarmup = true - - try { - Log.info('[WARMUP] Starting silent echo test to warm up audio devices...') - - // Hide the PhoneIsland window to prevent it from showing during warm-up - PhoneIslandController.instance.forceHide() - - // Mute the PhoneIsland window audio - PhoneIslandController.instance.muteAudio() - - // Wait a bit to ensure mute and hide are applied - await new Promise(resolve => setTimeout(resolve, 550)) - - // Start echo test call to *43 - Log.info('[WARMUP] Starting call to *43') - PhoneIslandController.instance.call('*43') - - // Keep the call active for 5 seconds to warm up devices - await new Promise(resolve => setTimeout(resolve, 1500)) - - // End the call - Log.info('[WARMUP] Ending echo test call') - PhoneIslandController.instance.window.emit(IPC_EVENTS.END_CALL) - - // Wait a bit before unmuting and showing - await new Promise(resolve => setTimeout(resolve, 550)) - - // Unmute the PhoneIsland window audio - PhoneIslandController.instance.unmuteAudio() - - // Show the window again (only if it has content) - PhoneIslandController.instance.forceShow() - - Log.info('[WARMUP] Audio warm-up completed successfully') - } catch (error) { - Log.error('[WARMUP] Error during audio warm-up:', error) - // Make sure to unmute and show even if there's an error - PhoneIslandController.instance.unmuteAudio() - PhoneIslandController.instance.forceShow() - } - }) - ipcMain.on(IPC_EVENTS.CHANGE_PREFERRED_DEVICES, (_, devices) => { Log.info('Received CHANGE_PREFERRED_DEVICES in ipcEvents:', devices) AccountController.instance.updatePreferredDevice(devices) diff --git a/src/renderer/src/pages/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index ad0a6566..e0a0e04b 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -80,10 +80,10 @@ export function PhoneIslandPage() { // Run audio warm-up first, only once after PhoneIsland is fully initialized // Only on Windows/macOS where the issue occurs - if (!hasRunWarmup.current && (window.api?.platform === 'win32' || window.api?.platform === 'darwin')) { + if (!hasRunWarmup.current) { hasRunWarmup.current = true Log.info('Requesting audio warm-up from main process...') - window.electron.send(IPC_EVENTS.WARMUP_AUDIO_DEVICES) + eventDispatch(PHONE_ISLAND_EVENTS['phone-island-init-audio']) // Dispatch device changes after warm-up completes (after ~5 seconds) setTimeout(() => { diff --git a/src/shared/constants.ts b/src/shared/constants.ts index e244f583..92b9c548 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -94,7 +94,6 @@ export enum IPC_EVENTS { COPY_TO_CLIPBOARD = "COPY_TO_CLIPBOARD", CHANGE_PREFERRED_DEVICES = "CHANGE_PREFERRED_DEVICES", PHONE_ISLAND_READY = "PHONE_ISLAND_READY", - WARMUP_AUDIO_DEVICES = "WARMUP_AUDIO_DEVICES", URL_OPEN = "URL_OPEN", } @@ -230,4 +229,6 @@ export enum PHONE_ISLAND_EVENTS { // Url param 'phone-island-url-parameter-opened-external' = 'phone-island-url-parameter-opened-external', 'phone-island-already-opened-external-page' = 'phone-island-already-opened-external-page', + // Init audio + 'phone-island-init-audio' = 'phone-island-init-audio', } From 1a28332870022067eb0c5339eefab81660b48d71 Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 10 Nov 2025 10:23:40 +0100 Subject: [PATCH 18/20] fix: avoid *43 inside last calls counter --- .../Modules/NethVoice/LastCalls/LastCallsBox.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx index 864f9291..e433786e 100644 --- a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx @@ -18,7 +18,17 @@ export function LastCallsBox({ showContactForm }): JSX.Element { const [operators] = useNethlinkData('operators') const [missedCalls, setMissedCalls] = useNethlinkData('missedCalls') const [preparedCalls, setPreparedCalls] = useState([]) - const title = `${t('LastCalls.Calls', { count: lastCalls?.length })} (${lastCalls?.length || 0})` + + const getFilteredCallsCount = (): number => { + if (!lastCalls) return 0 + return lastCalls.filter((call) => { + const numberToCheck = call.direction === 'in' ? call.src : call.dst + return !numberToCheck?.includes('*43') + }).length + } + + const filteredCount = getFilteredCallsCount() + const title = `${t('LastCalls.Calls', { count: filteredCount })} (${filteredCount})` useEffect(() => { prepareCalls() From 358c351f50969267911ac1222af9ca5c57f85793 Mon Sep 17 00:00:00 2001 From: Tommaso Ascani Date: Fri, 12 Sep 2025 11:52:52 +0200 Subject: [PATCH 19/20] feat: company and url rebranding --- src/renderer/src/App.tsx | 4 ++-- src/shared/types.ts | 2 ++ src/shared/useLogin.ts | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index c287469d..2324b66c 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -44,9 +44,9 @@ const RequestStateComponent = () => { // @ts-ignore (define in dts) window['CONFIG'] = { PRODUCT_NAME: 'NethLink', - COMPANY_NAME: 'Nethesis', + COMPANY_NAME: account.companyName, COMPANY_SUBNAME: 'CTI', - COMPANY_URL: 'https://www.nethesis.it/', + COMPANY_URL: account.companyUrl, API_ENDPOINT: `${account.host}`, API_SCHEME: 'https://', WS_ENDPOINT: `wss://${account.host}/ws`, diff --git a/src/shared/types.ts b/src/shared/types.ts index 0debed3f..7526e222 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -23,6 +23,8 @@ export type Account = { theme: AvailableThemes phoneIslandPosition?: { x: number; y: number } nethlinkBounds?: Electron.Rectangle + companyName?: string + companyUrl?: string sipPort?: string sipHost?: string voiceEndpoint?: string diff --git a/src/shared/useLogin.ts b/src/shared/useLogin.ts index 45dfe92f..dc7e5bcc 100644 --- a/src/shared/useLogin.ts +++ b/src/shared/useLogin.ts @@ -6,18 +6,24 @@ export const useLogin = () => { const voiceHost = account.host.split('.') voiceHost.shift() voiceHost.join('.') + let COMPANY_NAME = 'Nethesis' + let COMPANY_URL = 'https://www.nethesis.it/' let SIP_HOST = '127.0.0.1' let SIP_PORT = '5060' let NUMERIC_TIMEZONE = '+0200' let TIMEZONE = 'Europe/Rome' let VOICE_ENDPOINT = `voice.${voiceHost}` + COMPANY_NAME = config.split("COMPANY_NAME: '")[1].split("',")[0].trim() // + COMPANY_URL = config.split("COMPANY_URL: '")[1].split("',")[0].trim() // SIP_HOST = config.split("SIP_HOST: '")[1].split("',")[0].trim() // SIP_PORT = config.split("SIP_PORT: '")[1].split("',")[0].trim() // NUMERIC_TIMEZONE = config.split("NUMERIC_TIMEZONE: '")[1].split("',")[0].trim() // TIMEZONE = config.split(" TIMEZONE: '")[1].split("',")[0].trim() // VOICE_ENDPOINT = config.split(" VOICE_ENDPOINT: '")[1].split("',")[0].trim() // + account.companyName = COMPANY_NAME + account.companyUrl = COMPANY_URL account.sipHost = SIP_HOST account.sipPort = SIP_PORT account.numeric_timezone = NUMERIC_TIMEZONE From e4e6e4b27b78516e3dd8ccaa1cc54451162602eb Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 10 Nov 2025 11:13:23 +0100 Subject: [PATCH 20/20] chore(dep): update phone-island --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21b6cd13..49b56d06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.17.3", + "@nethesis/phone-island": "^0.17.4", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -5573,9 +5573,9 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.17.3", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.17.3.tgz", - "integrity": "sha512-vwuNV9c8BmCBUMlmcIVQEBTqBDl0oREDh3D4xKQo1ApdHlHqSf1udGE8F4ILhTCQbunuinT/vAuOaEezpWOGvw==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.17.4.tgz", + "integrity": "sha512-3QRVzxyXEcPQIljI2vnO3VrYE+J5sM9Tz9iInna9ciKfLUGnBJWaj8K1towNhrfLbf/OAGlSwZeQE/Z1OGVGLA==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 6a4dd2bd..05da5c4c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethesis/phone-island": "^0.17.3", + "@nethesis/phone-island": "^0.17.4", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9",