Skip to content
This repository was archived by the owner on Feb 8, 2025. It is now read-only.

Commit aa3ca89

Browse files
committed
feat(app): implement Snackbar with react-call
1 parent 6907e41 commit aa3ca89

31 files changed

+160
-198
lines changed
Binary file not shown.

app/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@
124124
"react-native-screens": "3.31.1",
125125
"react-native-svg": "15.2.0",
126126
"react-native-tab-view": "^3.5.2",
127-
"react-native-toast-message": "^2.2.0",
128127
"react-native-typewriter": "^0.7.0",
129128
"react-native-unistyles": "^2.9.1",
130129
"react-native-web": "~0.19.12",

app/src/app/(nav)/[account]/swap.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { getSwapOperations } from '~/util/swap/syncswap/swap';
2424
import Decimal from 'decimal.js';
2525
import { ampli } from '~/lib/ampli';
2626
import { SwapToTokenItem } from '#/swap/SwapToTokenItem';
27-
import { showError } from '#/provider/SnackbarProvider';
27+
import { showError } from '#/Snackbar';
2828
import { estimateSwap } from '~/util/swap/syncswap/estimate';
2929
import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton';
3030
import { graphql } from 'relay-runtime';

app/src/app/(sheet)/link.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { materialCommunityIcon } from '@theme/icons';
44
import { View } from 'react-native';
55
import { Text } from 'react-native-paper';
66
import { Button } from '#/Button';
7-
import { showSuccess } from '#/provider/SnackbarProvider';
7+
import { showSuccess } from '#/Snackbar';
88
import { z } from 'zod';
99
import { useLocalParams } from '~/hooks/useLocalParams';
1010
import { createStyles, useStyles } from '@theme/styles';

app/src/app/(sheet)/select/address.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ADDRESS_SELECTED } from '~/hooks/useSelectAddress';
2121
import { zChain, zArray, zUAddress, zAddress } from '~/lib/zod';
2222
import * as Clipboard from 'expo-clipboard';
2323
import { isAddress } from 'viem';
24-
import { showWarning } from '#/provider/SnackbarProvider';
24+
import { showWarning } from '#/Snackbar';
2525
import { BottomSheetScrollView } from '@gorhom/bottom-sheet';
2626
import { graphql } from 'relay-runtime';
2727
import { useLazyQuery } from '~/api';

app/src/app/(sheet)/sessions/connect/[id].tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Actions } from '#/layout/Actions';
77
import { Sheet } from '#/sheet/Sheet';
88
import { AccountsList } from '#/walletconnect/AccountsList';
99
import { DappHeader } from '#/walletconnect/DappHeader';
10-
import { hideSnackbar, showError, showSuccess } from '#/provider/SnackbarProvider';
10+
import { hideSnackbar, showError, showSuccess } from '#/Snackbar';
1111
import {
1212
sessionChains,
1313
supportedNamespaces,
@@ -91,7 +91,7 @@ export default function ConnectSessionSheet() {
9191
}),
9292
});
9393

94-
showSuccess(`Connected with ${dapp}`, { visibilityTime: 2000 });
94+
showSuccess(`Connected with ${dapp}`, { duration: 2000 });
9595
} catch (error) {
9696
showError(`Failed to connect to ${dapp}: ${(error as Error).message}`, { event: { error } });
9797
}

app/src/app/(sheet)/wc.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Sheet } from '#/sheet/Sheet';
88
import { useLocalParams } from '~/hooks/useLocalParams';
99
import { useWalletConnectWithoutWatching } from '~/lib/wc';
1010
import { zWalletConnectUri } from '~/lib/wc/uri';
11-
import { showError } from '#/provider/SnackbarProvider';
11+
import { showError } from '#/Snackbar';
1212
import { useRouter } from 'expo-router';
1313

1414
export const WalletConnectUriScreenParams = z.object({ uri: zWalletConnectUri() });

