import React, { Component } from 'react'; import { Encoder as QREncoder, ErrorCorrectionLevel as QRErrorCorrectionLevel } from '@nuintun/qrcode'; import { OAuthDeviceAuthorizationResponse, OAuthSlowDownError, OAuthTokenResponse } from '../Models/OAuth'; import Rooftop from '../Components/Rooftop'; import ConnectCodes from '../Components/ConnectCodes'; import { AuthContext } from '../Context/AuthContext'; import { accessToken, deviceAuthorization } from '../Services/authService'; import { AxiosError } from 'axios'; export type ConnectProps = Record; export interface ConnectState { timeout?: number; refresh: boolean; deviceCode?: string; userCode?: string; verificationURI?: string; verificationURIComplete?: string; interval?: number; } class Connect extends Component { fetchTimeout: NodeJS.Timeout | null = null; noConnectionTimeout: NodeJS.Timeout | null = null; deviceCodeExpiredTimeout: NodeJS.Timeout | null = null; state: ConnectState = { refresh: true, }; static contextType = AuthContext; declare context: React.ContextType; public componentDidMount = (): void => { this.getDeviceCode(); }; public componentDidUpdate = (_prevProps: ConnectProps, prevState: ConnectState): void => { if (prevState.deviceCode && !this.state.deviceCode) { this.getDeviceCode(); } }; public componentWillUnmount = (): void => { if (this.fetchTimeout) { clearTimeout(this.fetchTimeout); this.fetchTimeout = null; } if (this.noConnectionTimeout) { clearTimeout(this.noConnectionTimeout); this.noConnectionTimeout = null; } if (this.deviceCodeExpiredTimeout) { clearTimeout(this.deviceCodeExpiredTimeout); this.deviceCodeExpiredTimeout = null; } }; private getDeviceCode = async (): Promise => { try { const deviceCodeAuthResponse = await deviceAuthorization(); this.handleDeviceCode(deviceCodeAuthResponse); } catch { this.setState({ timeout: 60, refresh: false }, this.handleNoConnectionTimeout); } }; private getAccessToken = async (): Promise => { const { deviceCode } = this.state; if (!deviceCode) { this.handleDeviceCodeExpired(); return; } try { const response = await accessToken(deviceCode); this.handleAccessToken(response, deviceCode); } catch (error) { if (!(error as AxiosError).response) { this.handleDeviceCodeExpired(); return; } if ((error as AxiosError).response?.data.error === 'authorization_pending') { this.setPollingTimeout(); return; } if ((error as AxiosError).response?.data.error === 'slow_down') { this.fetchTimeout && clearTimeout(this.fetchTimeout); this.fetchTimeout = null; this.setState((prevState: ConnectState) => ({ interval: ((error as AxiosError).response?.data as OAuthSlowDownError).slow_down || prevState.interval }), this.setPollingTimeout); return; } this.handleDeviceCodeExpired(); } }; private handleDeviceCodeExpired = () => { this.fetchTimeout && clearTimeout(this.fetchTimeout); this.fetchTimeout = null; this.setState({ deviceCode: undefined, userCode: undefined, verificationURI: undefined, verificationURIComplete: undefined, interval: undefined, refresh: true, }); }; private handleDeviceCode = (response: OAuthDeviceAuthorizationResponse): void => { this.setState({ deviceCode: response.device_code, userCode: response.user_code, verificationURI: response.verification_uri, verificationURIComplete: response.verification_uri_complete, interval: response.interval }, () => { this.getAccessToken(); if (this.deviceCodeExpiredTimeout) { clearTimeout(this.deviceCodeExpiredTimeout); this.deviceCodeExpiredTimeout = null; } this.deviceCodeExpiredTimeout = setTimeout(this.handleDeviceCodeExpired, response.expires_in * 1000); }); }; private handleAccessToken = (response: OAuthTokenResponse, deviceCode: string) => { const { setAuth } = this.context; setAuth && setAuth({ deviceCode, accessToken: response.access_token, refreshToken: response.refresh_token, }); }; private handleNoConnectionTimeout = () => { if (this.noConnectionTimeout) { clearTimeout(this.noConnectionTimeout); this.noConnectionTimeout = null; } this.noConnectionTimeout = setTimeout(() => { this.setState((prevState) => { if (typeof prevState.timeout === 'number') { return { ...prevState, timeout: prevState.timeout - 1 }; } return prevState; }, () => { if (this.state.timeout === 0) { this.getDeviceCode(); } else { this.handleNoConnectionTimeout(); } }); }, 1000); }; private setPollingTimeout() { this.fetchTimeout = setTimeout(this.getAccessToken, (this.state.interval || 5) * 1000); } public render(): JSX.Element { const { timeout, refresh, deviceCode, userCode, verificationURI, verificationURIComplete } = this.state; if (!deviceCode || !userCode || !verificationURI) { if (refresh) { return ( <> ); } return (
{ timeout ? 'Kan geen verbinding maken met de server' : ( timeout === 0 ? 'Opnieuw proberen...' : 'Bezig met laden...' ) }
{ timeout ? (
Opnieuw proberen over { timeout } seconde{ timeout === 1 ? '' : 'n' }...
) : undefined }
); } let qrcode; if (verificationURIComplete) { qrcode = new QREncoder() .setEncodingHint(true) .setErrorCorrectionLevel(QRErrorCorrectionLevel.H) .write(verificationURIComplete) .make(); } return ( <> ); } } export default Connect;