diff --git a/fbw-common/src/systems/instruments/src/EFB/Apis/Navigraph/Components/Authentication.tsx b/fbw-common/src/systems/instruments/src/EFB/Apis/Navigraph/Components/Authentication.tsx index ac878d4382a..8fe54b93e50 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Apis/Navigraph/Components/Authentication.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Apis/Navigraph/Components/Authentication.tsx @@ -4,12 +4,16 @@ import { useInterval } from '@flybywiresim/react-components'; import React, { useEffect, useState } from 'react'; import { CloudArrowDown, ShieldLock } from 'react-bootstrap-icons'; -import { toast } from 'react-toastify'; +import { DeviceFlowParams } from 'navigraph/auth'; import QRCode from 'qrcode.react'; -import { NavigraphClient, NavigraphSubscriptionStatus, usePersistentProperty } from '@flybywiresim/fbw-sdk'; +import { NavigraphKeys, NavigraphSubscriptionStatus } from '@flybywiresim/fbw-sdk'; import { useHistory } from 'react-router-dom'; import { t } from '../../../Localization/translation'; -import { useNavigraph } from '../Navigraph'; +import { useNavigraphAuth } from '../../../../react/navigraph'; +import { CancelToken } from '@navigraph/auth'; + +const NAVIGRAPH_SUBSCRIPTION_CHARTS = 'charts'; +const NAVIGRAPH_SUBSCRIPTION_FMSDATA = 'fmsdata'; export type NavigraphAuthInfo = | { @@ -24,48 +28,45 @@ export type NavigraphAuthInfo = }; export const useNavigraphAuthInfo = (): NavigraphAuthInfo => { - const navigraph = useNavigraph(); - - const [tokenAvail, setTokenAvail] = useState(false); - const [subscriptionStatus, setSubscriptionStatus] = useState( - NavigraphSubscriptionStatus.Unknown, - ); - const [username] = usePersistentProperty('NAVIGRAPH_USERNAME'); - - useInterval( - () => { - if (tokenAvail !== navigraph.hasToken && navigraph.hasToken) { - navigraph - .fetchSubscriptionStatus() - .then(setSubscriptionStatus) - .catch(() => setSubscriptionStatus(NavigraphSubscriptionStatus.Unknown)); - } else if (!navigraph.hasToken) { - setSubscriptionStatus(NavigraphSubscriptionStatus.None); - } - - setTokenAvail(navigraph.hasToken); - }, - 1000, - { runOnStart: true }, - ); - - if (tokenAvail) { - return { loggedIn: tokenAvail, username, subscriptionStatus }; + const navigraphAuth = useNavigraphAuth(); + + if (navigraphAuth.user) { + return { + loggedIn: true, + username: navigraphAuth.user.preferred_username, + subscriptionStatus: [NAVIGRAPH_SUBSCRIPTION_CHARTS, NAVIGRAPH_SUBSCRIPTION_FMSDATA].every((it) => + navigraphAuth.user.subscriptions.includes(it), + ) + ? NavigraphSubscriptionStatus.Unlimited + : NavigraphSubscriptionStatus.Unknown, + }; } + return { loggedIn: false }; }; -const Loading = () => { - const navigraph = useNavigraph(); - const [, setRefreshToken] = usePersistentProperty('NAVIGRAPH_REFRESH_TOKEN'); +interface LoadingProps { + onNewDeviceFlowParams: (params: DeviceFlowParams) => void; +} + +const Loading: React.FC = ({ onNewDeviceFlowParams }) => { + const navigraph = useNavigraphAuth(); const [showResetButton, setShowResetButton] = useState(false); + const [cancelToken] = useState(CancelToken.source()); const handleResetRefreshToken = () => { - setRefreshToken(''); - navigraph.authenticate(); + cancelToken.cancel('reset requested by user'); + + navigraph.signIn((params) => { + onNewDeviceFlowParams(params); + }); }; useEffect(() => { + navigraph.signIn((params) => { + onNewDeviceFlowParams(params); + }, cancelToken.token); + const timeout = setTimeout(() => { setShowResetButton(true); }, 2_000); @@ -94,40 +95,17 @@ const Loading = () => { }; export const NavigraphAuthUI = () => { - const navigraph = useNavigraph(); + const [params, setParams] = useState(null); + const [displayAuthCode, setDisplayAuthCode] = useState(t('NavigationAndCharts.Navigraph.LoadingMsg').toUpperCase()); useInterval(() => { - if (navigraph.auth.code) { - setDisplayAuthCode(navigraph.auth.code); + if (params?.user_code) { + setDisplayAuthCode(params.user_code); } }, 1000); - const hasQr = !!navigraph.auth.qrLink; - - useInterval(async () => { - if (!navigraph.hasToken) { - try { - await navigraph.getToken(); - } catch (e) { - toast.error(`Navigraph Authentication Error: ${e.message}`, { autoClose: 10_000 }); - } - } - }, navigraph.auth.interval * 1000); - - useInterval(async () => { - try { - await navigraph.getToken(); - } catch (e) { - toast.error(`Navigraph Authentication Error: ${e.message}`, { autoClose: 10_000 }); - } - }, navigraph.tokenRefreshInterval * 1000); - - useEffect(() => { - if (!navigraph.hasToken) { - navigraph.authenticate(); - } - }, []); + const hasQr = !!params?.verification_uri_complete; return (
@@ -140,7 +118,7 @@ export const NavigraphAuthUI = () => {

{t('NavigationAndCharts.Navigraph.ScanTheQrCodeOrOpen')}{' '} - {navigraph.auth.link}{' '} + {params?.verification_uri_complete ?? ''}{' '} {t('NavigationAndCharts.Navigraph.IntoYourBrowserAndEnterTheCodeBelow')}

@@ -154,10 +132,10 @@ export const NavigraphAuthUI = () => {
{hasQr ? (
- +
) : ( - + )}
@@ -175,24 +153,20 @@ export const NavigraphAuthUIWrapper: React.FC = ({ onSuccessfulLogin, children, }) => { - const [tokenAvail, setTokenAvail] = useState(false); - - const navigraph = useNavigraph(); + const navigraph = useNavigraphAuth(); useInterval( () => { - if (!tokenAvail && navigraph.hasToken) { + if (navigraph.user) { onSuccessfulLogin?.(); } - - setTokenAvail(navigraph.hasToken); }, 1000, { runOnStart: true }, ); let ui: React.ReactNode; - if (tokenAvail) { + if (navigraph.user) { ui = children; } else if (showLogin) { ui = ; @@ -200,7 +174,7 @@ export const NavigraphAuthUIWrapper: React.FC = ({ ui = ; } - return NavigraphClient.hasSufficientEnv ? ( + return NavigraphKeys.hasSufficientEnv ? ( <>{ui} ) : (
diff --git a/fbw-common/src/systems/instruments/src/EFB/Apis/Navigraph/Navigraph.tsx b/fbw-common/src/systems/instruments/src/EFB/Apis/Navigraph/Navigraph.tsx deleted file mode 100644 index 802a852bd1a..00000000000 --- a/fbw-common/src/systems/instruments/src/EFB/Apis/Navigraph/Navigraph.tsx +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2023-2024 FlyByWire Simulations -// SPDX-License-Identifier: GPL-3.0 - -import React, { useContext } from 'react'; -import { NavigraphClient } from '@flybywiresim/fbw-sdk'; - -export const NavigraphContext = React.createContext(undefined!); - -export const useNavigraph = () => useContext(NavigraphContext); diff --git a/fbw-common/src/systems/instruments/src/EFB/Dashboard/Widgets/FlightWidget.tsx b/fbw-common/src/systems/instruments/src/EFB/Dashboard/Widgets/FlightWidget.tsx index faabd30f4a9..377ec7c9a37 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Dashboard/Widgets/FlightWidget.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Dashboard/Widgets/FlightWidget.tsx @@ -20,6 +20,7 @@ import { setToastPresented, setSimbriefDataPending, } from '@flybywiresim/flypad'; +import { useNavigraphAuthInfo } from '../../Apis/Navigraph/Components/Authentication'; interface InformationEntryProps { title: string; @@ -37,7 +38,9 @@ export const FlightWidget = () => { const { data } = useAppSelector((state) => state.simbrief); const simbriefDataPending = useAppSelector((state) => state.simbrief.simbriefDataPending); const aircraftIcao = useAppSelector((state) => state.simbrief.data.aircraftIcao); - const [navigraphUsername] = usePersistentProperty('NAVIGRAPH_USERNAME'); + + const navigraphAuthInfo = useNavigraphAuthInfo(); + const [overrideSimBriefUserID] = usePersistentProperty('CONFIG_OVERRIDE_SIMBRIEF_USERID'); const [autoSimbriefImport] = usePersistentProperty('CONFIG_AUTO_SIMBRIEF_IMPORT'); const airframeInfo = useAppSelector((state) => state.config.airframeInfo); @@ -104,7 +107,10 @@ export const FlightWidget = () => { toast.error(t('Dashboard.YourFlight.NoImportDueToBoardingOrRefuel')); } else { try { - const action = await fetchSimbriefDataAction(navigraphUsername ?? '', overrideSimBriefUserID ?? ''); + const action = await fetchSimbriefDataAction( + (navigraphAuthInfo.loggedIn && navigraphAuthInfo.username) || '', + overrideSimBriefUserID ?? '', + ); dispatch(action); dispatch(setFuelImported(false)); dispatch(setPayloadImported(false)); @@ -122,7 +128,7 @@ export const FlightWidget = () => { useEffect(() => { if ( !simbriefDataPending && - (navigraphUsername || overrideSimBriefUserID) && + ((navigraphAuthInfo.loggedIn && navigraphAuthInfo.username) || overrideSimBriefUserID) && !toastPresented && fuelImported && payloadImported @@ -141,11 +147,11 @@ export const FlightWidget = () => { (!data || !isSimbriefDataLoaded()) && !simbriefDataPending && autoSimbriefImport === 'ENABLED' && - (navigraphUsername || overrideSimBriefUserID) + ((navigraphAuthInfo.loggedIn && navigraphAuthInfo.username) || overrideSimBriefUserID) ) { fetchData(); } - }, []); + }, [navigraphAuthInfo]); const simbriefDataLoaded = isSimbriefDataLoaded(); @@ -252,7 +258,7 @@ export const FlightWidget = () => {
- + ); default: throw new Error('Invalid content state provided'); diff --git a/fbw-common/src/systems/instruments/src/EFB/Navigation/Navigation.tsx b/fbw-common/src/systems/instruments/src/EFB/Navigation/Navigation.tsx index 7b7cd6e2629..f471cc389ab 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Navigation/Navigation.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Navigation/Navigation.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0 /* eslint-disable max-len,react/no-this-in-sfc,no-console */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { ArrowClockwise, ArrowCounterclockwise, @@ -17,10 +17,9 @@ import { } from 'react-bootstrap-icons'; import { useSimVar } from '@flybywiresim/fbw-sdk'; import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; +import { Chart } from 'navigraph/charts'; import { t } from '../Localization/translation'; import { TooltipWrapper } from '../UtilComponents/TooltipWrapper'; -// import { DrawableCanvas } from '../UtilComponents/DrawableCanvas'; -import { useNavigraph } from '../Apis/Navigraph/Navigraph'; import { SimpleInput } from '../UtilComponents/Form/SimpleInput/SimpleInput'; import { useAppDispatch, useAppSelector } from '../Store/store'; import { @@ -38,6 +37,8 @@ import { Navbar } from '../UtilComponents/Navbar'; import { NavigraphPage } from './Pages/NavigraphPage/NavigraphPage'; import { getPdfUrl, LocalFilesPage } from './Pages/LocalFilesPage/LocalFilesPage'; import { PinnedChartUI } from './Pages/PinnedChartsPage'; +import { useNavigraphAuth } from '../../react/navigraph'; +import { navigraphCharts } from '../../navigraph'; export const navigationTabs: (PageLink & { associatedTab: NavigationTab })[] = [ { name: 'Navigraph', alias: '', component: , associatedTab: NavigationTab.NAVIGRAPH }, @@ -100,7 +101,7 @@ export const ChartViewer = () => { // const [drawMode] = useState(false); // const [brushSize] = useState(10); - const { userName } = useNavigraph(); + const navigraphAuth = useNavigraphAuth(); const ref = useRef(null); const chartRef = useRef(null); @@ -115,22 +116,28 @@ export const ChartViewer = () => { const [aircraftLongitude] = useSimVar('PLANE LONGITUDE', 'degree longitude', 1000); const [aircraftTrueHeading] = useSimVar('PLANE HEADING DEGREES TRUE', 'degrees', 100); + const [chartLightBlob, setChartLightBlob] = useState(null); + const chartLightUrl = useMemo(() => (chartLightBlob ? URL.createObjectURL(chartLightBlob) : null), [chartLightBlob]); + + const [chartDarkBlob, setChartDarkBlob] = useState(null); + const chartDarkUrl = useMemo(() => (chartLightBlob ? URL.createObjectURL(chartDarkBlob) : null), [chartDarkBlob]); + useEffect(() => { let visible = false; if ( boundingBox && - aircraftLatitude >= boundingBox.bottomLeft.lat && - aircraftLatitude <= boundingBox.topRight.lat && - aircraftLongitude >= boundingBox.bottomLeft.lon && - aircraftLongitude <= boundingBox.topRight.lon + aircraftLatitude >= boundingBox.planview.latlng.lat1 && + aircraftLatitude <= boundingBox.planview.latlng.lat2 && + aircraftLongitude >= boundingBox.planview.latlng.lng1 && + aircraftLongitude <= boundingBox.planview.latlng.lng2 ) { - const dx = boundingBox.topRight.xPx - boundingBox.bottomLeft.xPx; - const dy = boundingBox.bottomLeft.yPx - boundingBox.topRight.yPx; - const dLat = boundingBox.topRight.lat - boundingBox.bottomLeft.lat; - const dLon = boundingBox.topRight.lon - boundingBox.bottomLeft.lon; - const x = boundingBox.bottomLeft.xPx + dx * ((aircraftLongitude - boundingBox.bottomLeft.lon) / dLon); - const y = boundingBox.topRight.yPx + dy * ((boundingBox.topRight.lat - aircraftLatitude) / dLat); + const dx = boundingBox.planview.pixels.x2 - boundingBox.planview.pixels.x1; + const dy = boundingBox.planview.pixels.y1 - boundingBox.planview.pixels.y2; + const dLat = boundingBox.planview.latlng.lat2 - boundingBox.planview.latlng.lat1; + const dLon = boundingBox.planview.latlng.lng2 - boundingBox.planview.latlng.lng1; + const x = boundingBox.planview.pixels.x1 + dx * ((aircraftLongitude - boundingBox.planview.latlng.lng1) / dLon); + const y = boundingBox.planview.pixels.y2 + dy * ((boundingBox.planview.latlng.lat1 - aircraftLatitude) / dLat); setAircraftIconPosition({ x, y, r: aircraftTrueHeading }); visible = true; @@ -145,6 +152,21 @@ export const ChartViewer = () => { aircraftTrueHeading.toFixed(1), ]); + useEffect(() => { + navigraphCharts + .getChartImage({ + chart: { image_day_url: chartLinks.light, image_night_url: chartLinks.dark } as Chart, + theme: 'light', + }) + .then((blob) => setChartLightBlob(blob)); + navigraphCharts + .getChartImage({ + chart: { image_day_url: chartLinks.light, image_night_url: chartLinks.dark } as Chart, + theme: 'dark', + }) + .then((blob) => setChartDarkBlob(blob)); + }, [chartLinks]); + const handleRotateRight = () => { dispatch(editTabProperty({ tab: currentTab, chartRotation: (chartRotation + 90) % 360 })); }; @@ -508,14 +530,14 @@ export const ChartViewer = () => { > {chartLinks && provider === 'NAVIGRAPH' && (

- This chart is linked to {userName} + This chart is linked to {navigraphAuth.user?.preferred_username ?? ''}

)} {aircraftIconVisible && boundingBox && ( { )}
- chart - chart + {chartLightUrl && ( + chart + )} + + {chartDarkUrl && ( + chart + )}
diff --git a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/LocalFilesPage/LocalFileChartSelector.tsx b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/LocalFilesPage/LocalFileChartSelector.tsx index e045d3e72b9..ab366409402 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/LocalFilesPage/LocalFileChartSelector.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/LocalFilesPage/LocalFileChartSelector.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { CloudArrowDown, Pin, PinFill } from 'react-bootstrap-icons'; import { toast } from 'react-toastify'; -import { Viewer } from '@flybywiresim/fbw-sdk'; +import { LocalChartCategory, Viewer } from '@flybywiresim/fbw-sdk'; import { t } from '../../../Localization/translation'; import { addPinnedChart, @@ -12,6 +12,7 @@ import { editPinnedChart, editTabProperty, isChartPinned, + LocalChartCategoryToIndex, NavigationTab, removedPinnedChart, setBoundingBox, @@ -27,7 +28,7 @@ export type LocalFileChart = { }; export type LocalFileOrganizedCharts = { - name: string; + name: LocalChartCategory; alias: string; charts: LocalFileChart[]; }; @@ -39,7 +40,7 @@ interface LocalFileChartSelectorProps { export const LocalFileChartSelector = ({ selectedTab, loading }: LocalFileChartSelectorProps) => { const dispatch = useAppDispatch(); - const { chartId, selectedTabIndex } = useAppSelector((state) => state.navigationTab[NavigationTab.LOCAL_FILES]); + const { chartId, selectedTabType } = useAppSelector((state) => state.navigationTab[NavigationTab.LOCAL_FILES]); const { pinnedCharts } = useAppSelector((state) => state.navigationTab); if (loading) { @@ -138,7 +139,7 @@ export const LocalFileChartSelector = ({ selectedTab, loading }: LocalFileChartS chartName: { light: '', dark: '' }, title: chart.fileName, subTitle: '', - tabIndex: selectedTabIndex, + tabIndex: LocalChartCategoryToIndex[selectedTabType], timeAccessed: 0, tag: chart.type, provider: ChartProvider.LOCAL_FILES, diff --git a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/LocalFilesPage/LocalFileChartUI.tsx b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/LocalFilesPage/LocalFileChartUI.tsx index ed74d2026a9..530859c2e32 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/LocalFilesPage/LocalFileChartUI.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/LocalFilesPage/LocalFileChartUI.tsx @@ -12,7 +12,7 @@ import { SimpleInput } from '../../../UtilComponents/Form/SimpleInput/SimpleInpu import { SelectGroup, SelectItem } from '../../../UtilComponents/Form/Select'; import { useAppDispatch, useAppSelector } from '../../../Store/store'; import { isSimbriefDataLoaded } from '../../../Store/features/simBrief'; -import { NavigationTab, editTabProperty } from '../../../Store/features/navigationPage'; +import { NavigationTab, editTabProperty, LocalChartCategoryToIndex } from '../../../Store/features/navigationPage'; import { ChartViewer } from '../../Navigation'; interface LocalFileCharts { @@ -33,7 +33,7 @@ export const LocalFileChartUI = () => { { name: 'PDF', alias: t('NavigationAndCharts.LocalFiles.Pdf'), charts: charts.pdfs }, { name: 'BOTH', alias: t('NavigationAndCharts.LocalFiles.Both'), charts: [...charts.images, ...charts.pdfs] }, ]); - const { searchQuery, isFullScreen, chartName, selectedTabIndex } = useAppSelector( + const { searchQuery, isFullScreen, chartName, selectedTabType } = useAppSelector( (state) => state.navigationTab[NavigationTab.LOCAL_FILES], ); @@ -42,11 +42,11 @@ export const LocalFileChartUI = () => { const searchableCharts: string[] = []; - if (selectedTabIndex === 0 || selectedTabIndex === 2) { + if (selectedTabType === 'IMAGE' || selectedTabType === 'BOTH') { searchableCharts.push(...charts.images.map((image) => image.fileName)); } - if (selectedTabIndex === 1 || selectedTabIndex === 2) { + if (selectedTabType === 'PDF' || selectedTabType === 'BOTH') { searchableCharts.push(...charts.pdfs.map((pdf) => pdf.fileName)); } @@ -67,7 +67,7 @@ export const LocalFileChartUI = () => { useEffect(() => { handleIcaoChange(searchQuery); - }, [selectedTabIndex]); + }, [selectedTabType]); useEffect(() => { updateSearchStatus(); @@ -93,7 +93,7 @@ export const LocalFileChartUI = () => { try { // IMAGE or BOTH - if (selectedTabIndex === 0 || selectedTabIndex === 2) { + if (selectedTabType === 'IMAGE' || selectedTabType === 'BOTH') { const imageNames: string[] = await Viewer.getImageList(); imageNames.forEach((imageName) => { if (imageName.toUpperCase().includes(searchQuery)) { @@ -106,7 +106,7 @@ export const LocalFileChartUI = () => { } // PDF or BOTH - if (selectedTabIndex === 1 || selectedTabIndex === 2) { + if (selectedTabType === 'PDF' || selectedTabType === 'BOTH') { const pdfNames: string[] = await Viewer.getPDFList(); pdfNames.forEach((pdfName) => { if (pdfName.toUpperCase().includes(searchQuery)) { @@ -195,11 +195,13 @@ export const LocalFileChartUI = () => {
- {organizedCharts.map((organizedChart, index) => ( + {organizedCharts.map((organizedChart) => ( - dispatch(editTabProperty({ tab: NavigationTab.LOCAL_FILES, selectedTabIndex: index })) + dispatch( + editTabProperty({ tab: NavigationTab.LOCAL_FILES, selectedTabType: organizedChart.name }), + ) } key={organizedChart.name} className="flex w-full justify-center uppercase" @@ -210,7 +212,10 @@ export const LocalFileChartUI = () => { - +
diff --git a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartSelector.tsx b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartSelector.tsx index af4812f0d41..976628372d0 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartSelector.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartSelector.tsx @@ -1,10 +1,10 @@ // Copyright (c) 2023-2024 FlyByWire Simulations // SPDX-License-Identifier: GPL-3.0 -import { NavigraphChart } from '@flybywiresim/fbw-sdk'; - import React, { useState, useEffect } from 'react'; import { CloudArrowDown, PinFill, Pin } from 'react-bootstrap-icons'; +import { Chart } from 'navigraph/charts'; + import { t } from '../../../Localization/translation'; import { useAppDispatch, useAppSelector } from '../../../Store/store'; import { @@ -16,6 +16,7 @@ import { isChartPinned, removedPinnedChart, addPinnedChart, + ChartTabTypeToIndex, } from '../../../Store/features/navigationPage'; import { navigationTabs } from '../../Navigation'; @@ -26,12 +27,12 @@ interface NavigraphChartSelectorProps { type RunwayOrganizedChart = { name: string; - charts: NavigraphChart[]; + charts: Chart[]; }; export type OrganizedChart = { name: string; - charts: NavigraphChart[]; + charts: Chart[]; bundleRunways?: boolean; }; @@ -42,9 +43,10 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS const dispatch = useAppDispatch(); - const { chartId, searchQuery, selectedTabIndex } = useAppSelector( + const { chartId, searchQuery, selectedTabType } = useAppSelector( (state) => state.navigationTab[NavigationTab.NAVIGRAPH], ); + const { pinnedCharts } = useAppSelector((state) => state.navigationTab); useEffect(() => { @@ -52,10 +54,10 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS const runwayNumbers: string[] = []; selectedTab.charts.forEach((chart) => { - if (chart.runway.length !== 0) { - chart.runway.forEach((runway) => { + if (chart.runways.length !== 0) { + for (const runway of chart.runways) { runwayNumbers.push(runway); - }); + } } else { runwayNumbers.push(NO_RUNWAY_NAME); } @@ -75,7 +77,7 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS organizedRunwayCharts.push({ name: runway, charts: selectedTab.charts.filter( - (chart) => chart.runway.includes(runway) || (chart.runway.length === 0 && runway === NO_RUNWAY_NAME), + (chart) => chart.runways.includes(runway) || (chart.runways.length === 0 && runway === NO_RUNWAY_NAME), ), }); }); @@ -86,7 +88,7 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS } }, [runwaySet]); - const handleChartClick = (chart: NavigraphChart) => { + const handleChartClick = (chart: Chart) => { dispatch(editTabProperty({ tab: NavigationTab.NAVIGRAPH, pagesViewable: 1 })); dispatch(editTabProperty({ tab: NavigationTab.NAVIGRAPH, currentPage: 1 })); @@ -96,11 +98,15 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS dispatch( editTabProperty({ tab: NavigationTab.NAVIGRAPH, chartDimensions: { width: undefined, height: undefined } }), ); + dispatch(editTabProperty({ tab: NavigationTab.NAVIGRAPH, chartName: { light: chart.name, dark: chart.name } })); dispatch( - editTabProperty({ tab: NavigationTab.NAVIGRAPH, chartName: { light: chart.fileDay, dark: chart.fileNight } }), + editTabProperty({ + tab: NavigationTab.NAVIGRAPH, + chartLinks: { light: chart.image_day_url, dark: chart.image_night_url }, + }), ); - dispatch(setBoundingBox(chart.boundingBox)); + dispatch(setBoundingBox(chart.bounding_boxes)); dispatch(setProvider(ChartProvider.NAVIGRAPH)); }; @@ -153,15 +159,15 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS dispatch( addPinnedChart({ chartId: chart.id, - chartName: { light: chart.fileDay, dark: chart.fileNight }, + chartName: { light: chart.image_day_url, dark: chart.image_night_url }, title: searchQuery, - subTitle: chart.procedureIdentifier, - tabIndex: selectedTabIndex, + subTitle: chart.procedures[0], + tabIndex: ChartTabTypeToIndex[selectedTabType], timeAccessed: 0, tag: selectedTab.name, provider: ChartProvider.NAVIGRAPH, pagesViewable: 1, - boundingBox: chart.boundingBox, + boundingBox: chart.bounding_boxes, pageIndex: navigationTabs.findIndex( (tab) => tab.associatedTab === NavigationTab.NAVIGRAPH, ), @@ -178,9 +184,9 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS
- {chart.procedureIdentifier} + {chart.name} - {chart.indexNumber} + {chart.index_number}
@@ -213,15 +219,19 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS dispatch( addPinnedChart({ chartId: chart.id, - chartName: { light: chart.fileDay, dark: chart.fileNight }, + chartName: { light: chart.image_day_url, dark: chart.image_night_url }, title: searchQuery, - subTitle: chart.procedureIdentifier, - tabIndex: selectedTabIndex, + subTitle: chart.procedures[0], + tabIndex: ChartTabTypeToIndex[selectedTabType], timeAccessed: 0, tag: selectedTab.name, provider: ChartProvider.NAVIGRAPH, pagesViewable: 1, - boundingBox: chart.boundingBox, + boundingBox: chart.bounding_boxes, + chartLinks: { + light: chart.image_day_url, + dark: chart.image_night_url, + }, pageIndex: navigationTabs.findIndex((tab) => tab.associatedTab === NavigationTab.NAVIGRAPH), }), ); @@ -236,9 +246,9 @@ export const NavigraphChartSelector = ({ selectedTab, loading }: NavigraphChartS
- {chart.procedureIdentifier} + {chart.name} - {chart.indexNumber} + {chart.index_number}
diff --git a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartUI.tsx b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartUI.tsx index 9a9f44a5cf1..2cfdbe8409f 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartUI.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartUI.tsx @@ -1,53 +1,48 @@ // Copyright (c) 2023-2024 FlyByWire Simulations // SPDX-License-Identifier: GPL-3.0 -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { ArrowReturnRight } from 'react-bootstrap-icons'; -import { emptyNavigraphCharts, NavigraphAirportCharts } from '@flybywiresim/fbw-sdk'; -import { useNavigraph } from '../../../Apis/Navigraph/Navigraph'; + import { t } from '../../../Localization/translation'; import { NavigraphChartSelector, OrganizedChart } from './NavigraphChartSelector'; -import { NavigationTab, editTabProperty } from '../../../Store/features/navigationPage'; +import { editTabProperty, NavigationTab } from '../../../Store/features/navigationPage'; import { isSimbriefDataLoaded } from '../../../Store/features/simBrief'; import { useAppDispatch, useAppSelector } from '../../../Store/store'; import { SelectGroup, SelectItem } from '../../../UtilComponents/Form/Select'; import { SimpleInput } from '../../../UtilComponents/Form/SimpleInput/SimpleInput'; import { ScrollableContainer } from '../../../UtilComponents/ScrollableContainer'; import { ChartViewer } from '../../Navigation'; +import { navigraphCharts } from '../../../../navigraph'; +import { ChartCategory } from '@flybywiresim/fbw-sdk'; export const NavigraphChartUI = () => { const dispatch = useAppDispatch(); - const navigraph = useNavigraph(); - const [statusBarInfo, setStatusBarInfo] = useState(''); const [icaoAndNameDisagree, setIcaoAndNameDisagree] = useState(false); const [chartListDisagrees, setChartListDisagrees] = useState(false); - const [charts, setCharts] = useState({ - arrival: [], - approach: [], - airport: [], - departure: [], - reference: [], - }); - - const [organizedCharts, setOrganizedCharts] = useState([ - { name: 'STAR', charts: charts.arrival }, - { name: 'APP', charts: charts.approach, bundleRunways: true }, - { name: 'TAXI', charts: charts.airport }, - { name: 'SID', charts: charts.departure }, - { name: 'REF', charts: charts.reference }, - ]); - - const { isFullScreen, searchQuery, chartName, selectedTabIndex } = useAppSelector( + const { availableCharts, isFullScreen, searchQuery, selectedTabType } = useAppSelector( (state) => state.navigationTab[NavigationTab.NAVIGRAPH], ); + const organizedCharts = useMemo( + () => + ({ + STAR: { name: 'STAR', charts: availableCharts.STAR }, + APP: { name: 'APP', charts: availableCharts.APP, bundleRunways: true }, + TAXI: { name: 'TAXI', charts: availableCharts.TAXI }, + SID: { name: 'SID', charts: availableCharts.SID }, + REF: { name: 'REF', charts: availableCharts.REF }, + }) satisfies Record, + [availableCharts], + ); + const assignAirportInfo = async () => { setIcaoAndNameDisagree(true); - const airportInfo = await navigraph.getAirportInfo(searchQuery); + const airportInfo = await navigraphCharts.getAirportInfo({ icao: searchQuery }); setStatusBarInfo(airportInfo?.name || t('NavigationAndCharts.Navigraph.AirportDoesNotExist')); setIcaoAndNameDisagree(false); }; @@ -57,39 +52,40 @@ export const NavigraphChartUI = () => { assignAirportInfo(); } else { setStatusBarInfo(''); - setCharts(emptyNavigraphCharts); + dispatch( + editTabProperty({ + tab: NavigationTab.NAVIGRAPH, + availableCharts: { + STAR: [], + APP: [], + TAXI: [], + SID: [], + REF: [], + }, + }), + ); } }, [searchQuery]); - useEffect(() => { - setOrganizedCharts([ - { name: 'STAR', charts: charts.arrival }, - { name: 'APP', charts: charts.approach, bundleRunways: true }, - { name: 'TAXI', charts: charts.airport }, - { name: 'SID', charts: charts.departure }, - { name: 'REF', charts: charts.reference }, - ]); - }, [charts]); - - useEffect(() => { - if (chartName && (chartName.light !== '' || chartName.dark !== '')) { - const fetchCharts = async () => { - const light = await navigraph.chartCall(searchQuery, chartName.light); - const dark = await navigraph.chartCall(searchQuery, chartName.dark); - dispatch(editTabProperty({ tab: NavigationTab.NAVIGRAPH, chartLinks: { light, dark } })); - }; - fetchCharts(); - } - }, [chartName]); - const handleIcaoChange = async (value: string) => { if (value.length !== 4) return; const newValue = value.toUpperCase(); dispatch(editTabProperty({ tab: NavigationTab.NAVIGRAPH, searchQuery: newValue })); setChartListDisagrees(true); - const chartList = await navigraph.getChartList(newValue); + const chartList = await navigraphCharts.getChartsIndex({ icao: newValue }); if (chartList) { - setCharts(chartList); + dispatch( + editTabProperty({ + tab: NavigationTab.NAVIGRAPH, + availableCharts: { + STAR: chartList.filter((it) => it.category === 'ARR'), + APP: chartList.filter((it) => it.category === 'APP'), + TAXI: chartList.filter((it) => it.category === 'APT'), + SID: chartList.filter((it) => it.category === 'DEP'), + REF: chartList.filter((it) => it.category === 'REF'), + }, + }), + ); } setChartListDisagrees(false); }; @@ -165,21 +161,21 @@ export const NavigraphChartUI = () => {
- {organizedCharts.map((organizedChart, index) => ( + {(['STAR', 'APP', 'TAXI', 'SID', 'REF'] satisfies ChartCategory[]).map((tabType) => ( - dispatch(editTabProperty({ tab: NavigationTab.NAVIGRAPH, selectedTabIndex: index })) + dispatch(editTabProperty({ tab: NavigationTab.NAVIGRAPH, selectedTabType: tabType })) } - key={organizedChart.name} + key={tabType} className="flex w-full justify-center" > - {organizedChart.name} + {tabType} ))} - +
diff --git a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/PinnedChartsPage.tsx b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/PinnedChartsPage.tsx index 7672a9bcbc5..b78b91f83f2 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/PinnedChartsPage.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Navigation/Pages/PinnedChartsPage.tsx @@ -10,8 +10,10 @@ import { TooltipWrapper } from '../../UtilComponents/TooltipWrapper'; import { useAppDispatch, useAppSelector } from '../../Store/store'; import { ChartProvider, + ChartTabTypeIndices, editPinnedChart, editTabProperty, + LocalChartTabTypeIndices, NavigationTab, PinnedChart, removedPinnedChart, @@ -88,11 +90,24 @@ export const PinnedChartCard = ({ pinnedChart, className, showDelete }: PinnedCh className={`${showDelete && 'rounded-t-none'} bg-theme-accent relative flex flex-col flex-wrap overflow-hidden rounded-md px-2 pb-2 pt-3 ${className}`} onClick={() => { dispatch(editTabProperty({ tab, chartDimensions: { width: undefined, height: undefined } })); - dispatch(editTabProperty({ tab, chartLinks: { light: '', dark: '' } })); + dispatch( + editTabProperty({ + tab, + chartLinks: { light: pinnedChart.chartLinks?.light ?? '', dark: pinnedChart.chartLinks?.dark ?? '' }, + }), + ); dispatch(editTabProperty({ tab, chartName })); dispatch(editTabProperty({ tab, chartId })); dispatch(editTabProperty({ tab, searchQuery: title })); - dispatch(editTabProperty({ tab, selectedTabIndex: tabIndex })); + dispatch( + editTabProperty({ + tab, + selectedTabType: + provider === ChartProvider.NAVIGRAPH + ? ChartTabTypeIndices[tabIndex] + : LocalChartTabTypeIndices[tabIndex], + }), + ); dispatch(editTabProperty({ tab, chartRotation: 0 })); dispatch(editTabProperty({ tab, currentPage: 1 })); dispatch(setBoundingBox(undefined)); diff --git a/fbw-common/src/systems/instruments/src/EFB/Settings/Pages/ThirdPartyOptionsPage.tsx b/fbw-common/src/systems/instruments/src/EFB/Settings/Pages/ThirdPartyOptionsPage.tsx index 22722869fb7..eb7df10cfcf 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Settings/Pages/ThirdPartyOptionsPage.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Settings/Pages/ThirdPartyOptionsPage.tsx @@ -10,15 +10,14 @@ import { Toggle } from '../../UtilComponents/Form/Toggle'; import { FullscreenSettingsPage, SettingItem, SettingsPage } from '../Settings'; import { t } from '../../Localization/translation'; import { NavigraphAuthUIWrapper, useNavigraphAuthInfo } from '../../Apis/Navigraph/Components/Authentication'; -import { useNavigraph } from '../../Apis/Navigraph/Navigraph'; import { TooltipWrapper } from '../../UtilComponents/TooltipWrapper'; import { SimpleInput } from '../../UtilComponents/Form/SimpleInput/SimpleInput'; // @ts-ignore import NavigraphIcon from '../../Assets/navigraph-logo-alone.svg'; +import { navigraphAuth } from '../../../navigraph'; export const ThirdPartyOptionsPage = () => { const history = useHistory(); - const navigraph = useNavigraph(); const navigraphAuthInfo = useNavigraphAuthInfo(); const [gsxFuelSyncEnabled, setGsxFuelSyncEnabled] = usePersistentNumberProperty('GSX_FUEL_SYNC', 0); @@ -95,7 +94,7 @@ export const ThirdPartyOptionsPage = () => { }; const handleNavigraphAccountUnlink = () => { - navigraph.deAuthenticate(); + navigraphAuth.signOut(); }; return ( diff --git a/fbw-common/src/systems/instruments/src/EFB/Store/features/navigationPage.ts b/fbw-common/src/systems/instruments/src/EFB/Store/features/navigationPage.ts index ced72351f2a..c1050ccf578 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Store/features/navigationPage.ts +++ b/fbw-common/src/systems/instruments/src/EFB/Store/features/navigationPage.ts @@ -1,9 +1,10 @@ // Copyright (c) 2023-2024 FlyByWire Simulations // SPDX-License-Identifier: GPL-3.0 -import { NavigraphBoundingBox } from '@flybywiresim/fbw-sdk'; - +import { ChartCategory, LocalChartCategory, NavigraphAirportCharts } from '@flybywiresim/fbw-sdk'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { Chart } from 'navigraph/charts'; import { store, RootState } from '../store'; import { PinSort } from '../../Navigation/Pages/PinnedChartsPage'; @@ -35,14 +36,32 @@ export type PinnedChart = { tag: string; provider: ChartProvider; pagesViewable: number; - boundingBox?: NavigraphBoundingBox; + boundingBox?: Chart['bounding_boxes']; + chartLinks?: { light: string; dark: string }; pageIndex: number; }; -type ProviderTabInfo = { +export const ChartTabTypeIndices: readonly ChartCategory[] = ['STAR', 'APP', 'TAXI', 'SID', 'REF']; +export const LocalChartTabTypeIndices: readonly LocalChartCategory[] = ['IMAGE', 'PDF', 'BOTH']; + +export const ChartTabTypeToIndex: Record = { + STAR: 0, + APP: 1, + TAXI: 2, + SID: 3, + REF: 4, +}; + +export const LocalChartCategoryToIndex: Record = { + IMAGE: 0, + PDF: 1, + BOTH: 2, +}; + +type ProviderTabInfo = { chartRotation: number; searchQuery: string; - selectedTabIndex: number; + selectedTabType: C; isFullScreen: boolean; chartDimensions: { width?: number; @@ -56,11 +75,17 @@ type ProviderTabInfo = { chartPosition: { positionX: number; positionY: number; scale: number }; }; +type NavigraphProviderTabInfo = ProviderTabInfo & { + availableCharts: NavigraphAirportCharts; +}; + +type LocalFilesTabInfo = ProviderTabInfo; + interface InitialChartState { selectedNavigationTabIndex: number; usingDarkTheme: boolean; - [NavigationTab.NAVIGRAPH]: ProviderTabInfo; - [NavigationTab.LOCAL_FILES]: ProviderTabInfo; + [NavigationTab.NAVIGRAPH]: NavigraphProviderTabInfo; + [NavigationTab.LOCAL_FILES]: LocalFilesTabInfo; [NavigationTab.PINNED_CHARTS]: { searchQuery: string; selectedProvider: ChartProvider | 'ALL'; @@ -69,7 +94,7 @@ interface InitialChartState { editMode: boolean; }; planeInFocus: boolean; - boundingBox?: NavigraphBoundingBox; + boundingBox?: Chart['bounding_boxes']; pagesViewable: number; pinnedCharts: PinnedChart[]; provider: ChartProvider; @@ -79,9 +104,16 @@ const initialState: InitialChartState = { selectedNavigationTabIndex: 0, usingDarkTheme: true, [NavigationTab.NAVIGRAPH]: { + availableCharts: { + STAR: [], + APP: [], + TAXI: [], + SID: [], + REF: [], + }, chartRotation: 0, searchQuery: '', - selectedTabIndex: 0, + selectedTabType: 'STAR', isFullScreen: false, chartDimensions: { width: 0, @@ -107,7 +139,7 @@ const initialState: InitialChartState = { [NavigationTab.LOCAL_FILES]: { chartRotation: 0, searchQuery: '', - selectedTabIndex: 0, + selectedTabType: 'PDF', isFullScreen: false, chartDimensions: { width: 0, @@ -157,7 +189,7 @@ export const navigationTabSlice = createSlice({ setPlaneInFocus: (state, action: PayloadAction) => { state.planeInFocus = action.payload; }, - setBoundingBox: (state, action: PayloadAction) => { + setBoundingBox: (state, action: PayloadAction) => { state.boundingBox = action.payload; }, setProvider: (state, action: PayloadAction) => { @@ -200,11 +232,8 @@ export const navigationTabSlice = createSlice({ /** * @returns Whether or not associated chart with the passed chartId is pinned */ -export const isChartPinned = (chartId: string): boolean => { - return (store.getState() as RootState).navigationTab.pinnedCharts.some( - (pinnedChart) => pinnedChart.chartId === chartId, - ); -}; +export const isChartPinned = (chartId: string): boolean => + (store.getState() as RootState).navigationTab.pinnedCharts.some((pinnedChart) => pinnedChart.chartId === chartId); export const { setUsingDarkTheme, diff --git a/fbw-common/src/systems/instruments/src/navigraph.ts b/fbw-common/src/systems/instruments/src/navigraph.ts new file mode 100644 index 00000000000..0f118c8c3cd --- /dev/null +++ b/fbw-common/src/systems/instruments/src/navigraph.ts @@ -0,0 +1,23 @@ +import { initializeApp, Scope } from 'navigraph/app'; +import { getAuth } from 'navigraph/auth'; +import { getChartsAPI } from 'navigraph/charts'; +import { NXDataStore } from '@flybywiresim/fbw-sdk'; + +initializeApp({ + clientId: process.env.CLIENT_ID, + clientSecret: process.env.CLIENT_SECRET, + scopes: [Scope.CHARTS], +}); + +export const navigraphAuth = getAuth({ + storage: { + getItem: (key) => NXDataStore.get(key), + setItem: (key, value) => NXDataStore.set(key, value), + }, + keys: { + accessToken: 'NAVIGRAPH_ACCESS_TOKEN', + refreshToken: 'NAVIGRAPH_REFRESH_TOKEN', + }, +}); + +export const navigraphCharts = getChartsAPI(); diff --git a/fbw-common/src/systems/instruments/src/react/navigraph.tsx b/fbw-common/src/systems/instruments/src/react/navigraph.tsx new file mode 100644 index 00000000000..23ed7a0bfad --- /dev/null +++ b/fbw-common/src/systems/instruments/src/react/navigraph.tsx @@ -0,0 +1,52 @@ +import { User } from 'navigraph/auth'; +import React, { useState, useEffect, useContext, createContext } from 'react'; + +import { navigraphAuth } from '../navigraph'; +import { NXDataStore } from '@flybywiresim/fbw-sdk'; + +interface NavigraphAuthContext { + initialized: boolean; + user: User | null; + signIn: typeof navigraphAuth.signInWithDeviceFlow; +} + +const authContext = createContext({ + initialized: false, + user: null, + signIn: () => Promise.reject(new Error('Not initialized')), +}); + +function useProvideAuth() { + const [user, setUser] = useState(null); + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + const unsubscribe = navigraphAuth.onAuthStateChanged((u) => { + if (!initialized) { + setInitialized(true); + } + + if (u) { + // Compat for some old legacy JS + NXDataStore.set('NAVIGRAPH_USERNAME', u.preferred_username); + } + + setUser(u); + }); + + return () => unsubscribe(); + }, []); + + return { + user, + initialized, + signIn: navigraphAuth.signInWithDeviceFlow, + }; +} + +export function NavigraphAuthProvider({ children }: { children: React.ReactNode }) { + const auth = useProvideAuth(); + return {children}; +} + +export const useNavigraphAuth = () => useContext(authContext); diff --git a/fbw-common/src/systems/shared/src/navigraph/client.ts b/fbw-common/src/systems/shared/src/navigraph/client.ts index b444bc03101..5f4b6e67a74 100644 --- a/fbw-common/src/systems/shared/src/navigraph/client.ts +++ b/fbw-common/src/systems/shared/src/navigraph/client.ts @@ -1,348 +1,12 @@ -import pkce from '@navigraph/pkce'; -import { AirportInfo, AuthType, NavigraphAirportCharts, NavigraphChart, NavigraphSubscriptionStatus } from './types'; -import { NXDataStore } from '../persistence'; - -const NAVIGRAPH_API_SCOPES = 'openid charts offline_access'; - -const NAVIGRAPH_DEFAULT_AUTH_STATE = { - code: '', - link: '', - qrLink: '', - interval: 5, - disabled: false, -}; - -export const emptyNavigraphCharts = { - arrival: [], - approach: [], - airport: [], - departure: [], - reference: [], -}; - -function formatFormBody(body: Object) { - return Object.keys(body) - .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(body[key])}`) - .join('&'); -} - -export class NavigraphClient { +export class NavigraphKeys { private static clientId = process.env.CLIENT_ID; private static clientSecret = process.env.CLIENT_SECRET; - private pkce: ReturnType; - - private deviceCode: string; - - private refreshToken: string | null = null; - - public tokenRefreshInterval = 3600; - - private accessToken: string | null = null; - - public auth: AuthType = NAVIGRAPH_DEFAULT_AUTH_STATE; - - public userName = ''; - public static get hasSufficientEnv() { - if (NavigraphClient.clientSecret === undefined || NavigraphClient.clientId === undefined) { + if (NavigraphKeys.clientSecret === undefined || NavigraphKeys.clientId === undefined) { return false; } - return !(NavigraphClient.clientSecret === '' || NavigraphClient.clientId === ''); - } - - constructor() { - if (NavigraphClient.hasSufficientEnv) { - this.pkce = pkce(); - - const token = NXDataStore.get('NAVIGRAPH_REFRESH_TOKEN'); - - if (token) { - this.refreshToken = token; - this.getToken(); - } - } - } - - public async authenticate(): Promise { - this.pkce = pkce(); - this.refreshToken = null; - - const secret = { - client_id: NavigraphClient.clientId, - client_secret: NavigraphClient.clientSecret, - code_challenge: this.pkce.code_challenge, - code_challenge_method: 'S256', - }; - - try { - const authResp = await fetch('https://identity.api.navigraph.com/connect/deviceauthorization', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, - body: formatFormBody(secret), - }); - - if (authResp.ok) { - const json = await authResp.json(); - - this.auth.code = json.user_code; - this.auth.link = json.verification_uri; - this.auth.qrLink = json.verification_uri_complete; - this.auth.interval = json.interval; - this.deviceCode = json.device_code; - } - } catch (_) { - console.log('Unable to Authorize Device. #NV101'); - } - } - - public deAuthenticate() { - this.refreshToken = null; - this.accessToken = null; - this.userName = ''; - this.auth = NAVIGRAPH_DEFAULT_AUTH_STATE; - NXDataStore.set('NAVIGRAPH_REFRESH_TOKEN', ''); - NXDataStore.set('NAVIGRAPH_USERNAME', ''); - } - - private async tokenCall(body): Promise { - if (this.deviceCode || !this.auth.disabled) { - try { - const tokenResp = await fetch('https://identity.api.navigraph.com/connect/token/', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, - body: formatFormBody(body), - }); - - if (tokenResp.ok) { - const json = await tokenResp.json(); - - const refreshToken = json.refresh_token; - - this.refreshToken = refreshToken; - NXDataStore.set('NAVIGRAPH_REFRESH_TOKEN', refreshToken); - - this.accessToken = json.access_token; - - await this.assignUserName(); - } else { - const respText = await tokenResp.text(); - - const parsedText = JSON.parse(respText); - - const { error } = parsedText; - - switch (error) { - case 'authorization_pending': { - console.log('Token Authorization Pending'); - break; - } - case 'slow_down': { - this.auth.interval += 5; - break; - } - case 'access_denied': { - this.auth.disabled = true; - throw new Error('Access Denied'); - } - default: { - await this.authenticate(); - } - } - } - } catch (e) { - console.log('Token Authentication Failed. #NV102'); - if (e.message === 'Access Denied') { - throw e; - } - } - } - } - - public async getToken(): Promise { - if (NavigraphClient.hasSufficientEnv) { - const newTokenBody = { - grant_type: 'urn:ietf:params:oauth:grant-type:device_code', - device_code: this.deviceCode, - client_id: NavigraphClient.clientId, - client_secret: NavigraphClient.clientSecret, - scope: NAVIGRAPH_API_SCOPES, - code_verifier: this.pkce.code_verifier, - }; - - const refreshTokenBody = { - grant_type: 'refresh_token', - refresh_token: this.refreshToken, - client_id: NavigraphClient.clientId, - client_secret: NavigraphClient.clientSecret, - }; - - if (!this.refreshToken) { - await this.tokenCall(newTokenBody); - } else { - await this.tokenCall(refreshTokenBody); - } - } - } - - public async chartCall(icao: string, item: string): Promise { - if (icao.length === 4) { - const callResp = await fetch(`https://charts.api.navigraph.com/2/airports/${icao}/signedurls/${item}`, { - headers: { - Authorization: `Bearer ${this.accessToken}`, - }, - }); - if (callResp.ok) { - return callResp.text(); - } - // Unauthorized - if (callResp.status === 401) { - await this.getToken(); - return this.chartCall(icao, item); - } - } - return Promise.reject(); - } - - public async amdbCall(query: string): Promise { - const callResp = await fetch(`https://amdb.api.navigraph.com/v1/${query}`, { - headers: { - Authorization: `Bearer ${this.accessToken}`, - }, - }); - - if (callResp.ok) { - return callResp.text(); - } - - // Unauthorized - if (callResp.status === 401) { - await this.getToken(); - - return this.amdbCall(query); - } - - return Promise.reject(); - } - - public async getChartList(icao: string): Promise { - if (this.hasToken) { - const chartJsonUrl = await this.chartCall(icao, 'charts.json'); - - const chartJsonResp = await fetch(chartJsonUrl); - - if (chartJsonResp.ok) { - const chartJson = await chartJsonResp.json(); - - const chartArray: NavigraphChart[] = chartJson.charts.map((chart) => ({ - fileDay: chart.file_day, - fileNight: chart.file_night, - thumbDay: chart.thumb_day, - thumbNight: chart.thumb_night, - icaoAirportIdentifier: chart.icao_airport_identifier, - id: chart.id, - extId: chart.ext_id, - fileName: chart.file_name, - type: { - code: chart.type.code, - category: chart.type.category, - details: chart.type.details, - precision: chart.type.precision, - section: chart.type.section, - }, - indexNumber: chart.index_number, - procedureIdentifier: chart.procedure_identifier, - runway: chart.runway, - boundingBox: chart.planview - ? { - bottomLeft: { - lat: chart.planview.bbox_geo[1], - lon: chart.planview.bbox_geo[0], - xPx: chart.planview.bbox_local[0], - yPx: chart.planview.bbox_local[1], - }, - topRight: { - lat: chart.planview.bbox_geo[3], - lon: chart.planview.bbox_geo[2], - xPx: chart.planview.bbox_local[2], - yPx: chart.planview.bbox_local[3], - }, - width: chart.bbox_local[2], - height: chart.bbox_local[1], - } - : undefined, - })); - - return { - arrival: chartArray.filter((chart) => chart.type.category === 'ARRIVAL'), - approach: chartArray.filter((chart) => chart.type.category === 'APPROACH'), - airport: chartArray.filter((chart) => chart.type.category === 'AIRPORT'), - departure: chartArray.filter((chart) => chart.type.category === 'DEPARTURE'), - reference: chartArray.filter( - (chart) => - chart.type.category !== 'ARRIVAL' && - chart.type.category !== 'APPROACH' && - chart.type.category !== 'AIRPORT' && - chart.type.category !== 'DEPARTURE', - ), - }; - } - } - - return emptyNavigraphCharts; - } - - public async getAirportInfo(icao: string): Promise { - if (this.hasToken) { - const chartJsonUrl = await this.chartCall(icao, 'airport.json'); - - const chartJsonResp = await fetch(chartJsonUrl); - - if (chartJsonResp.ok) { - const chartJson = await chartJsonResp.json(); - - return { name: chartJson.name }; - } - } - - return null; - } - - public get hasToken(): boolean { - return !!this.accessToken; - } - - public async assignUserName(): Promise { - if (this.hasToken) { - try { - const userInfoResp = await fetch('https://identity.api.navigraph.com/connect/userinfo', { - headers: { Authorization: `Bearer ${this.accessToken}` }, - }); - - if (userInfoResp.ok) { - const userInfoJson = await userInfoResp.json(); - - this.userName = userInfoJson.preferred_username; - NXDataStore.set('NAVIGRAPH_USERNAME', this.userName); - } - } catch (_) { - console.log('Unable to Fetch User Info. #NV103'); - } - } - } - - public async fetchSubscriptionStatus(): Promise { - if (this.hasToken) { - const decodedToken = JSON.parse(atob(this.accessToken.split('.')[1])); - - const subscriptionTypes = decodedToken.subscriptions as string[]; - - if (subscriptionTypes.includes('fmsdata') && subscriptionTypes.includes('charts')) { - return NavigraphSubscriptionStatus.Unlimited; - } - } - - return NavigraphSubscriptionStatus.None; + return !(NavigraphKeys.clientSecret === '' || NavigraphKeys.clientId === ''); } } diff --git a/fbw-common/src/systems/shared/src/navigraph/types.ts b/fbw-common/src/systems/shared/src/navigraph/types.ts index 453b0092c36..1ef45a8d235 100644 --- a/fbw-common/src/systems/shared/src/navigraph/types.ts +++ b/fbw-common/src/systems/shared/src/navigraph/types.ts @@ -1,56 +1,13 @@ +import { Chart } from 'navigraph/charts'; + export enum NavigraphSubscriptionStatus { None, Unlimited, Unknown, } -export interface NavigraphBoundingBox { - bottomLeft: { lat: number; lon: number; xPx: number; yPx: number }; - topRight: { lat: number; lon: number; xPx: number; yPx: number }; - width: number; - height: number; -} - -export interface ChartType { - code: string; - category: string; - details: string; - precision: string; - section: string; -} - -export interface NavigraphChart { - fileDay: string; - fileNight: string; - thumbDay: string; - thumbNight: string; - icaoAirportIdentifier: string; - id: string; - extId: string; - fileName: string; - type: ChartType; - indexNumber: string; - procedureIdentifier: string; - runway: string[]; - boundingBox?: NavigraphBoundingBox; -} +export type ChartCategory = 'STAR' | 'APP' | 'TAXI' | 'SID' | 'REF'; -export interface NavigraphAirportCharts { - arrival: NavigraphChart[]; - approach: NavigraphChart[]; - airport: NavigraphChart[]; - departure: NavigraphChart[]; - reference: NavigraphChart[]; -} +export type LocalChartCategory = 'IMAGE' | 'PDF' | 'BOTH'; -export interface AirportInfo { - name: string; -} - -export interface AuthType { - code: string; - link: string; - qrLink: string; - interval: number; - disabled: boolean; -} +export type NavigraphAirportCharts = Record; diff --git a/package.json b/package.json index cb501aaea22..9cb0ef48771 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-tailwindcss": "^3.13.0", + "eslint-plugin-tailwindcss": "^3.17.3", "fs-extra": "^10.1.0", "jest": "^29.7.0", "jsdom": "^16.4.0", @@ -158,6 +158,7 @@ "@sentry/tracing": "^6.17.7", "@tabler/icons": "^1.41.2", "@types/react-canvas-draw": "^1.1.1", + "navigraph": "^1.2.35", "byte-data": "^19.0.1", "classnames": "^2.2.6", "esbuild-plugin-inline-image": "^0.0.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e43c29d629..81ea3138b6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ dependencies: nanoid: specifier: ^3.3.1 version: 3.3.7 + navigraph: + specifier: ^1.2.35 + version: 1.2.35 network: specifier: ^0.6.1 version: 0.6.1 @@ -260,8 +263,8 @@ devDependencies: specifier: ^4.6.0 version: 4.6.0(eslint@8.56.0) eslint-plugin-tailwindcss: - specifier: ^3.13.0 - version: 3.13.0(tailwindcss@3.3.6) + specifier: ^3.17.3 + version: 3.17.3(tailwindcss@3.3.6) fs-extra: specifier: ^10.1.0 version: 10.1.0 @@ -2498,6 +2501,31 @@ packages: resolution: {integrity: sha512-IWPuI0cKrybcB1xScZx+hTekXyAMFb3wQLjcS6sLXyDygsitH4M0VvpuIfL3KCKRfUUaQRgFhZCXymJ28qWnqA==} dev: false + /@navigraph/app@1.3.5: + resolution: {integrity: sha512-TyfqLvc9AsyXFSaExj0wpo80Qrl/uPYXs1PTx3zVRUSAoGAtsqawtKLC1AFGtfz2EzF/IAeQ3+RMSS24ykFWAA==} + dev: false + + /@navigraph/auth@2.5.1: + resolution: {integrity: sha512-wM0UAlEtEyFT7lZMnLeSjOFt8GfsaJz41JePBVVgCauoRGZYcVxmkKz9IUMQVBgGsdCn5fOillT7LfpDqsUHQg==} + engines: {node: '>=10'} + dependencies: + '@navigraph/app': 1.3.5 + dev: false + + /@navigraph/charts@2.0.4: + resolution: {integrity: sha512-6KK4emnmKwsz8rpJAuo9L1wDbloyfztZKdxBb2Xa2djm0aFjonGAFgbb+04ZEfJ9zPFgXBz6b5apnLOJ/S9RfA==} + dependencies: + '@navigraph/app': 1.3.5 + '@navigraph/auth': 2.5.1 + dev: false + + /@navigraph/packages@1.0.0: + resolution: {integrity: sha512-1JHR+BakEAFnIFd5YqMRMlLIu6TWyPVGTuvls0UGk8ylihHdX49EmCj0s2qv19mratjVaEjhiy5LSsNFst3few==} + dependencies: + '@navigraph/app': 1.3.5 + '@navigraph/auth': 2.5.1 + dev: false + /@navigraph/pkce@1.0.3: resolution: {integrity: sha512-eVR3+TMAoQYcmGPIkayDwbCfdxv8vSgrvWOeqKX5HK34liOMkWmHBDpDN3lYbpySsGSNO+QKJhmAKERrHW+F2w==} dev: true @@ -5013,11 +5041,11 @@ packages: string.prototype.matchall: 4.0.10 dev: true - /eslint-plugin-tailwindcss@3.13.0(tailwindcss@3.3.6): - resolution: {integrity: sha512-Fcep4KDRLWaK3KmkQbdyKHG0P4GdXFmXdDaweTIPcgOP60OOuWFbh1++dufRT28Q4zpKTKaHwTsXPJ4O/EjU2Q==} - engines: {node: '>=12.13.0'} + /eslint-plugin-tailwindcss@3.17.3(tailwindcss@3.3.6): + resolution: {integrity: sha512-DVMVVUFQ+lPraRSuUk2I41XMnusXT6b3WaQZYlUHduULnERaqe9sNfmpRY1IyxlzmKoQxSbZ8IHRhl9ePo8PeA==} + engines: {node: '>=18.12.0'} peerDependencies: - tailwindcss: ^3.3.2 + tailwindcss: ^3.4.0 dependencies: fast-glob: 3.3.2 postcss: 8.4.32 @@ -6952,6 +6980,15 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /navigraph@1.2.35: + resolution: {integrity: sha512-ykcbI8J9mHQc0YIEZ1hNWUWg1toyHfcXi/bVnmpGeaLRWCboI4XfRS35WCHIR77QUywbUsegqy+5WQaEEmGr6g==} + dependencies: + '@navigraph/app': 1.3.5 + '@navigraph/auth': 2.5.1 + '@navigraph/charts': 2.0.4 + '@navigraph/packages': 1.0.0 + dev: false + /needle@3.3.0: resolution: {integrity: sha512-Kaq820952NOrLY/LVbIhPZeXtCGDBAPVgT0BYnoT3p/Nr3nkGXdvWXXA3zgy7wpAgqRULu9p/NvKiFo6f/12fw==} engines: {node: '>= 4.4.x'} diff --git a/scripts/build.sh b/scripts/build.sh index d0f4212ec85..eb95e765923 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -28,7 +28,7 @@ for arg in "$@"; do done # run build with the new arguments -time npx igniter "${args[@]}" +FBW_TYPECHECK=1 time npx igniter "${args[@]}" # restore ownership (when run as github action) if [ "${GITHUB_ACTIONS}" == "true" ]; then