app/src/app/_layout.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { WalletConnectListeners } from '#/walletconnect/WalletConnectListeners';
1313
import { AuthGate } from '#/provider/AuthGate';
1414
import { ApiProvider } from '~/api/ApiProvider';
1515
import { NotificationsProvider } from '#/provider/NotificationsProvider';
16-
import { SnackbarProvider } from '#/provider/SnackbarProvider';
16+
import { Snackbar } from '#/Snackbar';
1717
import { UpdateProvider } from '#/provider/UpdateProvider';
1818
import { ThemeProvider } from '~/util/theme/ThemeProvider';
1919
import { SafeAreaProvider } from 'react-native-safe-area-context';
@@ -114,7 +114,7 @@ function RootLayout() {
114114
</AuthGate>
115115
</Suspense>
116116
</Background>
117-
<SnackbarProvider />
117+
<Snackbar.Root />
118118
<Confirm.Root />
119119
</GestureHandlerRootView>
120120
</IntlProvider>

app/src/app/auth.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { useEffect } from 'react';
2020
import AsyncStorage from '@react-native-async-storage/async-storage';
2121
import * as Updates from 'expo-updates';
2222
import { Confirm } from '#/Confirm';
23-
import { showInfo } from '#/provider/SnackbarProvider';
23+
import { showInfo } from '#/Snackbar';
2424

2525
const UNLOCKED = new Subject<true>();
2626
const emitAuth = () => UNLOCKED.next(true);

app/src/app/scan.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Actions } from '#/layout/Actions';
77
import { Address, UAddress, tryAsAddress } from 'lib';
88
import * as Linking from 'expo-linking';
99
import useAsyncEffect from 'use-async-effect';
10-
import { showError } from '#/provider/SnackbarProvider';
10+
import { showError } from '#/Snackbar';
1111
import { parseAppLink } from '~/lib/appLink';
1212
import { useFocusEffect, useRouter } from 'expo-router';
1313
import { ScanOverlay } from '#/ScanOverlay';

app/src/components/ScanOverlay.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BackIcon, ContactsIcon, PasteIcon } from '~/util/theme/icons';
22
import { IconButton } from 'react-native-paper';
33
import { StyleSheet, View } from 'react-native';
44
import * as Clipboard from 'expo-clipboard';
5-
import { showWarning } from '#/provider/SnackbarProvider';
5+
import { showWarning } from '#/Snackbar';
66
import { useSelectAddress } from '~/hooks/useSelectAddress';
77
import { useRouter } from 'expo-router';
88
import { createStyles, useStyles } from '@theme/styles';

