diff --git a/react-app-rewired/headers/csps/wallets/keepkey.ts b/react-app-rewired/headers/csps/wallets/keepkey.ts index 87b3e18a300..db03002136a 100644 --- a/react-app-rewired/headers/csps/wallets/keepkey.ts +++ b/react-app-rewired/headers/csps/wallets/keepkey.ts @@ -2,6 +2,8 @@ import type { Csp } from '../../types' export const csp: Csp = { 'connect-src': [ + process.env.REACT_APP_KEEPKEY_FIRMWARE_RELEASES_URL!, + process.env.REACT_APP_KEEPKEY_GITHUB_RELEASES_API_URL!, process.env.REACT_APP_KEEPKEY_VERSIONS_URL!, process.env.REACT_APP_KEEPKEY_DESKTOP_URL!, ], diff --git a/src/components/Layout/Header/NavBar/KeepKey/KeepKeyMenu.tsx b/src/components/Layout/Header/NavBar/KeepKey/KeepKeyMenu.tsx index 6d80c56fa4c..b4560603902 100644 --- a/src/components/Layout/Header/NavBar/KeepKey/KeepKeyMenu.tsx +++ b/src/components/Layout/Header/NavBar/KeepKey/KeepKeyMenu.tsx @@ -19,6 +19,7 @@ import { SubMenuContainer } from 'components/Layout/Header/NavBar/SubMenuContain import { SubmenuHeader } from 'components/Layout/Header/NavBar/SubmenuHeader' import { WalletImage } from 'components/Layout/Header/NavBar/WalletImage' import { RawText, Text } from 'components/Text' +import { WalletActions } from 'context/WalletProvider/actions' import { useKeepKeyVersions } from 'context/WalletProvider/KeepKey/hooks/useKeepKeyVersions' import { useKeepKey } from 'context/WalletProvider/KeepKeyProvider' import { useModal } from 'hooks/useModal/useModal' @@ -54,6 +55,7 @@ export const KeepKeyMenu = () => { const { setDeviceState, state: { isConnected, walletInfo }, + dispatch, } = useWallet() const keepKeyWipe = useModal('keepKeyWipe') @@ -81,6 +83,10 @@ export const KeepKeyMenu = () => { keepKeyWipe.open({}) } + const handleUpdateClick = useCallback(() => { + dispatch({ type: WalletActions.DOWNLOAD_UPDATER, payload: false }) + }, [dispatch]) + const deviceTimeoutTranslation: string = typeof deviceTimeout?.label === 'object' ? translate(...deviceTimeout?.label) @@ -134,7 +140,7 @@ export const KeepKeyMenu = () => { badgeColor={versions?.bootloader.updateAvailable ? 'yellow' : 'green'} valueDisposition={versions?.bootloader.updateAvailable ? 'info' : 'neutral'} isDisabled={!versions?.bootloader.updateAvailable} - externalUrl={updaterUrl} + onClick={handleUpdateClick} /> { badgeColor={versions?.firmware.updateAvailable ? 'yellow' : 'green'} valueDisposition={versions?.firmware.updateAvailable ? 'info' : 'neutral'} isDisabled={!versions?.firmware.updateAvailable} - externalUrl={updaterUrl} + onClick={handleUpdateClick} /> { + const toast = useToast() const platform = useMemo(() => getPlatform(), []) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [version, setVersion] = useState('') + + // Use separate state variables for each platform URL + const [urlMacOS, setUrlMacOS] = useState('') + const [urlWindows, setUrlWindows] = useState('') + const [urlLinux, setUrlLinux] = useState('') + + // Find latest release links directly from the GitHub API + const findLatestReleaseLinks = useCallback(async () => { + setIsLoading(true) + setError(null) + + try { + // Use the exact API URL + const resp = await axios({ + method: 'GET', + url: GITHUB_API_URL, + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'KeepKey-Desktop-App' + } + }) + + console.log('findLatestReleaseLinks', resp.data) + + if (!resp.data || !resp.data.tag_name || !resp.data.assets) { + throw new Error('Invalid response from GitHub API') + } + + // Extract version from tag_name + const versionWithV = resp.data.tag_name + const versionNumber = versionWithV.replace('v', '') + setVersion(versionNumber) + + // Find the correct assets by examining the assets array + const assets = resp.data.assets + + // Find macOS universal DMG + const macAsset = assets.find(asset => + asset.name.includes('universal.dmg') || + asset.name.includes('universal-mac') + ) + + // Find Windows EXE + const windowsAsset = assets.find(asset => + asset.name.includes('Setup') && asset.name.endsWith('.exe') + ) + + // Find Linux AppImage + const linuxAsset = assets.find(asset => + asset.name.endsWith('.AppImage') + ) + + // Set the URLs from the assets or construct them if not found + setUrlMacOS(macAsset?.browser_download_url || + `https://github.com/keepkey/keepkey-desktop/releases/download/v${versionNumber}/KeepKey-Desktop-${versionNumber}-universal.dmg`) + + setUrlWindows(windowsAsset?.browser_download_url || + `https://github.com/keepkey/keepkey-desktop/releases/download/v${versionNumber}/KeepKey-Desktop-Setup-${versionNumber}.exe`) + + setUrlLinux(linuxAsset?.browser_download_url || + `https://github.com/keepkey/keepkey-desktop/releases/download/v${versionNumber}/KeepKey-Desktop-${versionNumber}.AppImage`) + + console.log('Download URLs:', { + macOS: macAsset?.browser_download_url, + windows: windowsAsset?.browser_download_url, + linux: linuxAsset?.browser_download_url + }) + + setError(null) + } catch (e) { + console.error('Error fetching latest version:', e) + setError('Failed to fetch the latest version. Please try again or visit the releases page.') + } finally { + setIsLoading(false) + } + }, []) + + // Call the function on component mount + useEffect(() => { + findLatestReleaseLinks() + }, [findLatestReleaseLinks]) const platformFilename = useMemo(() => { + if (isLoading) return null switch (platform) { case 'Mac OS': - return 'KeepKey-Updater-2.1.4.dmg' + return `KeepKey Desktop ${version} (macOS)` case 'Windows': - return 'KeepKey-Updater-Setup-2.1.4.exe' + return `KeepKey Desktop ${version} (Windows)` case 'Linux': - return 'KeepKey-Updater-2.1.4.AppImage' + return `KeepKey Desktop ${version} (Linux)` default: - return null + return 'Desktop App' } - }, [platform]) + }, [platform, isLoading, version]) const platformIcon = useMemo(() => { switch (platform) { @@ -43,12 +145,56 @@ export const KeepKeyDownloadUpdaterApp = () => { const downloadUpdaterTranslation: TextPropTypes['translation'] = useMemo( () => [ 'modals.keepKey.downloadUpdater.button', - { filename: platformFilename || 'Updater App' }, + { filename: platformFilename || 'Desktop App' }, ], [platformFilename], ) - const updaterUrl = platformFilename ? `${UPDATER_BASE_URL}${platformFilename}` : RELEASE_PAGE + const handleDownload = useCallback(() => { + let downloadUrl = RELEASE_PAGE + + // Get the appropriate URL based on platform + switch (platform) { + case 'Mac OS': + downloadUrl = urlMacOS || RELEASE_PAGE + break + case 'Windows': + downloadUrl = urlWindows || RELEASE_PAGE + break + case 'Linux': + downloadUrl = urlLinux || RELEASE_PAGE + break + } + + if (!downloadUrl || downloadUrl === RELEASE_PAGE) { + // If we don't have a specific URL, open the releases page + window.open(RELEASE_PAGE, '_blank') + return + } + + // Create a temporary link element for better download handling + const link = document.createElement('a') + link.href = downloadUrl + link.rel = 'noopener noreferrer' + + // Append to body, click, and remove + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // Show a toast notification + toast({ + title: 'Download Started', + description: 'Your download should begin shortly', + status: 'success', + duration: 5000, + isClosable: true, + }) + }, [platform, urlMacOS, urlWindows, urlLinux, toast]) + + const handleRetry = useCallback(() => { + findLatestReleaseLinks() + }, [findLatestReleaseLinks]) return ( <> @@ -60,15 +206,82 @@ export const KeepKeyDownloadUpdaterApp = () => { {platform && ( <> {platform} - - - + )} - + + + )} + + ) } + +const getPlatform = () => { + const platform = navigator?.platform + const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'] + const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'] + + if (macosPlatforms.includes(platform)) { + return 'Mac OS' + } else if (windowsPlatforms.includes(platform)) { + return 'Windows' + } else if (/Linux/.test(platform)) { + return 'Linux' + } + + const userAgent = navigator.userAgent.toLowerCase() + if (userAgent.includes('mac')) { + return 'Mac OS' + } else if (userAgent.includes('win')) { + return 'Windows' + } else if (userAgent.includes('linux')) { + return 'Linux' + } + + return null +} diff --git a/src/context/WalletProvider/KeepKey/helpers.ts b/src/context/WalletProvider/KeepKey/helpers.ts index ada83414f9b..0786a6146a4 100644 --- a/src/context/WalletProvider/KeepKey/helpers.ts +++ b/src/context/WalletProvider/KeepKey/helpers.ts @@ -1,11 +1,7 @@ import type { RecoverDevice } from '@shapeshiftoss/hdwallet-core' -import { getConfig } from 'config' import type { KeyboardEvent } from 'react' import { VALID_ENTROPY_NUMBERS } from 'context/WalletProvider/KeepKey/components/RecoverySettings' -export const RELEASE_PAGE = getConfig().REACT_APP_KEEPKEY_UPDATER_RELEASE_PAGE -export const UPDATER_BASE_URL = getConfig().REACT_APP_KEEPKEY_UPDATER_BASE_URL - export const isValidInput = ( e: KeyboardEvent, wordEntropy: number, diff --git a/src/context/WalletProvider/KeepKeyProvider.tsx b/src/context/WalletProvider/KeepKeyProvider.tsx index a186e7e97e8..34baa6cff67 100644 --- a/src/context/WalletProvider/KeepKeyProvider.tsx +++ b/src/context/WalletProvider/KeepKeyProvider.tsx @@ -5,9 +5,9 @@ import { AlertTitle, Box, CloseButton, - Link, Text, useToast, + Button, } from '@chakra-ui/react' import type { Features } from '@keepkey/device-protocol/lib/messages_pb' import type { KeepKeyHDWallet } from '@shapeshiftoss/hdwallet-keepkey' @@ -26,6 +26,7 @@ import type { RadioOption } from 'components/Radio/Radio' import { useWallet } from 'hooks/useWallet/useWallet' import { poll } from 'lib/poll/poll' import { isKeepKeyHDWallet } from 'lib/utils' +import { WalletActions } from 'context/WalletProvider/actions' import { useKeepKeyVersions } from './KeepKey/hooks/useKeepKeyVersions' @@ -123,6 +124,7 @@ const KeepKeyContext = createContext(null) export const KeepKeyProvider = ({ children }: { children: React.ReactNode }): JSX.Element => { const { state: { wallet }, + dispatch: walletDispatch, } = useWallet() const { versions, updaterUrl, isLTCSupportedFirmwareVersion } = useKeepKeyVersions() const translate = useTranslate() @@ -211,9 +213,16 @@ export const KeepKeyProvider = ({ children }: { children: React.ReactNode }): JS ) : null} - +