-
Notifications
You must be signed in to change notification settings - Fork 138
/
_app.tsx
159 lines (141 loc) · 5.55 KB
/
_app.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { useAuth0 } from "@auth0/auth0-react";
import { Store } from "@reduxjs/toolkit";
import type { AppContext, AppProps } from "next/app";
import NextApp from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
import React, { ReactNode, memo, useEffect, useState } from "react";
import { Provider } from "react-redux";
import "../src/global-css";
import "../src/test-prep";
import { SystemProvider } from "design";
import { recordData as recordTelemetryData } from "replay-next/src/utils/telemetry";
import { replayClient } from "shared/client/ReplayClientContext";
import { ApolloWrapper } from "ui/components/ApolloWrapper";
import _App from "ui/components/App";
import { RootErrorBoundary } from "ui/components/Errors/RootErrorBoundary";
import MaintenanceModeScreen from "ui/components/MaintenanceMode";
import { ConfirmProvider } from "ui/components/shared/Confirm";
import LoadingScreen from "ui/components/shared/LoadingScreen";
import useAuthTelemetry from "ui/hooks/useAuthTelemetry";
import { bootstrapApp } from "ui/setup";
import { useLaunchDarkly } from "ui/utils/launchdarkly";
import { InstallRouteListener } from "ui/utils/routeListener";
import tokenManager from "ui/utils/tokenManager";
import "../src/base.css";
interface AuthProps {
apiKey?: string;
}
// We need to ensure that we always pass the same handleAuthError function
// to ApolloWrapper, otherwise it will create a new apolloClient every time
// and parts of the UI will reset (https://github.com/RecordReplay/devtools/issues/6168).
// But handleAuthError needs access to the current values from useAuth0(),
// so we use a constant wrapper around the _handleAuthError() function that
// will be recreated with the current values.
let _handleAuthError: () => Promise<void>;
function handleAuthError() {
_handleAuthError?.();
}
function AppUtilities({ children }: { children: ReactNode }) {
const router = useRouter();
const { getAccessTokenSilently, isAuthenticated } = useAuth0();
_handleAuthError = async () => {
// This handler attempts to handle the scenario in which the frontend and
// our auth client think the user has a valid auth session but the backend
// disagrees. In this case, we should refresh the token so we can continue
// or, if that fails, return to the login page so the user can resume.
if (!isAuthenticated || router.pathname.startsWith("/login")) {
return;
}
try {
recordTelemetryData("devtools-auth-error-refresh");
await getAccessTokenSilently({ ignoreCache: true });
} catch {
recordTelemetryData("devtools-auth-error-refresh-fail");
const returnToPath = window.location.pathname + window.location.search;
router.push({ pathname: "/login", query: { returnTo: returnToPath } });
}
};
return (
<ApolloWrapper onAuthError={handleAuthError}>
<ConfirmProvider>{children}</ConfirmProvider>
</ApolloWrapper>
);
}
function Routing({ Component, pageProps }: AppProps) {
const [store, setStore] = useState<Store | null>(null);
const { getFeatureFlag } = useLaunchDarkly();
useEffect(() => {
bootstrapApp().then((store: Store) => setStore(store));
}, []);
if (!store) {
return null;
}
if (getFeatureFlag("maintenance-mode")) {
return <MaintenanceModeScreen />;
}
return (
<Provider store={store}>
<MemoizedHeader />
<RootErrorBoundary replayClient={replayClient}>
<_App>
<InstallRouteListener />
<React.Suspense fallback={<LoadingScreen message="Fetching data..." />}>
<Component {...pageProps} />
</React.Suspense>
</_App>
</RootErrorBoundary>
</Provider>
);
}
// Ensure this component only renders once even if the parent component re-renders
// Otherwise it can temporarily override titles set by child components,
// causing the document title to flicker.
const MemoizedHeader = memo(function MemoizedHeader() {
return (
<Head>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<title>Replay</title>
</Head>
);
});
const App = ({ apiKey, ...props }: AppProps & AuthProps) => {
useAuthTelemetry();
const router = useRouter();
let head: React.ReactNode;
// HACK: Coordinates with the recording page to render its <head> contents for
// social meta tags. This can be removed once we are able to handle SSP
// properly all the way to the pages. __N_SSP is a very private
// (https://github.com/vercel/next.js/discussions/12558) Next.js prop to
// indicate server-side rendering. It works for now but likely will be removed
// or replaced so we need to fix our SSR and stop using it.
if (props.__N_SSP && router.pathname.match(/^\/recording\//)) {
head = <props.Component {...props.pageProps} headOnly />;
}
return (
<SystemProvider>
<tokenManager.Auth0Provider apiKey={apiKey}>
{head}
<AppUtilities>
<Routing {...props} />
</AppUtilities>
</tokenManager.Auth0Provider>
</SystemProvider>
);
};
App.getInitialProps = (appContext: AppContext) => {
const props = NextApp.getInitialProps(appContext);
const authHeader = appContext.ctx.req?.headers.authorization;
const authProps: AuthProps = { apiKey: undefined };
if (authHeader) {
const [scheme, token] = authHeader.split(" ", 2);
if (!token || !/^Bearer$/i.test(scheme)) {
console.error("Format is Authorization: Bearer [token]");
} else {
authProps.apiKey = token;
}
}
return { ...props, ...authProps };
};
export default App;