app/src/components/Snackbar.tsx

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { createStyles, useStyles } from '@theme/styles';
2+
import { useEffect } from 'react';
3+
import { createCallable } from 'react-call';
4+
import { Snackbar as BaseSnackbar, Text } from 'react-native-paper';
5+
import { Subject } from 'rxjs';
6+
import { hapticFeedback } from '~/lib/haptic';
7+
import { logEvent, LogEventParams } from '~/util/analytics';
8+
9+
const HIDE_SNACKBAR = new Subject<true>();
10+
11+
type SnackbarVariant = 'info' | 'success' | 'warning' | 'error';
12+
13+
export interface SnackbarProps {
14+
variant?: SnackbarVariant;
15+
message: string;
16+
duration?: number;
17+
action?: string;
18+
event?: Partial<LogEventParams> | boolean;
19+
}
20+
21+
export const Snackbar = createCallable<SnackbarProps, boolean>(
22+
({ call, variant = 'info', message, duration = 6000, action, event }) => {
23+
const { styles } = useStyles(getStylesheet({ variant }));
24+
25+
useEffect(() => {
26+
const sub = HIDE_SNACKBAR.subscribe(() => call.end(false));
27+
return () => sub.unsubscribe();
28+
}, [call]);
29+
30+
useEffect(() => {
31+
if (variant !== 'info') hapticFeedback(variant);
32+
}, [variant]);
33+
34+
useEffect(() => {
35+
if (event && variant !== 'success') {
36+
logEvent({
37+
level: variant,
38+
message,
39+
snackbar: true,
40+
...(typeof event === 'object' && event),
41+
});
42+
}
43+
}, [message, variant, event]);
44+
45+
return (
46+
<BaseSnackbar
47+
elevation={2}
48+
visible
49+
duration={duration}
50+
onDismiss={() => call.end(false)}
51+
style={[styles.snackbarBase, styles.snackbar]}
52+
{...(action && {
53+
action: {
54+
label: action,
55+
labelStyle: styles.actionLabel,
56+
onPress: () => call.end(true),
57+
},
58+
})}
59+
>
60+
<Text variant="bodyMedium" style={styles.message}>
61+
{message}
62+
</Text>
63+
</BaseSnackbar>
64+
);
65+
},
66+
);
67+
68+
const getStylesheet = ({ variant }: { variant: SnackbarVariant }) =>
69+
createStyles(({ colors }) => {
70+
const s = {
71+
info: {
72+
snackbar: { backgroundColor: colors.inverseSurface },
73+
message: { color: colors.inverseOnSurface },
74+
actionLabel: { color: colors.inversePrimary },
75+
},
76+
success: {
77+
snackbar: { backgroundColor: colors.successContainer },
78+
message: { color: colors.onSuccessContainer },
79+
},
80+
warning: {
81+
snackbar: { backgroundColor: colors.warningContainer },
82+
message: { color: colors.onWarningContainer },
83+
},
84+
error: {
85+
snackbar: { backgroundColor: colors.errorContainer },
86+
message: { color: colors.onErrorContainer },
87+
},
88+
}[variant];
89+
90+
return {
91+
actionLabel: {
92+
color: colors.primary,
93+
},
94+
snackbarBase: {
95+
maxWidth: 600,
96+
},
97+
...s,
98+
};
99+
});
100+
101+
type ShowOptions = Omit<SnackbarProps, 'variant' | 'message'>;
102+
103+
export const showInfo = (message: string, options?: ShowOptions) =>
104+
Snackbar.call({ variant: 'info', message, ...options });
105+
106+
export const showSuccess = (message: string, options?: ShowOptions) =>
107+
Snackbar.call({ variant: 'success', message, ...options });
108+
109+
export const showWarning = (message: string, options?: ShowOptions) =>
110+
Snackbar.call({ variant: 'warning', message, ...options });
111+
112+
export const showError = (message: string, options?: ShowOptions) =>
113+
Snackbar.call({ variant: 'error', message, ...options });
114+
115+
export const hideSnackbar = () => HIDE_SNACKBAR.next(true);

app/src/components/account/PolicySuggestions.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Chip } from '#/Chip';
2-
import { showError } from '#/provider/SnackbarProvider';
32
import { createStyles, useStyles } from '@theme/styles';
43
import { useRouter } from 'expo-router';
54
import { asChain } from 'lib';

app/src/components/auth/PasswordSettings.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { changeSecureStorePassword } from '~/lib/secure-storage';
1111
import { createStyles, useStyles } from '@theme/styles';
1212
import { Actions } from '#/layout/Actions';
1313
import { Button } from '#/Button';
14-
import { showInfo } from '#/provider/SnackbarProvider';
14+
import { showInfo } from '#/Snackbar';
1515

1616
const PASSWORD_HASH = persistedAtom<string | null>('passwordHash', null);
1717
export const usePasswordHash = () => useAtomValue(PASSWORD_HASH);

app/src/components/cloud/google/useLinkGoogle.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { showError } from '#/provider/SnackbarProvider';
1+
import { showError } from '#/Snackbar';
22
import { ampli } from '~/lib/ampli';
33
import { useGetGoogleApprover } from '#/cloud/google/useGetGoogleApprover';
44
import { graphql } from 'relay-runtime';

app/src/components/cloud/useLinkApple.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useGetAppleApprover } from './useGetAppleApprover';
2-
import { showError } from '#/provider/SnackbarProvider';
2+
import { showError } from '#/Snackbar';
33
import { ampli } from '~/lib/ampli';
44
import { graphql } from 'relay-runtime';
55
import { useFragment } from 'react-relay';

