From 72e762980dc983cfe09bac20d6312568794c6024 Mon Sep 17 00:00:00 2001 From: Benjamin Dupont <4503241+Benjozork@users.noreply.github.com> Date: Sun, 16 Jul 2023 20:19:54 -0400 Subject: [PATCH] Initial commit --- .../html_ui/Pages/A32NX_Core/A32NX_ATSU.js | 14 +- .../Pages/A32NX_Utils/NXSystemMessages.js | 6 +- .../html_ui/Pages/A32NX_Utils/SimBriefApi.js | 8 +- fbw-a32nx/src/localization/flypad/en.json | 21 + fbw-a32nx/src/systems/extras-host/build.js | 2 +- .../Navigraph/Components/Authentication.tsx | 94 ++++- .../src/EFB/Apis/Navigraph/Navigraph.tsx | 364 +----------------- .../src/EFB/Apis/Simbrief/simbriefParser.ts | 8 +- .../EFB/Dashboard/Widgets/FlightWidget.tsx | 4 +- .../src/EFB/Dispatch/Pages/LoadsheetPage.tsx | 4 +- .../src/systems/instruments/src/EFB/Efb.tsx | 8 +- .../Pages/NavigraphPage/NavigraphChartUI.tsx | 7 +- .../Pages/NavigraphPage/NavigraphPage.tsx | 6 +- .../src/EFB/Settings/Pages/AtsuAocPage.tsx | 67 +--- .../Settings/Pages/ThirdPartyOptionsPage.tsx | 103 +++-- .../instruments/src/EFB/Settings/Settings.tsx | 24 +- .../src/EFB/Store/features/simBrief.ts | 10 +- fbw-a32nx/src/systems/systems-host/build.js | 2 +- fbw-common/src/systems/shared/src/index.ts | 1 + .../systems/shared/src/navigraph/client.ts | 343 +++++++++++++++++ .../src/systems/shared/src/navigraph/index.ts | 2 + .../src/systems/shared/src/navigraph/types.ts | 56 +++ 22 files changed, 667 insertions(+), 487 deletions(-) create mode 100644 fbw-common/src/systems/shared/src/navigraph/client.ts create mode 100644 fbw-common/src/systems/shared/src/navigraph/index.ts create mode 100644 fbw-common/src/systems/shared/src/navigraph/types.ts diff --git a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Core/A32NX_ATSU.js b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Core/A32NX_ATSU.js index 193604373ca2..23724872a946 100644 --- a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Core/A32NX_ATSU.js +++ b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Core/A32NX_ATSU.js @@ -1,3 +1,7 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + function translateAtsuMessageType(type) { switch (type) { case AtsuCommon.AtsuMessageType.Freetext: @@ -38,18 +42,18 @@ const lbsToKg = (value) => { * @param {() => void} updateView */ const getSimBriefOfp = (mcdu, updateView, callback = () => {}) => { - const simBriefUserId = NXDataStore.get("CONFIG_SIMBRIEF_USERID", ""); + const navigraphUsername = NXDataStore.get("NAVIGRAPH_USERNAME", ""); - if (!simBriefUserId) { - mcdu.setScratchpadMessage(NXFictionalMessages.noSimBriefUser); - throw new Error("No SimBrief pilot ID provided"); + if (!navigraphUsername) { + mcdu.setScratchpadMessage(NXFictionalMessages.noNavigraphUser); + throw new Error("No Navigraph username provided"); } mcdu.simbrief["sendStatus"] = "REQUESTING"; updateView(); - return SimBriefApi.getSimBriefOfp(simBriefUserId) + return SimBriefApi.getSimBriefOfp(navigraphUsername) .then(data => { mcdu.simbrief["units"] = data.params.units; mcdu.simbrief["route"] = data.general.route; diff --git a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Utils/NXSystemMessages.js b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Utils/NXSystemMessages.js index 8f14c262b94c..7fae8cf815bd 100644 --- a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Utils/NXSystemMessages.js +++ b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Utils/NXSystemMessages.js @@ -1,3 +1,7 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + class McduMessage { constructor(text, isAmber = false, replace = "") { this.text = text; @@ -102,7 +106,7 @@ const NXSystemMessages = { }; const NXFictionalMessages = { - noSimBriefUser: new TypeIMessage("NO SIMBRIEF USER"), + noNavigraphUser: new TypeIMessage("NO NAVIGRAPH USER"), noAirportSpecified: new TypeIMessage("NO AIRPORT SPECIFIED"), fltNbrInUse: new TypeIMessage("FLT NBR IN USE"), fltNbrMissing: new TypeIMessage("ENTER ATC FLT NBR"), diff --git a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Utils/SimBriefApi.js b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Utils/SimBriefApi.js index f51fffd3832d..57521bff7d23 100644 --- a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Utils/SimBriefApi.js +++ b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Utils/SimBriefApi.js @@ -1,7 +1,11 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + class SimBriefApi { static getSimBriefOfp(userId) { if (userId) { - return fetch(`${SimBriefApi.url}&userid=${userId}`) + return fetch(`${SimBriefApi.url}&username=${userId}`) .then((response) => { if (!response.ok) { throw new HttpError(response.status); @@ -10,7 +14,7 @@ class SimBriefApi { return response.json(); }); } else { - throw new Error("No SimBrief pilot ID provided"); + throw new Error("No Navigraph username provided"); } } } diff --git a/fbw-a32nx/src/localization/flypad/en.json b/fbw-a32nx/src/localization/flypad/en.json index b0c81f30f191..4201ef625a4e 100644 --- a/fbw-a32nx/src/localization/flypad/en.json +++ b/fbw-a32nx/src/localization/flypad/en.json @@ -223,6 +223,10 @@ }, "Navigraph": { "AirportDoesNotExist": "Airport Does Not Exist", + "GoToThirdPartyOptions": { + "Title": "Go to 3rd Party Options to link Navigraph account", + "Button": "3rd Party Options" + }, "AuthenticateWithNavigraph": "Authenticate with Navigraph", "InsufficientEnv": "Insufficient .env File", "IntoYourBrowserAndEnterTheCodeBelow": "into your browser and enter the code below\n", @@ -574,6 +578,23 @@ "inHg": "inHg" }, "ThirdPartyOptions": { + "TT": { + "OverrideSimBriefUserID": "Alternative SimBrief user ID to use instead of the linked Navigraph Account" + }, + "NavigraphAccountLink": { + "SettingTitle": "Navigraph Account Link", + "Connected": "Connected", + "Link": "Link Account", + "Unlink": "Unlink Account", + "SubscriptionStatus": { + "None": "None", + "Unlimited": "Navigraph Unlimited" + }, + "LoginPage": { + "Title": "Navigraph Account Login" + } + }, + "OverrideSimBriefUserID": "Override SimBrief User ID", "GsxFuelEnabled": "GSX Fueling Synchronization", "GsxPayloadEnabled": "GSX Payload Synchronization", "Title": "3rd Party Options" diff --git a/fbw-a32nx/src/systems/extras-host/build.js b/fbw-a32nx/src/systems/extras-host/build.js index 55dbc0cffd7a..f2d44658b52d 100644 --- a/fbw-a32nx/src/systems/extras-host/build.js +++ b/fbw-a32nx/src/systems/extras-host/build.js @@ -15,7 +15,7 @@ const isProductionBuild = process.env.A32NX_PRODUCTION_BUILD === '1'; esbuild.build({ absWorkingDir: __dirname, - define: { DEBUG: 'false' }, + define: { 'DEBUG': 'false', 'process.env.CLIENT_ID': `'${process.env.CLIENT_ID}'`, 'process.env.CLIENT_SECRET': `'${process.env.CLIENT_SECRET}'` }, entryPoints: ['./index.ts'], bundle: true, diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Navigraph/Components/Authentication.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Navigraph/Components/Authentication.tsx index 1f3cd4d25f2c..f9fef84857f7 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Navigraph/Components/Authentication.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Navigraph/Components/Authentication.tsx @@ -7,9 +7,42 @@ import React, { useEffect, useState } from 'react'; import { CloudArrowDown, ShieldLock } from 'react-bootstrap-icons'; import { toast } from 'react-toastify'; import QRCode from 'qrcode.react'; -import { usePersistentProperty } from '@flybywiresim/fbw-sdk'; +import { NavigraphClient, NavigraphSubscriptionStatus, usePersistentProperty } from '@flybywiresim/fbw-sdk'; +import { useHistory } from 'react-router-dom'; import { t } from '../../../translation'; -import NavigraphClient, { useNavigraph } from '../Navigraph'; +import { useNavigraph } from '../Navigraph'; + +export type NavigraphAuthInfo = { + loggedIn: false, +} | { + loggedIn: true, + + username: string, + + subscriptionStatus: NavigraphSubscriptionStatus, +} + +export const useNavigraphAuthInfo = (): NavigraphAuthInfo => { + const navigraph = useNavigraph(); + + const [tokenAvail, setTokenAvail] = useState(false); + const [subscriptionStatus, setSubscriptionStatus] = useState(NavigraphSubscriptionStatus.None); + + useInterval(() => { + if ((tokenAvail !== navigraph.hasToken) && navigraph.hasToken) { + navigraph.subscriptionStatus().then(setSubscriptionStatus); + } else if (!navigraph.hasToken) { + setSubscriptionStatus(NavigraphSubscriptionStatus.None); + } + + setTokenAvail(navigraph.hasToken); + }, 1000, { runOnStart: true }); + + if (tokenAvail) { + return { loggedIn: tokenAvail, username: navigraph.userName, subscriptionStatus }; + } + return { loggedIn: false }; +}; const Loading = () => { const navigraph = useNavigraph(); @@ -86,7 +119,7 @@ export const NavigraphAuthUI = () => { }, []); return ( -
+
@@ -126,29 +159,70 @@ export const NavigraphAuthUI = () => { ); }; -export const NavigraphAuthUIWrapper = (props) => { - const { children } = props; +export interface NavigraphAuthUIWrapperProps { + showLogin: boolean, + onSuccessfulLogin?: () => void, +} + +export const NavigraphAuthUIWrapper: React.FC = ({ showLogin, onSuccessfulLogin, children }) => { const [tokenAvail, setTokenAvail] = useState(false); const navigraph = useNavigraph(); useInterval(() => { + if (!tokenAvail && navigraph.hasToken) { + onSuccessfulLogin?.(); + } + setTokenAvail(navigraph.hasToken); }, 1000, { runOnStart: true }); + let ui: React.ReactNode; + if (tokenAvail) { + ui = children; + } else if (showLogin) { + ui = ; + } else { + ui = ; + } + return ( NavigraphClient.hasSufficientEnv ? ( <> - {tokenAvail - ? children - : } + {ui} ) : ( -
-

Insufficient .env file

+
+

{t('NavigationAndCharts.Navigraph.InsufficientEnv')}

) ); }; + +export const NavigraphAuthRedirectUI = () => { + const history = useHistory(); + + const handleGoToThirdPArtySettings = () => { + history.push('/settings/3rd-party-options'); + }; + + return ( +
+
+

{t('NavigationAndCharts.Navigraph.GoToThirdPartyOptions.Title')}

+ + +
+
+ ); +}; diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Navigraph/Navigraph.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Navigraph/Navigraph.tsx index d6db7b9ffbd9..fbc4ebfd4ea9 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Navigraph/Navigraph.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Navigraph/Navigraph.tsx @@ -3,369 +3,7 @@ // SPDX-License-Identifier: GPL-3.0 import React, { useContext } from 'react'; -import pkce from '@navigraph/pkce'; - -import { NXDataStore } from '@flybywiresim/fbw-sdk'; - -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 NavigraphAirportCharts = { - arrival: NavigraphChart[], - approach: NavigraphChart[], - airport: NavigraphChart[], - departure: NavigraphChart[], - reference: NavigraphChart[], -}; - -export type AirportInfo = { - name: string, -} - -export type AuthType = { - code: string, - link: string, - qrLink: string, - interval: number, - disabled: boolean, -} - -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 default class NavigraphClient { - private static clientId = process.env.CLIENT_ID; - - private static clientSecret = process.env.CLIENT_SECRET; - - private pkce; - - private deviceCode: string; - - private refreshToken: string | null; - - public tokenRefreshInterval = 3600; - - private accessToken: string; - - public auth: AuthType = { - code: '', - link: '', - qrLink: '', - interval: 5, - disabled: false, - } - - public userName = ''; - - public static get hasSufficientEnv() { - if (NavigraphClient.clientSecret === undefined || NavigraphClient.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'); - } - } - - 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: 'openid charts offline_access', - 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 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; - } - } catch (_) { - console.log('Unable to Fetch User Info. #NV103'); - } - } - } - - public async subscriptionStatus(): Promise { - if (this.hasToken) { - try { - const subscriptionResp = await fetch('https://subscriptions.api.navigraph.com/2/subscriptions/valid', { headers: { Authorization: `Bearer ${this.accessToken}` } }); - - if (subscriptionResp.ok) { - const subscriptionJson = await subscriptionResp.json(); - - return subscriptionJson.subscription_name; - } - } catch (_) { - console.log('Unable to Fetch Subscription Status. #NV104'); - } - } - - return ''; - } -} +import { NavigraphClient } from '@flybywiresim/fbw-sdk'; export const NavigraphContext = React.createContext(undefined!); diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Simbrief/simbriefParser.ts b/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Simbrief/simbriefParser.ts index dd79bf790182..0314e1851cb7 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Simbrief/simbriefParser.ts +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Apis/Simbrief/simbriefParser.ts @@ -1,3 +1,7 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + import { ISimbriefData } from './simbriefInterface'; const simbriefApiUrl = new URL('https://www.simbrief.com/api/xml.fetcher.php'); @@ -8,8 +12,8 @@ const getRequestData: RequestInit = { method: 'GET', }; -export const getSimbriefData = (simbriefUserId: string): Promise => { - simbriefApiParams.append('userid', simbriefUserId); +export const getSimbriefData = (navigraphUsername: string): Promise => { + simbriefApiParams.append('username', navigraphUsername); simbriefApiParams.append('json', '1'); simbriefApiUrl.search = simbriefApiParams.toString(); diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Dashboard/Widgets/FlightWidget.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Dashboard/Widgets/FlightWidget.tsx index 10707a68e762..dc335ee2a91c 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Dashboard/Widgets/FlightWidget.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Dashboard/Widgets/FlightWidget.tsx @@ -67,7 +67,7 @@ const NoSimBriefDataOverlay = ({ simbriefDataLoaded, simbriefDataPending, fetchD export const FlightWidget = () => { const { data } = useAppSelector((state) => state.simbrief); const [simbriefDataPending, setSimbriefDataPending] = useState(false); - const [simbriefUserId] = usePersistentProperty('CONFIG_SIMBRIEF_USERID'); + const [navigraphUsername] = usePersistentProperty('NAVIGRAPH_USERNAME'); const { schedIn, @@ -112,7 +112,7 @@ export const FlightWidget = () => { setSimbriefDataPending(true); try { - const action = await fetchSimbriefDataAction(simbriefUserId ?? ''); + const action = await fetchSimbriefDataAction(navigraphUsername ?? ''); dispatch(action); } catch (e) { diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Dispatch/Pages/LoadsheetPage.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Dispatch/Pages/LoadsheetPage.tsx index 98aea14aa1b4..8727d5f59238 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Dispatch/Pages/LoadsheetPage.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Dispatch/Pages/LoadsheetPage.tsx @@ -19,13 +19,13 @@ const NoSimBriefDataOverlay = () => { const simbriefDataLoaded = isSimbriefDataLoaded(); const [simbriefDataPending, setSimbriefDataPending] = useState(false); - const [simbriefUserId] = usePersistentProperty('CONFIG_SIMBRIEF_USERID'); + const [navigraphUsername] = usePersistentProperty('NAVIGRAPH_USERNAME'); const fetchData = async () => { setSimbriefDataPending(true); try { - const action = await fetchSimbriefDataAction(simbriefUserId ?? ''); + const action = await fetchSimbriefDataAction(navigraphUsername ?? ''); dispatch(action); dispatch(setOfpScroll(0)); diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Efb.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Efb.tsx index c221849687b8..7afbe7460fc5 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Efb.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Efb.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0 import React, { useEffect, useState } from 'react'; -import { useSimVar, useInterval, useInteractionEvent, usePersistentNumberProperty, usePersistentProperty } from '@flybywiresim/fbw-sdk'; +import { useSimVar, useInterval, useInteractionEvent, usePersistentNumberProperty, usePersistentProperty, NavigraphClient } from '@flybywiresim/fbw-sdk'; import { Redirect, Route, Switch, useHistory } from 'react-router-dom'; import { Battery } from 'react-bootstrap-icons'; import { toast, ToastContainer } from 'react-toastify'; @@ -10,7 +10,7 @@ import { distanceTo } from 'msfs-geo'; import { Tooltip } from './UtilComponents/TooltipWrapper'; import { FbwLogo } from './UtilComponents/FbwLogo'; import { AlertModal, ModalContainer, useModals } from './UtilComponents/Modals/Modals'; -import NavigraphClient, { NavigraphContext } from './Apis/Navigraph/Navigraph'; +import { NavigraphContext } from './Apis/Navigraph/Navigraph'; import { StatusBar } from './StatusBar/StatusBar'; import { ToolBar } from './ToolBar/ToolBar'; import { Dashboard } from './Dashboard/Dashboard'; @@ -84,7 +84,7 @@ const Efb = () => { const dispatch = useAppDispatch(); const simbriefData = useAppSelector((state) => state.simbrief.data); - const [simbriefUserId] = usePersistentProperty('CONFIG_SIMBRIEF_USERID'); + const [navigraphUsername] = usePersistentProperty('NAVIGRAPH_USERNAME'); const [autoSimbriefImport] = usePersistentProperty('CONFIG_AUTO_SIMBRIEF_IMPORT'); const [dc2BusIsPowered] = useSimVar('L:A32NX_ELEC_DC_2_BUS_IS_POWERED', 'bool'); @@ -197,7 +197,7 @@ const Efb = () => { } if ((!simbriefData || !isSimbriefDataLoaded()) && autoSimbriefImport === 'ENABLED') { - fetchSimbriefDataAction(simbriefUserId ?? '').then((action) => { + fetchSimbriefDataAction(navigraphUsername ?? '').then((action) => { dispatch(action); }).catch((e) => { toast.error(e.message); diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartUI.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartUI.tsx index a49f98d0604c..06a5c21469f0 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartUI.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphChartUI.tsx @@ -1,5 +1,11 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + import React, { useEffect, 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 '../../../translation'; import { NavigraphChartSelector, OrganizedChart } from './NavigraphChartSelector'; import { NavigationTab, editTabProperty } from '../../../Store/features/navigationPage'; @@ -8,7 +14,6 @@ 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 { useNavigraph, emptyNavigraphCharts, NavigraphAirportCharts } from '../../../Apis/Navigraph/Navigraph'; import { ChartViewer } from '../../Navigation'; export const NavigraphChartUI = () => { diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphPage.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphPage.tsx index e33dcf50874a..8684127dfd12 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphPage.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Navigation/Pages/NavigraphPage/NavigraphPage.tsx @@ -1,9 +1,13 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + import React from 'react'; import { NavigraphAuthUIWrapper } from '../../../Apis/Navigraph/Components/Authentication'; import { NavigraphChartUI } from './NavigraphChartUI'; export const NavigraphPage = () => ( - + ); diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Pages/AtsuAocPage.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Pages/AtsuAocPage.tsx index 9e85318f6683..a1e65e05a83e 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Pages/AtsuAocPage.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Pages/AtsuAocPage.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: GPL-3.0 /* eslint-disable max-len */ -import React, { useState } from 'react'; +import React from 'react'; import { usePersistentProperty } from '@flybywiresim/fbw-sdk'; @@ -24,9 +24,6 @@ export const AtsuAocPage = () => { const [tafSource, setTafSource] = usePersistentProperty('CONFIG_TAF_SRC', 'NOAA'); const [telexEnabled, setTelexEnabled] = usePersistentProperty('CONFIG_ONLINE_FEATURES_STATUS', 'DISABLED'); - const [simbriefUserId, setSimbriefUserId] = usePersistentProperty('CONFIG_SIMBRIEF_USERID'); - const [simbriefDisplay, setSimbriefDisplay] = useState(simbriefUserId); - const [autoSimbriefImport, setAutoSimbriefImport] = usePersistentProperty('CONFIG_AUTO_SIMBRIEF_IMPORT', 'DISABLED'); const [hoppieEnabled, setHoppieEnabled] = usePersistentProperty('CONFIG_HOPPIE_ENABLED', 'DISABLED'); @@ -34,59 +31,6 @@ export const AtsuAocPage = () => { const [sentryEnabled, setSentryEnabled] = usePersistentProperty(SENTRY_CONSENT_KEY, SentryConsentState.Refused); - const getSimbriefUserData = (value: string): Promise => { - const SIMBRIEF_URL = 'http://www.simbrief.com/api/xml.fetcher.php?json=1'; - - if (!value) { - throw new Error('No SimBrief username/pilot ID provided'); - } - - // The SimBrief API will try both username and pilot ID if either one - // isn't valid, so request both if the input is plausibly a pilot ID. - let apiUrl = `${SIMBRIEF_URL}&username=${value}`; - if (/^\d{1,8}$/.test(value)) { - apiUrl += `&userid=${value}`; - } - - return fetch(apiUrl) - .then((response) => { - // 400 status means request was invalid, probably invalid username so preserve to display error properly - if (!response.ok && response.status !== 400) { - throw new Error(`Error when making fetch request to SimBrief API. Response status code: ${response.status}`); - } - - return response.json(); - }); - }; - - const getSimbriefUserId = (value: string):Promise => new Promise((resolve, reject) => { - if (!value) { - reject(new Error('No SimBrief username/pilot ID provided')); - } - getSimbriefUserData(value) - .then((data) => { - if (data.fetch.status === 'Error: Unknown UserID') { - reject(new Error('Error: Unknown UserID')); - } - resolve(data.fetch.userid); - }) - .catch((_error) => { - reject(_error); - }); - }); - - const handleUsernameInput = (value: string) => { - getSimbriefUserId(value).then((response) => { - toast.success(`${t('Settings.AtsuAoc.YourSimBriefPilotIdHasBeenValidatedAndUpdatedTo')} ${response}`); - - setSimbriefUserId(response); - setSimbriefDisplay(response); - }).catch(() => { - setSimbriefDisplay(simbriefUserId); - toast.error(t('Settings.AtsuAoc.PleaseCheckThatYouHaveCorrectlyEnteredYourSimbBriefUsernameOrPilotId')); - }); - }; - const getHoppieResponse = (value: string): Promise => new Promise((resolve, reject) => { if (!value || value === '') { resolve(value); @@ -270,15 +214,6 @@ export const AtsuAocPage = () => { onChange={(value) => setHoppieUserId(value)} /> - - - handleUsernameInput(value.replace(/\s/g, ''))} - onChange={(value) => setSimbriefDisplay(value)} - /> - ); }; diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Pages/ThirdPartyOptionsPage.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Pages/ThirdPartyOptionsPage.tsx index 4243a4c13352..644727ceec1d 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Pages/ThirdPartyOptionsPage.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Pages/ThirdPartyOptionsPage.tsx @@ -3,12 +3,20 @@ // SPDX-License-Identifier: GPL-3.0 import React, { useEffect } from 'react'; -import { usePersistentNumberProperty } from '@flybywiresim/fbw-sdk'; +import { NavigraphSubscriptionStatus, usePersistentNumberProperty } from '@flybywiresim/fbw-sdk'; +import { Route, Switch, useHistory } from 'react-router-dom'; import { Toggle } from '../../UtilComponents/Form/Toggle'; -import { SettingItem, SettingsPage } from '../Settings'; +import { FullscreenSettingsPage, SettingItem, SettingsPage } from '../Settings'; import { t } from '../../translation'; +import { NavigraphAuthUIWrapper, useNavigraphAuthInfo } from '../../Apis/Navigraph/Components/Authentication'; +import { useNavigraph } from '../../Apis/Navigraph/Navigraph'; +import { TooltipWrapper } from '../../UtilComponents/TooltipWrapper'; export const ThirdPartyOptionsPage = () => { + const history = useHistory(); + const navigraph = useNavigraph(); + const navigraphAuthInfo = useNavigraphAuthInfo(); + const [gsxFuelSyncEnabled, setGsxFuelSyncEnabled] = usePersistentNumberProperty('GSX_FUEL_SYNC', 0); const [gsxPayloadSyncEnabled, setGsxPayloadSyncEnabled] = usePersistentNumberProperty('GSX_PAYLOAD_SYNC', 0); const [, setWheelChocksEnabled] = usePersistentNumberProperty('MODEL_WHEELCHOCKS_ENABLED', 1); @@ -21,26 +29,77 @@ export const ThirdPartyOptionsPage = () => { } }, [gsxFuelSyncEnabled, gsxPayloadSyncEnabled]); + const handleNavigraphAccountSuccessfulLink = () => { + history.push('/settings/3rd-party-options'); + }; + + const handleNavigraphAccountUnlink = () => { + navigraph.deAuthenticate(); + }; + return ( - <> - - - { - setGsxFuelSyncEnabled(value ? 1 : 0); - }} - /> - - - { - setGsxPayloadSyncEnabled(value ? 1 : 0); - }} - /> - - - + + + + + + + + + + + {navigraphAuthInfo.loggedIn ? ( + <> + + {navigraphAuthInfo.username} + {' - '} + {t(`Settings.ThirdPartyOptions.NavigraphAccountLink.SubscriptionStatus.${NavigraphSubscriptionStatus[navigraphAuthInfo.subscriptionStatus]}`)} + + + + + ) : ( + + )} + + + + + + + + { + setGsxFuelSyncEnabled(value ? 1 : 0); + }} + /> + + + + { + setGsxPayloadSyncEnabled(value ? 1 : 0); + }} + /> + + + + ); }; diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Settings.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Settings.tsx index d662797564db..83e5d67da09e 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Settings.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Settings/Settings.tsx @@ -1,3 +1,7 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + import React, { FC } from 'react'; import { Route, Switch } from 'react-router'; @@ -84,7 +88,7 @@ export const SettingsPage: FC = ({ name, children }) => (
- +
{children}
@@ -93,6 +97,24 @@ export const SettingsPage: FC = ({ name, children }) => (
); +export const FullscreenSettingsPage: FC = ({ name, children }) => ( +
+ +
+ +

+ {t('Settings.Title')} + {' - '} + {name} +

+
+ +
+ {children} +
+
+); + // SettingsGroup wraps several SettingsItems into a group (no divider and closer together).
// The parent SettingItem should have groupType="parent", any dependent setting should have groupType="sub". export const SettingGroup: FC = ({ children }) => ( diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/Store/features/simBrief.ts b/fbw-a32nx/src/systems/instruments/src/EFB/Store/features/simBrief.ts index a32abfee5f0c..637b383c20c3 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/Store/features/simBrief.ts +++ b/fbw-a32nx/src/systems/instruments/src/EFB/Store/features/simBrief.ts @@ -1,3 +1,7 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { getSimbriefData } from '../../Apis/Simbrief'; import { IFuel, IWeights } from '../../Apis/Simbrief/simbriefInterface'; @@ -127,10 +131,10 @@ export const simbriefSlice = createSlice({ }, }); -export async function fetchSimbriefDataAction(simbriefUserId: string): Promise> { - const returnedSimbriefData = await getSimbriefData(simbriefUserId); +export async function fetchSimbriefDataAction(naivgraphUsername: string): Promise> { + const returnedSimbriefData = await getSimbriefData(naivgraphUsername); - if (simbriefUserId) { + if (naivgraphUsername) { return setSimbriefData({ airline: returnedSimbriefData.airline, flightNum: returnedSimbriefData.flightNumber, diff --git a/fbw-a32nx/src/systems/systems-host/build.js b/fbw-a32nx/src/systems/systems-host/build.js index 1b3d8c94b914..ca44006c5d17 100644 --- a/fbw-a32nx/src/systems/systems-host/build.js +++ b/fbw-a32nx/src/systems/systems-host/build.js @@ -15,7 +15,7 @@ const isProductionBuild = process.env.A32NX_PRODUCTION_BUILD === '1'; esbuild.build({ absWorkingDir: __dirname, - define: { DEBUG: 'false' }, + define: { 'DEBUG': 'false', 'process.env.CLIENT_ID': `'${process.env.CLIENT_ID}'`, 'process.env.CLIENT_SECRET': `'${process.env.CLIENT_SECRET}'` }, entryPoints: ['./index.ts'], bundle: true, diff --git a/fbw-common/src/systems/shared/src/index.ts b/fbw-common/src/systems/shared/src/index.ts index 25a38326581c..38f685102986 100644 --- a/fbw-common/src/systems/shared/src/index.ts +++ b/fbw-common/src/systems/shared/src/index.ts @@ -1,3 +1,4 @@ +export * from './navigraph'; export * from './ApproachUtils'; export * from './arinc429'; export * from './ata'; diff --git a/fbw-common/src/systems/shared/src/navigraph/client.ts b/fbw-common/src/systems/shared/src/navigraph/client.ts new file mode 100644 index 000000000000..c7e78649c4ff --- /dev/null +++ b/fbw-common/src/systems/shared/src/navigraph/client.ts @@ -0,0 +1,343 @@ +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 { + 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) { + 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; + } + + 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 subscriptionStatus(): 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; + } +} diff --git a/fbw-common/src/systems/shared/src/navigraph/index.ts b/fbw-common/src/systems/shared/src/navigraph/index.ts new file mode 100644 index 000000000000..d860d0670dbf --- /dev/null +++ b/fbw-common/src/systems/shared/src/navigraph/index.ts @@ -0,0 +1,2 @@ +export * from './client'; +export * from './types'; diff --git a/fbw-common/src/systems/shared/src/navigraph/types.ts b/fbw-common/src/systems/shared/src/navigraph/types.ts new file mode 100644 index 000000000000..721f66ba7018 --- /dev/null +++ b/fbw-common/src/systems/shared/src/navigraph/types.ts @@ -0,0 +1,56 @@ +export enum NavigraphSubscriptionStatus { + None, + Unlimited, +} + +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 interface NavigraphAirportCharts { + arrival: NavigraphChart[], + approach: NavigraphChart[], + airport: NavigraphChart[], + departure: NavigraphChart[], + reference: NavigraphChart[], +} + +export interface AirportInfo { + name: string, +} + +export interface AuthType { + code: string, + link: string, + qrLink: string, + interval: number, + disabled: boolean, +}