Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: crash screen #1613

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions apps/browser-extension-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@
},
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@cardano-sdk/cardano-services-client": "0.23.2",
"@cardano-sdk/core": "0.42.1",
"@cardano-sdk/dapp-connector": "0.12.47",
"@cardano-sdk/input-selection": "0.13.31",
"@cardano-sdk/tx-construction": "0.23.0",
"@cardano-sdk/cardano-services-client": "0.23.4",
"@cardano-sdk/core": "0.42.2",
"@cardano-sdk/dapp-connector": "0.12.48",
"@cardano-sdk/input-selection": "0.13.32",
"@cardano-sdk/tx-construction": "0.24.0",
"@cardano-sdk/util": "0.15.5",
"@cardano-sdk/util-rxjs": "0.8.0",
"@cardano-sdk/wallet": "0.46.2",
"@cardano-sdk/web-extension": "0.36.1",
"@cardano-sdk/util-rxjs": "0.9.0",
"@cardano-sdk/wallet": "0.47.0",
"@cardano-sdk/web-extension": "0.37.0",
"@emurgo/cip14-js": "~3.0.1",
"@input-output-hk/lace-ui-toolkit": "1.21.0",
"@lace/cardano": "0.1.0",
Expand Down Expand Up @@ -99,7 +99,7 @@
"zustand": "3.5.14"
},
"devDependencies": {
"@cardano-sdk/hardware-ledger": "0.12.16",
"@cardano-sdk/hardware-ledger": "0.12.17",
"@emurgo/cardano-message-signing-asmjs": "1.0.1",
"@openpgp/web-stream-tools": "0.0.11-patch-0",
"@pdfme/common": "^4.0.2",
Expand Down
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 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
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
Loading
Loading