Skip to content

Commit 4cf353c

Browse files
mkazlauskasmirceahasegan
authored andcommitted
feat: crash screen (#1613)
* feat: add crash screen service worker crashes might cause infinite loader show button to reload extension on: - unhandled errors in service worker - unhandled promise rejections in service worker - any error in BaseWallet observables * chore: sdk versions bump * fixup! chore: sdk versions bump --------- Co-authored-by: Mircea Hasegan <[email protected]>
1 parent 1c99926 commit 4cf353c

File tree

18 files changed

+196
-21
lines changed

18 files changed

+196
-21
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@import '../../../../../packages/common/src/ui/styles/theme.scss';
2+
3+
.crashContainer {
4+
@include flex-center;
5+
flex-direction: column;
6+
height: 100%;
7+
gap: size_unit(2);
8+
9+
.crashText {
10+
color: var(--text-color-primary);
11+
}
12+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
import { useTranslation } from 'react-i18next';
4+
import styles from './Crash.module.scss';
5+
import { Button } from '@input-output-hk/lace-ui-toolkit';
6+
import { useRuntime } from '@hooks/useRuntime';
7+
8+
export const Crash = (): React.ReactElement => {
9+
const { t } = useTranslation();
10+
const runtime = useRuntime();
11+
12+
return (
13+
<div className={classNames([styles.crashContainer])} data-testid="crash">
14+
<p className={styles.crashText} data-testid="crash-text">
15+
{t('general.errors.crash')}
16+
</p>
17+
<Button.CallToAction
18+
onClick={() => runtime.reload()}
19+
label={t('general.errors.reloadExtension')}
20+
data-testid="crash-reload"
21+
/>
22+
</div>
23+
);
24+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Crash';

apps/browser-extension-wallet/src/features/nami-migration/NamiMigration.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { WalletSetupLayout } from '@views/browser/components';
99
import { Portal } from '@views/browser/features/wallet-setup/components/Portal';
1010
import { useAnalyticsContext } from '@providers';
1111
import { postHogNamiMigrationActions } from '@providers/AnalyticsProvider/analyticsTracker';
12+
import { useFatalError } from '@hooks/useFatalError';
13+
import { Crash } from '@components/Crash';
1214

1315
const urlPath = walletRoutePaths.namiMigration;
1416

@@ -23,6 +25,11 @@ export const NamiMigration = (): JSX.Element => {
2325
analytics.sendEventToPostHog(postHogNamiMigrationActions.onboarding.OPEN);
2426
}, [analytics]);
2527

28+
const fatalError = useFatalError();
29+
if (fatalError) {
30+
return <Crash />;
31+
}
32+
2633
return (
2734
<Portal>
2835
<WalletSetupLayout>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { ObservableWallet } from '@cardano-sdk/wallet';
2+
import { useObservable } from '@lace/common';
3+
import { useBackgroundServiceAPIContext } from '@providers';
4+
import { useWalletStore } from '@src/stores';
5+
import { useMemo } from 'react';
6+
import { catchError, take, of, merge, EMPTY } from 'rxjs';
7+
import { toEmpty } from '@cardano-sdk/util-rxjs';
8+
import { getErrorMessage } from '@src/utils/get-error-message';
9+
10+
const anyError = (wallet: ObservableWallet | undefined) =>
11+
wallet
12+
? merge(
13+
wallet.addresses$,
14+
wallet.assetInfo$,
15+
wallet.balance.rewardAccounts.deposit$,
16+
wallet.balance.rewardAccounts.rewards$,
17+
wallet.balance.utxo.available$,
18+
wallet.balance.utxo.total$,
19+
wallet.balance.utxo.unspendable$,
20+
wallet.currentEpoch$,
21+
wallet.delegation.distribution$,
22+
wallet.delegation.portfolio$,
23+
wallet.delegation.rewardAccounts$,
24+
wallet.delegation.rewardsHistory$,
25+
wallet.eraSummaries$,
26+
wallet.genesisParameters$,
27+
wallet.handles$,
28+
wallet.protocolParameters$,
29+
wallet.governance.isRegisteredAsDRep$,
30+
wallet.publicStakeKeys$,
31+
wallet.syncStatus.isAnyRequestPending$,
32+
wallet.syncStatus.isSettled$,
33+
wallet.syncStatus.isUpToDate$,
34+
wallet.tip$,
35+
wallet.transactions.history$,
36+
wallet.transactions.rollback$,
37+
wallet.utxo.available$,
38+
wallet.utxo.total$,
39+
wallet.utxo.unspendable$
40+
).pipe(
41+
toEmpty,
42+
catchError((error) => of({ type: 'base-wallet-error', message: getErrorMessage(error) })),
43+
take(1)
44+
)
45+
: EMPTY;
46+
47+
type FatalError = {
48+
type: string;
49+
message: string;
50+
};
51+
52+
export const useFatalError = (): FatalError | undefined => {
53+
const backgroundService = useBackgroundServiceAPIContext();
54+
const unhandledServiceWorkerError = useObservable(backgroundService.unhandledError$);
55+
const { cardanoWallet } = useWalletStore();
56+
const walletError$ = useMemo(() => anyError(cardanoWallet?.wallet), [cardanoWallet?.wallet]);
57+
const walletError = useObservable(walletError$);
58+
return unhandledServiceWorkerError || walletError;
59+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { runtime } from 'webextension-polyfill';
2+
3+
export type LaceRuntime = { reload: () => void };
4+
5+
export const useRuntime = (): LaceRuntime => ({
6+
reload: runtime.reload
7+
});

apps/browser-extension-wallet/src/hooks/useWalletState.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ type RemoveObservableNameSuffix<T> = T extends `${infer S}$` ? S : T;
2424
type FlattenObservableProperties<T> = T extends Map<any, any> | String | Number | Array<any> | Date | null | BigInt
2525
? T
2626
: T extends object
27-
? {
28-
[k in keyof T as T[k] extends Function ? never : RemoveObservableNameSuffix<k>]: T[k] extends Observable<infer O>
29-
? FlattenObservableProperties<O>
30-
: FlattenObservableProperties<T[k]>;
31-
}
32-
: T;
27+
? {
28+
[k in keyof T as T[k] extends Function ? never : RemoveObservableNameSuffix<k>]: T[k] extends Observable<
29+
infer O
30+
>
31+
? FlattenObservableProperties<O>
32+
: FlattenObservableProperties<T[k]>;
33+
}
34+
: T;
3335
export type ObservableWalletState = FlattenObservableProperties<
34-
Omit<ObservableWallet, 'fatalError$' | 'transactions'> & {
36+
Omit<ObservableWallet, 'transactions'> & {
3537
transactions: {
3638
history$: ObservableWallet['transactions']['history$'];
3739
outgoing: Pick<ObservableWallet['transactions']['outgoing'], 'inFlight$' | 'signed$'>;

apps/browser-extension-wallet/src/lib/scripts/background/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ export const backgroundServiceProperties: RemoteApiProperties<BackgroundService>
2626
getBackgroundStorage: RemoteApiPropertyType.MethodReturningPromise,
2727
setBackgroundStorage: RemoteApiPropertyType.MethodReturningPromise,
2828
resetStorage: RemoteApiPropertyType.MethodReturningPromise,
29-
backendFailures$: RemoteApiPropertyType.HotObservable
29+
backendFailures$: RemoteApiPropertyType.HotObservable,
30+
unhandledError$: RemoteApiPropertyType.HotObservable
3031
};
3132

3233
const { BLOCKFROST_CONFIGS, BLOCKFROST_RATE_LIMIT_CONFIG } = config();

apps/browser-extension-wallet/src/lib/scripts/background/services/utilityServices.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
TokenPrices,
1313
CoinPrices,
1414
ChangeModeData,
15-
LaceFeaturesApi
15+
LaceFeaturesApi,
16+
UnhandledError
1617
} from '../../types';
17-
import { Subject, of, BehaviorSubject } from 'rxjs';
18+
import { Subject, of, BehaviorSubject, merge, map, fromEvent } from 'rxjs';
1819
import { walletRoutePaths } from '@routes/wallet-paths';
1920
import { backgroundServiceProperties } from '../config';
2021
import { exposeApi } from '@cardano-sdk/web-extension';
@@ -24,6 +25,7 @@ import { getADAPriceFromBackgroundStorage, closeAllLaceWindows } from '../util';
2425
import { currencies as currenciesMap, currencyCode } from '@providers/currency/constants';
2526
import { clearBackgroundStorage, getBackgroundStorage, setBackgroundStorage } from '../storage';
2627
import { laceFeaturesApiProperties, LACE_FEATURES_CHANNEL } from '../injectUtil';
28+
import { getErrorMessage } from '@src/utils/get-error-message';
2729

2830
export const requestMessage$ = new Subject<Message>();
2931
export const backendFailures$ = new BehaviorSubject(0);
@@ -204,6 +206,17 @@ exposeApi<LaceFeaturesApi>(
204206
{ logger: console, runtime }
205207
);
206208

209+
const toUnhandledError = (error: unknown, type: UnhandledError['type']): UnhandledError => ({
210+
type,
211+
message: getErrorMessage(error)
212+
});
213+
const unhandledError$ = merge(
214+
fromEvent(globalThis, 'error').pipe(map((e: ErrorEvent): UnhandledError => toUnhandledError(e, 'error'))),
215+
fromEvent(globalThis, 'unhandledrejection').pipe(
216+
map((e: PromiseRejectionEvent): UnhandledError => toUnhandledError(e, 'unhandledrejection'))
217+
)
218+
);
219+
207220
exposeApi<BackgroundService>(
208221
{
209222
api$: of({
@@ -222,7 +235,8 @@ exposeApi<BackgroundService>(
222235
await clearBackgroundStorage();
223236
await webStorage.local.set({ MIGRATION_STATE: { state: 'up-to-date' } as MigrationState });
224237
},
225-
backendFailures$
238+
backendFailures$,
239+
unhandledError$
226240
}),
227241
baseChannel: BaseChannels.BACKGROUND_ACTIONS,
228242
properties: backgroundServiceProperties

apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { of, combineLatest, map, EMPTY, BehaviorSubject, Observable, from, first
44
import { getProviders } from './config';
55
import { DEFAULT_POLLING_CONFIG, createPersonalWallet, storage, createSharedWallet } from '@cardano-sdk/wallet';
66
import { handleHttpProvider } from '@cardano-sdk/cardano-services-client';
7+
import { Cardano, HandleProvider } from '@cardano-sdk/core';
78
import {
89
AnyWallet,
910
StoresFactory,
@@ -21,7 +22,6 @@ import {
2122
walletRepositoryProperties
2223
} from '@cardano-sdk/web-extension';
2324
import { Wallet } from '@lace/cardano';
24-
import { Cardano, HandleProvider } from '@cardano-sdk/core';
2525
import { cacheActivatedWalletAddressSubscription } from './cache-wallets-address';
2626
import axiosFetchAdapter from '@shiroyasha9/axios-fetch-adapter';
2727
import { SharedWalletScriptKind } from '@lace/core';

apps/browser-extension-wallet/src/lib/scripts/types/background-service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BehaviorSubject, Subject } from 'rxjs';
1+
import { BehaviorSubject, Subject, Observable } from 'rxjs';
22
import { themes } from '@providers/ThemeProvider';
33
import { BackgroundStorage, MigrationState } from './storage';
44
import { CoinPrices } from './prices';
@@ -89,6 +89,11 @@ export type Message =
8989
| OpenBrowserMessage
9090
| ChangeMode;
9191

92+
export type UnhandledError = {
93+
type: 'error' | 'unhandledrejection';
94+
message: string;
95+
};
96+
9297
export type BackgroundService = {
9398
handleOpenBrowser: (data: OpenBrowserData, urlSearchParams?: string) => Promise<void>;
9499
handleOpenPopup: () => Promise<void>;
@@ -103,6 +108,7 @@ export type BackgroundService = {
103108
clearBackgroundStorage: typeof clearBackgroundStorage;
104109
resetStorage: () => Promise<void>;
105110
backendFailures$: BehaviorSubject<number>;
111+
unhandledError$: Observable<UnhandledError>;
106112
};
107113

108114
export type WalletMode = {

apps/browser-extension-wallet/src/routes/DappConnectorView.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { tabs } from 'webextension-polyfill';
2525
import { useTranslation } from 'react-i18next';
2626
import { DappSignDataSuccess } from '@src/features/dapp/components/DappSignDataSuccess';
2727
import { DappSignDataFail } from '@src/features/dapp/components/DappSignDataFail';
28+
import { Crash } from '@components/Crash';
29+
import { useFatalError } from '@hooks/useFatalError';
2830

2931
dayjs.extend(duration);
3032

@@ -57,17 +59,22 @@ export const DappConnectorView = (): React.ReactElement => {
5759
}, [isWalletLocked, cardanoWallet]);
5860

5961
const isLoading = useMemo(() => hdDiscoveryStatus !== 'Idle', [hdDiscoveryStatus]);
62+
const fatalError = useFatalError();
6063
useEffect(() => {
61-
if (!isLoading) {
64+
if (!isLoading || fatalError) {
6265
document.querySelector('#preloader')?.remove();
6366
}
64-
}, [isLoading]);
67+
}, [isLoading, fatalError]);
6568

6669
const onCloseClick = useCallback(() => {
6770
tabs.create({ url: `app.html#${walletRoutePaths.setup.home}` });
6871
window.close();
6972
}, []);
7073

74+
if (fatalError) {
75+
return <Crash />;
76+
}
77+
7178
if (hasNoAvailableWallet) {
7279
return (
7380
<MainLayout useSimpleHeader hideFooter showAnnouncement={false}>

apps/browser-extension-wallet/src/routes/PopupView.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { getValueFromLocalStorage } from '@src/utils/local-storage';
1212
import { MainLoader } from '@components/MainLoader';
1313
import { useAppInit } from '@hooks';
1414
import { ILocalStorage } from '@src/types';
15+
import { useFatalError } from '@hooks/useFatalError';
16+
import { Crash } from '@components/Crash';
1517

1618
dayjs.extend(duration);
1719

@@ -55,19 +57,24 @@ export const PopupView = (): React.ReactElement => {
5557
// (see useEffect in browser-view routes index)
5658
}, [isWalletLocked, backgroundServices, currentChain, chainName, cardanoWallet]);
5759

60+
const fatalError = useFatalError();
5861
const isLoaded = useMemo(
5962
() => !!cardanoWallet && walletInfo && walletState && inMemoryWallet && initialHdDiscoveryCompleted,
6063
[cardanoWallet, walletInfo, walletState, inMemoryWallet, initialHdDiscoveryCompleted]
6164
);
6265
useEffect(() => {
63-
if (isLoaded) {
66+
if (isLoaded || fatalError) {
6467
document.querySelector('#preloader')?.remove();
6568
}
66-
}, [isLoaded]);
69+
}, [isLoaded, fatalError]);
6770

6871
const checkMnemonicVerificationFrequency = () =>
6972
mnemonicVerificationFrequency && isLastValidationExpired(lastMnemonicVerification, mnemonicVerificationFrequency);
7073

74+
if (fatalError) {
75+
return <Crash />;
76+
}
77+
7178
if (checkMnemonicVerificationFrequency() && walletLock) {
7279
return <UnlockWalletContainer validateMnemonic />;
7380
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const getErrorMessage = (error: unknown): string =>
2+
error && typeof error.toString === 'function' ? error.toString() : '';

apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import { BackgroundStorage, Message, MessageTypes } from '@lib/scripts/types';
3636
import { getBackgroundStorage } from '@lib/scripts/background/storage';
3737
import { useTranslation } from 'react-i18next';
3838
import { POPUP_WINDOW_NAMI_TITLE } from '@src/utils/constants';
39+
import { useFatalError } from '@hooks/useFatalError';
40+
import { Crash } from '@components/Crash';
3941

4042
export const defaultRoutes: RouteMap = [
4143
{
@@ -211,11 +213,17 @@ export const BrowserViewRoutes = ({ routesMap = defaultRoutes }: { routesMap?: R
211213
[cardanoWallet, isLoadingWalletInfo, namiMigration?.mode]
212214
);
213215

216+
const fatalError = useFatalError();
217+
214218
useEffect(() => {
215-
if (isLoaded || isOnboarding || isInNamiMode) {
219+
if (isLoaded || isOnboarding || isInNamiMode || fatalError) {
216220
document.querySelector('#preloader')?.remove();
217221
}
218-
}, [isLoaded, isOnboarding, isInNamiMode]);
222+
}, [isLoaded, isOnboarding, isInNamiMode, fatalError]);
223+
224+
if (fatalError) {
225+
return <Crash />;
226+
}
219227

220228
if (isInNamiMode) {
221229
return (

apps/browser-extension-wallet/src/views/nami-mode/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import '../../lib/scripts/keep-alive-ui';
88
import './index.scss';
99
import { useBackgroundServiceAPIContext } from '@providers';
1010
import { BrowserViewSections } from '@lib/scripts/types';
11+
import { Crash } from '@components/Crash';
12+
import { useFatalError } from '@hooks/useFatalError';
1113

1214
export const NamiPopup = withDappContext((): React.ReactElement => {
1315
const {
@@ -24,11 +26,13 @@ export const NamiPopup = withDappContext((): React.ReactElement => {
2426
() => !!cardanoWallet && walletInfo && walletState && inMemoryWallet && initialHdDiscoveryCompleted && currentChain,
2527
[cardanoWallet, walletInfo, walletState, inMemoryWallet, initialHdDiscoveryCompleted, currentChain]
2628
);
29+
30+
const fatalError = useFatalError();
2731
useEffect(() => {
28-
if (isLoaded) {
32+
if (isLoaded || fatalError) {
2933
document.querySelector('#preloader')?.remove();
3034
}
31-
}, [isLoaded]);
35+
}, [isLoaded, fatalError]);
3236

3337
useAppInit();
3438

@@ -38,5 +42,9 @@ export const NamiPopup = withDappContext((): React.ReactElement => {
3842
}
3943
}, [backgroundServices, cardanoWallet, deletingWallet]);
4044

45+
if (fatalError) {
46+
return <Crash />;
47+
}
48+
4149
return <div id="nami-mode">{isLoaded ? <NamiView /> : <MainLoader />}</div>;
4250
});

0 commit comments

Comments
 (0)