import { BrowserApi } from "../browser/browserApi" ; import { SafariApp } from "../browser/safariApp" ; import { DeviceType } from "jslib-common/enums/deviceType" ; import { ThemeType } from "jslib-common/enums/themeType" ; import { MessagingService } from "jslib-common/abstractions/messaging.service" ; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service" ; import { StorageService } from "jslib-common/abstractions/storage.service" ; import { ConstantsService } from "jslib-common/services/constants.service" ; const DialogPromiseExpiration = 600000 ; export default class BrowserPlatformUtilsService implements PlatformUtilsService { identityClientId : string = "browser" ; private showDialogResolves = new Map < number , { resolve : ( value : boolean ) => void ; date : Date ; } > ( ) ; private passwordDialogResolves = new Map < number , { tryResolve : ( canceled : boolean , password : string ) => Promise < boolean > ; date : Date ; } > ( ) ; private deviceCache : DeviceType = null ; private prefersColorSchemeDark = window . matchMedia ( "(prefers-color-scheme: dark)" ) ; constructor ( private messagingService , private storageService , private clipboardWriteCallback , private biometricCallback ) { } getDevice ( ) { if ( this . deviceCache ) { return this . deviceCache ; } if ( navigator . userAgent . indexOf ( " Firefox/" ) !== - 1 || navigator . userAgent . indexOf ( " Gecko/" ) !== - 1 ) { this . deviceCache = DeviceType . FirefoxExtension ; } else if ( ( ! ! ( window as any ) . opr && ! ! opr . addons ) || ! ! ( window as any ) . opera || navigator . userAgent . indexOf ( " OPR/" ) >= 0 ) { this . deviceCache = DeviceType . OperaExtension ; } else if ( navigator . userAgent . indexOf ( " Edg/" ) !== - 1 ) { this . deviceCache = DeviceType . EdgeExtension ; } else if ( navigator . userAgent . indexOf ( " Vivaldi/" ) !== - 1 ) { this . deviceCache = DeviceType . VivaldiExtension ; } else if ( ( window as any ) . chrome && navigator . userAgent . indexOf ( " Chrome/" ) !== - 1 ) { this . deviceCache = DeviceType . ChromeExtension ; } else if ( navigator . userAgent . indexOf ( " Safari/" ) !== - 1 ) { this . deviceCache = DeviceType . SafariExtension ; } return this . deviceCache ; } getDeviceString ( ) { const device = DeviceType [ this . getDevice ( ) ] . toLowerCase ( ) ; return device . replace ( "extension" , "" ) ; } isFirefox ( ) { return this . getDevice ( ) === DeviceType . FirefoxExtension ; } isChrome ( ) { return this . getDevice ( ) === DeviceType . ChromeExtension ; } isEdge ( ) { return this . getDevice ( ) === DeviceType . EdgeExtension ; } isOpera ( ) { return this . getDevice ( ) === DeviceType . OperaExtension ; } isVivaldi ( ) { return this . getDevice ( ) === DeviceType . VivaldiExtension ; } isSafari ( ) { return this . getDevice ( ) === DeviceType . SafariExtension ; } isIE ( ) { return false ; } isMacAppStore ( ) { return false ; } async isViewOpen ( ) { if ( await BrowserApi . isPopupOpen ( ) ) { return true ; } if ( this . isSafari ( ) ) { return false ; } const sidebarView = this . sidebarViewName ( ) ; const sidebarOpen = sidebarView != null && chrome . extension . getViews ( { type : sidebarView } ) . length > 0 ; if ( sidebarOpen ) { return true ; } const tabOpen = chrome . extension . getViews ( { type : "tab" } ) . length > 0 ; return tabOpen ; } lockTimeout ( ) { return null ; } launchUri ( uri , options ? ) { BrowserApi . createNewTab ( uri , options && options . extensionPage === true ) ; } saveFile ( win , blobData , blobOptions , fileName ) { BrowserApi . downloadFile ( win , blobData , blobOptions , fileName ) ; } getApplicationVersion ( ) { return Promise . resolve ( BrowserApi . getApplicationVersion ( ) ) ; } supportsWebAuthn ( win ) { return typeof PublicKeyCredential !== "undefined" ; } supportsDuo ( ) { return true ; } showToast ( type , title , text , options ? ) { this . messagingService . send ( "showToast" , { text : text , title : title , type : type , options : options , } ) ; } showDialog ( body , title ? , confirmText ? , cancelText ? , type ? , bodyIsHtml = false ) { const dialogId = Math . floor ( Math . random ( ) * Number . MAX_SAFE_INTEGER ) ; this . messagingService . send ( "showDialog" , { text : bodyIsHtml ? null : body , html : bodyIsHtml ? body : null , title : title , confirmText : confirmText , cancelText : cancelText , type : type , dialogId : dialogId , } ) ; return new Promise < boolean > ( ( resolve ) => { this . showDialogResolves . set ( dialogId , { resolve : resolve , date : new Date ( ) } ) ; } ) ; } isDev ( ) { return process . env . ENV === "development" ; } isSelfHost ( ) { return false ; } copyToClipboard ( text , options ? ) { let win = window ; let doc = window . document ; if ( options && ( options . window || options . win ) ) { win = options . window || options . win ; doc = win . document ; } else if ( options && options . doc ) { doc = options . doc ; } const clearing = options ? ! ! options . clearing : false ; const clearMs = options && options . clearMs ? options . clearMs : null ; if ( this . isSafari ( ) ) { SafariApp . sendMessageToApp ( "copyToClipboard" , text ) . then ( ( ) => { if ( ! clearing && this . clipboardWriteCallback != null ) { this . clipboardWriteCallback ( text , clearMs ) ; } } ) ; } else if ( this . isFirefox ( ) && ( win as any ) . navigator . clipboard && ( win as any ) . navigator . clipboard . writeText ) { ( win as any ) . navigator . clipboard . writeText ( text ) . then ( ( ) => { if ( ! clearing && this . clipboardWriteCallback != null ) { this . clipboardWriteCallback ( text , clearMs ) ; } } ) ; } else if ( ( win as any ) . clipboardData && ( win as any ) . clipboardData . setData ) { ( win as any ) . clipboardData . setData ( "Text" , text ) ; if ( ! clearing && this . clipboardWriteCallback != null ) { this . clipboardWriteCallback ( text , clearMs ) ; } } else if ( doc . queryCommandSupported && doc . queryCommandSupported ( "copy" ) ) { if ( this . isChrome ( ) && text === "" ) { text = "\u0000" ; } const textarea = doc . createElement ( "textarea" ) ; textarea . textContent = text == null || text === "" ? " " : text ; textarea . style . position = "fixed" ; doc . body . appendChild ( textarea ) ; textarea . select ( ) ; try { if ( doc . execCommand ( "copy" ) && ! clearing && this . clipboardWriteCallback != null ) { this . clipboardWriteCallback ( text , clearMs ) ; } } catch ( e ) { console . warn ( "Copy to clipboard failed." , e ) ; } finally { doc . body . removeChild ( textarea ) ; } } } async readFromClipboard ( options ? ) { let win = window ; let doc = window . document ; if ( options && ( options . window || options . win ) ) { win = options . window || options . win ; doc = win . document ; } else if ( options && options . doc ) { doc = options . doc ; } if ( this . isSafari ( ) ) { return await SafariApp . sendMessageToApp ( "readFromClipboard" ) ; } else if ( this . isFirefox ( ) && ( win as any ) . navigator . clipboard && ( win as any ) . navigator . clipboard . readText ) { return await ( win as any ) . navigator . clipboard . readText ( ) ; } else if ( doc . queryCommandSupported && doc . queryCommandSupported ( "paste" ) ) { const textarea = doc . createElement ( "textarea" ) ; textarea . style . position = "fixed" ; doc . body . appendChild ( textarea ) ; textarea . focus ( ) ; try { if ( doc . execCommand ( "paste" ) ) { return textarea . value ; } } catch ( e ) { console . warn ( "Read from clipboard failed." , e ) ; } finally { doc . body . removeChild ( textarea ) ; } } return null ; } resolveDialogPromise ( dialogId , confirmed ) { if ( this . showDialogResolves . has ( dialogId ) ) { const resolveObj = this . showDialogResolves . get ( dialogId ) ; resolveObj . resolve ( confirmed ) ; this . showDialogResolves . delete ( dialogId ) ; } this . showDialogResolves . forEach ( ( val , key ) => { const age = new Date ( ) . getTime ( ) - val . date . getTime ( ) ; if ( age > DialogPromiseExpiration ) { this . showDialogResolves . delete ( key ) ; } } ) ; } async resolvePasswordDialogPromise ( dialogId , canceled , password ) { let result = false ; if ( this . passwordDialogResolves . has ( dialogId ) ) { const resolveObj = this . passwordDialogResolves . get ( dialogId ) ; if ( await resolveObj . tryResolve ( canceled , password ) ) { this . passwordDialogResolves . delete ( dialogId ) ; result = true ; } } this . passwordDialogResolves . forEach ( ( val , key ) => { const age = new Date ( ) . getTime ( ) - val . date . getTime ( ) ; if ( age > DialogPromiseExpiration ) { this . passwordDialogResolves . delete ( key ) ; } } ) ; return result ; } async supportsBiometric ( ) { const platformInfo = await BrowserApi . getPlatformInfo ( ) ; if ( platformInfo . os === "android" ) { return false ; } if ( this . isFirefox ( ) ) { return parseInt ( ( await browser . runtime . getBrowserInfo ( ) ) . version . split ( "." ) [ 0 ] , 10 ) >= 87 ; } return true ; } authenticateBiometric ( ) { return this . biometricCallback ( ) ; } sidebarViewName ( ) { if ( ( window as any ) . chrome . sidebarAction && this . isFirefox ( ) ) { return "sidebar" ; } else if ( this . isOpera ( ) && typeof opr !== "undefined" && opr . sidebarAction ) { return "sidebar_panel" ; } return null ; } supportsSecureStorage ( ) { return false ; } getDefaultSystemTheme ( ) { return Promise . resolve ( this . prefersColorSchemeDark . matches ? ThemeType . Dark : ThemeType . Light ) ; } onDefaultSystemThemeChange ( callback ) { this . prefersColorSchemeDark . addEventListener ( "change" , ( { matches } ) => { callback ( matches ? ThemeType . Dark : ThemeType . Light ) ; } ) ; } async getEffectiveTheme ( ) { const theme = await this . storageService . get < ThemeType > ( ConstantsService . themeKey ) ; if ( theme == null || theme === ThemeType . System ) { return this . getDefaultSystemTheme ( ) ; } else { return theme ; } } }