app/src/components/link/ledger/LedgerItem.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
33
import { ListItem } from '#/list/ListItem';
44
import { useGetLedgerApprover } from '~/app/(sheet)/ledger/approve';
55
import { APPROVER_BLE_IDS } from '~/hooks/ledger/useLedger';
6-
import { showError } from '#/provider/SnackbarProvider';
6+
import { showError } from '#/Snackbar';
77
import { useImmerAtom } from 'jotai-immer';
88
import { getLedgerDeviceModel } from '~/hooks/ledger/connectLedger';
99
import { elipseTruncate } from '~/util/format';
@@ -94,10 +94,11 @@ export function LedgerItem({ device: d, ...props }: LedgerItemProps) {
9494
},
9595
}),
9696
);
97-
if (!authHeaders)
98-
return showError('Connection request cancelled', {
99-
action: { label: 'Try again', onPress: connect },
100-
});
97+
if (!authHeaders) {
98+
const retry = await showError('Connection request cancelled', { action: 'Try again' });
99+
if (retry) connect();
100+
return;
101+
}
101102

102103
// 1. Link
103104
const { approvers } = (await link({ token: user.linkingToken }, { headers: authHeaders })).link;

app/src/components/policy/ApprovalSettings.tsx

+9-12
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ListItemHorizontalTrailing } from '#/list/ListItemHorizontalTrailing';
99
import { ListItemTrailingText } from '#/list/ListItemTrailingText';
1010
import { ApproverItem } from '#/policy/ApproverItem';
1111
import { ThresholdChip } from './ThresholdChip';
12-
import { showInfo } from '#/provider/SnackbarProvider';
12+
import { showInfo } from '#/Snackbar';
1313
import { useSelectAddress } from '~/hooks/useSelectAddress';
1414
import { useToggle } from '~/hooks/useToggle';
1515
import { usePolicyDraft } from '~/lib/policy/policyAsDraft';
@@ -38,24 +38,21 @@ export function ApprovalSettings() {
3838
}
3939
};
4040

41-
const remove = (approver: Address) => {
41+
const remove = async (approver: Address) => {
4242
const originalThreshold = policy.threshold;
4343

4444
update((draft) => {
4545
draft.approvers.delete(approver);
4646
draft.threshold = Math.max(policy.threshold, policy.approvers.size);
4747
});
4848

49-
showInfo('Approver removed', {
50-
action: {
51-
label: 'Undo',
52-
onPress: () =>
53-
update((draft) => {
54-
draft.approvers.add(approver);
55-
draft.threshold = originalThreshold;
56-
}),
57-
},
58-
});
49+
const undo = await showInfo('Approver removed', { action: 'Undo' });
50+
if (undo) {
51+
update((draft) => {
52+
draft.approvers.add(approver);
53+
draft.threshold = originalThreshold;
54+
});
55+
}
5956
};
6057

6158
return (

app/src/components/policy/PolicySideSheet.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { FormTextField } from '#/fields/FormTextField';
33
import { Actions } from '#/layout/Actions';
44
import { FormSubmitButton } from '#/fields/FormSubmitButton';
55
import { usePolicyDraft } from '~/lib/policy/policyAsDraft';
6-
import { showError } from '#/provider/SnackbarProvider';
6+
import { showError } from '#/Snackbar';
77
import { SideSheet } from '../SideSheet/SideSheet';
88
import { Button } from '../Button';
99
import { createStyles, useStyles } from '@theme/styles';
@@ -122,7 +122,7 @@ export function PolicySideSheet(props: PolicySideSheetProps) {
122122
if (policy && draft.key !== undefined) {
123123
const r = (await rename({ account: draft.account, key: draft.key, name }))
124124
?.updatePolicyDetails;
125-
if (r?.__typename !== 'Policy') return showError(r?.message);
125+
if (r?.__typename !== 'Policy') return showError(r?.message || 'Unknown error');
126126
}
127127

128128
updateDraft((draft) => {

0 commit comments

Comments
 (0)