Skip to content

Commit

Permalink
feat: add crash screen
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mkazlauskas committed Dec 20, 2024
1 parent 6258a10 commit 20e57a0
Show file tree
Hide file tree
Showing 18 changed files with 196 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import '../../../../../packages/common/src/ui/styles/theme.scss';

.crashContainer {
@include flex-center;
flex-direction: column;
height: 100%;
gap: size_unit(2);

.crashText {
color: var(--text-color-primary);
}
}
24 changes: 24 additions & 0 deletions apps/browser-extension-wallet/src/components/Crash/Crash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import styles from './Crash.module.scss';
import { Button } from '@input-output-hk/lace-ui-toolkit';
import { useRuntime } from '@hooks/useRuntime';

export const Crash = (): React.ReactElement => {
const { t } = useTranslation();
const runtime = useRuntime();

return (
<div className={classNames([styles.crashContainer])} data-testid="crash">
<p className={styles.crashText} data-testid="crash-text">
{t('general.errors.crash')}
</p>
<Button.CallToAction
onClick={() => runtime.reload()}
label={t('general.errors.reloadExtension')}
data-testid="crash-reload"
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Crash';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { WalletSetupLayout } from '@views/browser/components';
import { Portal } from '@views/browser/features/wallet-setup/components/Portal';
import { useAnalyticsContext } from '@providers';
import { postHogNamiMigrationActions } from '@providers/AnalyticsProvider/analyticsTracker';
import { useFatalError } from '@hooks/useFatalError';
import { Crash } from '@components/Crash';

const urlPath = walletRoutePaths.namiMigration;

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

const fatalError = useFatalError();
if (fatalError) {
return <Crash />;
}

return (
<Portal>
<WalletSetupLayout>
Expand Down
59 changes: 59 additions & 0 deletions apps/browser-extension-wallet/src/hooks/useFatalError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ObservableWallet } from '@cardano-sdk/wallet';
import { useObservable } from '@lace/common';
import { useBackgroundServiceAPIContext } from '@providers';
import { useWalletStore } from '@src/stores';
import { useMemo } from 'react';
import { catchError, take, of, merge, EMPTY } from 'rxjs';
import { toEmpty } from '@cardano-sdk/util-rxjs';
import { getErrorMessage } from '@src/utils/get-error-message';

const anyError = (wallet: ObservableWallet | undefined) =>
wallet
? merge(
wallet.addresses$,
wallet.assetInfo$,
wallet.balance.rewardAccounts.deposit$,
wallet.balance.rewardAccounts.rewards$,
wallet.balance.utxo.available$,
wallet.balance.utxo.total$,
wallet.balance.utxo.unspendable$,
wallet.currentEpoch$,
wallet.delegation.distribution$,
wallet.delegation.portfolio$,
wallet.delegation.rewardAccounts$,
wallet.delegation.rewardsHistory$,
wallet.eraSummaries$,
wallet.genesisParameters$,
wallet.handles$,
wallet.protocolParameters$,
wallet.governance.isRegisteredAsDRep$,
wallet.publicStakeKeys$,
wallet.syncStatus.isAnyRequestPending$,
wallet.syncStatus.isSettled$,
wallet.syncStatus.isUpToDate$,
wallet.tip$,
wallet.transactions.history$,
wallet.transactions.rollback$,
wallet.utxo.available$,
wallet.utxo.total$,
wallet.utxo.unspendable$
).pipe(
toEmpty,
catchError((error) => of({ type: 'base-wallet-error', message: getErrorMessage(error) })),
take(1)
)
: EMPTY;

type FatalError = {
type: string;
message: string;
};

export const useFatalError = (): FatalError | undefined => {
const backgroundService = useBackgroundServiceAPIContext();
const unhandledServiceWorkerError = useObservable(backgroundService.unhandledError$);
const { cardanoWallet } = useWalletStore();
const walletError$ = useMemo(() => anyError(cardanoWallet?.wallet), [cardanoWallet?.wallet]);
const walletError = useObservable(walletError$);
return unhandledServiceWorkerError || walletError;
};
7 changes: 7 additions & 0 deletions apps/browser-extension-wallet/src/hooks/useRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { runtime } from 'webextension-polyfill';

export type LaceRuntime = { reload: () => void };

export const useRuntime = (): LaceRuntime => ({
reload: runtime.reload
});
16 changes: 9 additions & 7 deletions apps/browser-extension-wallet/src/hooks/useWalletState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ type RemoveObservableNameSuffix<T> = T extends `${infer S}$` ? S : T;
type FlattenObservableProperties<T> = T extends Map<any, any> | String | Number | Array<any> | Date | null | BigInt
? T
: T extends object
? {
[k in keyof T as T[k] extends Function ? never : RemoveObservableNameSuffix<k>]: T[k] extends Observable<infer O>
? FlattenObservableProperties<O>
: FlattenObservableProperties<T[k]>;
}
: T;
? {

Check failure on line 27 in apps/browser-extension-wallet/src/hooks/useWalletState.ts

View workflow job for this annotation

GitHub Actions / Release package

Delete `··`
[k in keyof T as T[k] extends Function ? never : RemoveObservableNameSuffix<k>]: T[k] extends Observable<

Check failure on line 28 in apps/browser-extension-wallet/src/hooks/useWalletState.ts

View workflow job for this annotation

GitHub Actions / Release package

Replace `········[k·in·keyof·T·as·T[k]·extends·Function·?·never·:·RemoveObservableNameSuffix<k>]:·T[k]·extends·Observable<⏎··········infer·O⏎········` with `······[k·in·keyof·T·as·T[k]·extends·Function·?·never·:·RemoveObservableNameSuffix<k>]:·T[k]·extends·Observable<infer·O`
infer O
>
? FlattenObservableProperties<O>

Check failure on line 31 in apps/browser-extension-wallet/src/hooks/useWalletState.ts

View workflow job for this annotation

GitHub Actions / Release package

Delete `··`
: FlattenObservableProperties<T[k]>;

Check failure on line 32 in apps/browser-extension-wallet/src/hooks/useWalletState.ts

View workflow job for this annotation

GitHub Actions / Release package

Delete `··`
}

Check failure on line 33 in apps/browser-extension-wallet/src/hooks/useWalletState.ts

View workflow job for this annotation

GitHub Actions / Release package

Delete `··`
: T;

Check failure on line 34 in apps/browser-extension-wallet/src/hooks/useWalletState.ts

View workflow job for this annotation

GitHub Actions / Release package

Delete `··`
export type ObservableWalletState = FlattenObservableProperties<
Omit<ObservableWallet, 'fatalError$' | 'transactions'> & {
Omit<ObservableWallet, 'transactions'> & {
transactions: {
history$: ObservableWallet['transactions']['history$'];
outgoing: Pick<ObservableWallet['transactions']['outgoing'], 'inFlight$' | 'signed$'>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export const backgroundServiceProperties: RemoteApiProperties<BackgroundService>
getBackgroundStorage: RemoteApiPropertyType.MethodReturningPromise,
setBackgroundStorage: RemoteApiPropertyType.MethodReturningPromise,
resetStorage: RemoteApiPropertyType.MethodReturningPromise,
backendFailures$: RemoteApiPropertyType.HotObservable
backendFailures$: RemoteApiPropertyType.HotObservable,
unhandledError$: RemoteApiPropertyType.HotObservable
};

const { BLOCKFROST_CONFIGS, BLOCKFROST_RATE_LIMIT_CONFIG } = config();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
TokenPrices,
CoinPrices,
ChangeModeData,
LaceFeaturesApi
LaceFeaturesApi,
UnhandledError
} from '../../types';
import { Subject, of, BehaviorSubject } from 'rxjs';
import { Subject, of, BehaviorSubject, merge, map, fromEvent } from 'rxjs';
import { walletRoutePaths } from '@routes/wallet-paths';
import { backgroundServiceProperties } from '../config';
import { exposeApi } from '@cardano-sdk/web-extension';
Expand All @@ -24,6 +25,7 @@ import { getADAPriceFromBackgroundStorage, closeAllLaceWindows } from '../util';
import { currencies as currenciesMap, currencyCode } from '@providers/currency/constants';
import { clearBackgroundStorage, getBackgroundStorage, setBackgroundStorage } from '../storage';
import { laceFeaturesApiProperties, LACE_FEATURES_CHANNEL } from '../injectUtil';
import { getErrorMessage } from '@src/utils/get-error-message';

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

const toUnhandledError = (error: unknown, type: UnhandledError['type']): UnhandledError => ({
type,
message: getErrorMessage(error)
});
const unhandledError$ = merge(
fromEvent(globalThis, 'error').pipe(map((e: ErrorEvent): UnhandledError => toUnhandledError(e, 'error'))),
fromEvent(globalThis, 'unhandledrejection').pipe(
map((e: PromiseRejectionEvent): UnhandledError => toUnhandledError(e, 'unhandledrejection'))
)
);

exposeApi<BackgroundService>(
{
api$: of({
Expand All @@ -222,7 +235,8 @@ exposeApi<BackgroundService>(
await clearBackgroundStorage();
await webStorage.local.set({ MIGRATION_STATE: { state: 'up-to-date' } as MigrationState });
},
backendFailures$
backendFailures$,
unhandledError$
}),
baseChannel: BaseChannels.BACKGROUND_ACTIONS,
properties: backgroundServiceProperties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
createSharedWallet
} from '@cardano-sdk/wallet';
import { handleHttpProvider } from '@cardano-sdk/cardano-services-client';
import { Cardano, HandleProvider } from '@cardano-sdk/core';
import {
AnyWallet,
StoresFactory,
Expand All @@ -28,7 +29,6 @@ import {
walletRepositoryProperties
} from '@cardano-sdk/web-extension';
import { Wallet } from '@lace/cardano';
import { Cardano, HandleProvider } from '@cardano-sdk/core';
import { cacheActivatedWalletAddressSubscription } from './cache-wallets-address';
import axiosFetchAdapter from '@shiroyasha9/axios-fetch-adapter';
import { SharedWalletScriptKind } from '@lace/core';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BehaviorSubject, Subject } from 'rxjs';
import { BehaviorSubject, Subject, Observable } from 'rxjs';
import { themes } from '@providers/ThemeProvider';
import { BackgroundStorage, MigrationState } from './storage';
import { CoinPrices } from './prices';
Expand Down Expand Up @@ -89,6 +89,11 @@ export type Message =
| OpenBrowserMessage
| ChangeMode;

export type UnhandledError = {
type: 'error' | 'unhandledrejection';
message: string;
};

export type BackgroundService = {
handleOpenBrowser: (data: OpenBrowserData, urlSearchParams?: string) => Promise<void>;
handleOpenPopup: () => Promise<void>;
Expand All @@ -103,6 +108,7 @@ export type BackgroundService = {
clearBackgroundStorage: typeof clearBackgroundStorage;
resetStorage: () => Promise<void>;
backendFailures$: BehaviorSubject<number>;
unhandledError$: Observable<UnhandledError>;
};

export type WalletMode = {
Expand Down
11 changes: 9 additions & 2 deletions apps/browser-extension-wallet/src/routes/DappConnectorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { tabs } from 'webextension-polyfill';
import { useTranslation } from 'react-i18next';
import { DappSignDataSuccess } from '@src/features/dapp/components/DappSignDataSuccess';
import { DappSignDataFail } from '@src/features/dapp/components/DappSignDataFail';
import { Crash } from '@components/Crash';
import { useFatalError } from '@hooks/useFatalError';

dayjs.extend(duration);

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

const isLoading = useMemo(() => hdDiscoveryStatus !== 'Idle', [hdDiscoveryStatus]);
const fatalError = useFatalError();
useEffect(() => {
if (!isLoading) {
if (!isLoading || fatalError) {
document.querySelector('#preloader')?.remove();
}
}, [isLoading]);
}, [isLoading, fatalError]);

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

if (fatalError) {
return <Crash />;
}

if (hasNoAvailableWallet) {
return (
<MainLayout useSimpleHeader hideFooter showAnnouncement={false}>
Expand Down
11 changes: 9 additions & 2 deletions apps/browser-extension-wallet/src/routes/PopupView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { getValueFromLocalStorage } from '@src/utils/local-storage';
import { MainLoader } from '@components/MainLoader';
import { useAppInit } from '@hooks';
import { ILocalStorage } from '@src/types';
import { useFatalError } from '@hooks/useFatalError';
import { Crash } from '@components/Crash';

dayjs.extend(duration);

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

const fatalError = useFatalError();
const isLoaded = useMemo(
() => !!cardanoWallet && walletInfo && walletState && inMemoryWallet && initialHdDiscoveryCompleted,
[cardanoWallet, walletInfo, walletState, inMemoryWallet, initialHdDiscoveryCompleted]
);
useEffect(() => {
if (isLoaded) {
if (isLoaded || fatalError) {
document.querySelector('#preloader')?.remove();
}
}, [isLoaded]);
}, [isLoaded, fatalError]);

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

if (fatalError) {
return <Crash />;
}

if (checkMnemonicVerificationFrequency() && walletLock) {
return <UnlockWalletContainer validateMnemonic />;
}
Expand Down
2 changes: 2 additions & 0 deletions apps/browser-extension-wallet/src/utils/get-error-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const getErrorMessage = (error: unknown): string =>
error && typeof error.toString === 'function' ? error.toString() : '';
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { BackgroundStorage, Message, MessageTypes } from '@lib/scripts/types';
import { getBackgroundStorage } from '@lib/scripts/background/storage';
import { useTranslation } from 'react-i18next';
import { POPUP_WINDOW_NAMI_TITLE } from '@src/utils/constants';
import { useFatalError } from '@hooks/useFatalError';
import { Crash } from '@components/Crash';

export const defaultRoutes: RouteMap = [
{
Expand Down Expand Up @@ -211,11 +213,17 @@ export const BrowserViewRoutes = ({ routesMap = defaultRoutes }: { routesMap?: R
[cardanoWallet, isLoadingWalletInfo, namiMigration?.mode]
);

const fatalError = useFatalError();

useEffect(() => {
if (isLoaded || isOnboarding || isInNamiMode) {
if (isLoaded || isOnboarding || isInNamiMode || fatalError) {
document.querySelector('#preloader')?.remove();
}
}, [isLoaded, isOnboarding, isInNamiMode]);
}, [isLoaded, isOnboarding, isInNamiMode, fatalError]);

if (fatalError) {
return <Crash />;
}

if (isInNamiMode) {
return (
Expand Down
12 changes: 10 additions & 2 deletions apps/browser-extension-wallet/src/views/nami-mode/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import '../../lib/scripts/keep-alive-ui';
import './index.scss';
import { useBackgroundServiceAPIContext } from '@providers';
import { BrowserViewSections } from '@lib/scripts/types';
import { Crash } from '@components/Crash';
import { useFatalError } from '@hooks/useFatalError';

export const NamiPopup = withDappContext((): React.ReactElement => {
const {
Expand All @@ -24,11 +26,13 @@ export const NamiPopup = withDappContext((): React.ReactElement => {
() => !!cardanoWallet && walletInfo && walletState && inMemoryWallet && initialHdDiscoveryCompleted && currentChain,
[cardanoWallet, walletInfo, walletState, inMemoryWallet, initialHdDiscoveryCompleted, currentChain]
);

const fatalError = useFatalError();
useEffect(() => {
if (isLoaded) {
if (isLoaded || fatalError) {
document.querySelector('#preloader')?.remove();
}
}, [isLoaded]);
}, [isLoaded, fatalError]);

useAppInit();

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

if (fatalError) {
return <Crash />;
}

return <div id="nami-mode">{isLoaded ? <NamiView /> : <MainLoader />}</div>;
});
Loading

0 comments on commit 20e57a0

Please sign in to comment.