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: design system snackbar + design system examples #1164

Open
wants to merge 9 commits into
base: release/2.0.8
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import { Provider as PaperProvider } from "react-native-paper";
import { ThirdwebProvider } from "thirdweb/react";

import { Snackbars } from "@components/Snackbar/Snackbars";
import { xmtpCron, xmtpEngine } from "./components/XmtpEngine";
import config from "./config";
import {
Expand Down Expand Up @@ -78,10 +79,10 @@
}, []);

const showDebugMenu = useCallback(() => {
if (!debugRef.current || !(debugRef.current as any).showDebugMenu) {

Check warning on line 82 in App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return;
}
(debugRef.current as any).showDebugMenu();

Check warning on line 85 in App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
}, []);

useEffect(() => {
Expand Down Expand Up @@ -139,7 +140,7 @@
const AppKeyboardProvider =
Platform.OS === "ios" ? KeyboardProvider : React.Fragment;

export default function AppWithProviders() {

Check warning on line 143 in App.tsx

View workflow job for this annotation

GitHub Actions / lint

Prefer named exports
const colorScheme = useColorScheme();

const paperTheme = useMemo(() => {
Expand All @@ -157,10 +158,11 @@
<ActionSheetProvider>
<ThemeProvider value={{ themeScheme, setThemeContextOverride }}>
<PaperProvider theme={paperTheme}>
<GestureHandlerRootView style={{ flex: 1 }}>

Check warning on line 161 in App.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { flex: 1 }
<BottomSheetModalProvider>
<PortalProvider>
<App />
<Snackbars />
</PortalProvider>
</BottomSheetModalProvider>
</GestureHandlerRootView>
Expand Down
6 changes: 3 additions & 3 deletions components/NewAccount/NewAccountScreenComp.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { useAppTheme } from "@theme/useAppTheme";
import { memo } from "react";

import { spacing } from "../../theme";
import { Screen } from "../Screen/ScreenComp/Screen";
import { IScreenProps } from "../Screen/ScreenComp/Screen.props";

export const NewAccountScreenComp = memo(function (props: IScreenProps) {
const { contentContainerStyle, ...restProps } = props;
const { theme } = useAppTheme();

return (
<Screen
preset="scroll"
safeAreaEdges={["bottom"]}
contentContainerStyle={[
{
paddingHorizontal: spacing.md,
paddingHorizontal: theme.spacing.md,
},
contentContainerStyle,
]}
Expand Down
5 changes: 2 additions & 3 deletions components/Onboarding/ConnectViaWallet/ConnectViaWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ActivityIndicator } from "react-native";
import { Center } from "../../../design-system/Center";
import { VStack } from "../../../design-system/VStack";
import { useRouter } from "../../../navigation/useNavigation";
import { spacing } from "../../../theme";
import { useAppTheme } from "../../../theme/useAppTheme";
import { PictoTitleSubtitle } from "../../PictoTitleSubtitle";
import { Terms } from "../Terms";
Expand Down Expand Up @@ -194,8 +193,8 @@ const ConnectViaWalletUI = memo(function ConnectViaWalletUI(props: object) {
{showValueProps && <ValueProps />}
<VStack
style={{
rowGap: spacing.sm,
marginTop: spacing.lg,
rowGap: theme.spacing.sm,
marginTop: theme.spacing.lg,
}}
>
<Button
Expand Down
6 changes: 4 additions & 2 deletions components/Onboarding/OnboardingScreenComp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { memo } from "react";

import { spacing } from "../../theme";
import { useAppTheme } from "@theme/useAppTheme";
import { Screen } from "../Screen/ScreenComp/Screen";
import { IScreenProps } from "../Screen/ScreenComp/Screen.props";

Expand All @@ -9,13 +9,15 @@ export const OnboardingScreenComp = memo(function OnboardingScreenComp(
) {
const { contentContainerStyle, ...rest } = props;

const { theme } = useAppTheme();

return (
<Screen
preset="scroll"
safeAreaEdges={["bottom"]}
contentContainerStyle={[
{
paddingHorizontal: spacing.lg,
paddingHorizontal: theme.spacing.lg,
},
contentContainerStyle,
]}
Expand Down
8 changes: 4 additions & 4 deletions components/Screen/ScreenComp/Screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ScrollView, View, ViewStyle } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { VStack } from "../../../design-system/VStack";
import { useAppTheme } from "../../../theme/useAppTheme";
import {
isNonScrolling,
useAutoPreset,
Expand All @@ -15,9 +17,6 @@ import {
IScreenProps,
ScrollScreenProps,
} from "./Screen.props";
import { VStack } from "../../../design-system/VStack";
import { spacing } from "../../../theme";
import { useAppTheme } from "../../../theme/useAppTheme";

function ScreenWithoutScrolling(props: IScreenProps) {
const { style, contentContainerStyle, children } = props;
Expand All @@ -30,6 +29,7 @@ function ScreenWithoutScrolling(props: IScreenProps) {

function ScreenWithScrolling(props: IScreenProps) {
const insets = useSafeAreaInsets();
const { theme } = useAppTheme();

const {
children,
Expand All @@ -38,7 +38,7 @@ function ScreenWithScrolling(props: IScreenProps) {
ScrollViewProps,
keyboardOffset = insets.bottom +
// By default we never want the input hugging the keyboard
spacing.xs,
theme.spacing.xs,
style,
} = props as ScrollScreenProps;

Expand Down
21 changes: 21 additions & 0 deletions components/Snackbar/Snackbar.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { spacing } from "@theme/spacing";
import { initialWindowMetrics } from "react-native-safe-area-context";

export const SNACKBAR_HEIGHT = 50;

export const SNACKBAR_LARGE_TEXT_HEIGHT = 100;

export const SNACKBARS_MAX_VISIBLE = 3;

export const SNACKBAR_SPACE_BETWEEN_SNACKBARS = spacing.sm;

export const SNACKBAR_BACKDROP_ADDITIONAL_HEIGHT = SNACKBAR_LARGE_TEXT_HEIGHT;

export const SNACKBAR_BOTTOM_OFFSET =
initialWindowMetrics?.insets.bottom ?? spacing.md;

export const SNACKBAR_BACKDROP_MAX_HEIGHT =
SNACKBARS_MAX_VISIBLE *
(SNACKBAR_LARGE_TEXT_HEIGHT + SNACKBAR_SPACE_BETWEEN_SNACKBARS) +
SNACKBAR_BACKDROP_ADDITIONAL_HEIGHT +
SNACKBAR_BOTTOM_OFFSET;
91 changes: 91 additions & 0 deletions components/Snackbar/Snackbar.example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { showSnackbar } from "@components/Snackbar/Snackbar.service";
import { Button } from "@design-system/Button/Button";
import { VStack } from "@design-system/VStack";
import { useAppTheme } from "@theme/useAppTheme";
import { Image } from "expo-image";
import { memo } from "react";
import { Dimensions } from "react-native";

export const SnackbarExample = memo(function SnackbarExample() {
const { theme } = useAppTheme();

const windowWidth = Dimensions.get("window").width;

return (
<VStack style={{ rowGap: theme.spacing.lg }}>
<VStack style={{ rowGap: theme.spacing.md }}>
<Button
text="Show Basic"
onPress={() => showSnackbar({ message: "This is a basic snackbar" })}
/>
<Button
text="Show Error"
onPress={() =>
showSnackbar({
message: "Something went wrong!",
type: "error",
})
}
/>
<Button
text="Show Info"
onPress={() =>
showSnackbar({
message: "Here's some useful information",
type: "info",
})
}
/>
<Button
text="Show Large Text"
onPress={() =>
showSnackbar({
message:
"This is a longer message that will wrap to multiple lines and show how the snackbar handles longer content in a more spacious layout",
isMultiLine: true,
})
}
/>
<Button
text="Show with Actions"
onPress={() =>
showSnackbar({
message: "Do you want to proceed?",
type: "info",
actions: [
{
label: "Learn more",
onPress: () => console.log("Learn more pressed"),
},
],
})
}
/>
<Button
text="Show with actions large text"
onPress={() =>
showSnackbar({
message:
"This is a longer message that will wrap to multiple lines and show how the snackbar handles longer content in a more spacious layout",
isMultiLine: true,
type: "info",
actions: [
{
label: "Learn more",
onPress: () => console.log("Learn more pressed"),
},
],
})
}
/>
</VStack>

<Image
source={{
uri: `https://picsum.photos/600/600`,
}}
style={{ width: windowWidth - theme.spacing.lg * 2, height: 500 }}
/>
</VStack>
);
});
69 changes: 69 additions & 0 deletions components/Snackbar/Snackbar.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useSnackBarStore } from "@components/Snackbar/Snackbar.store";
import { ISnackbar } from "@components/Snackbar/Snackbar.types";
import { Haptics } from "@utils/haptics";
import { v4 as uuidv4 } from "uuid";

export type INewSnackbar = Partial<Exclude<ISnackbar, "key">> & {
message: string;
};

export function showSnackbar(newSnackbar: INewSnackbar) {
Haptics.softImpactAsync();

useSnackBarStore.setState((prev) => {
return {
snackbars: [
{
message: newSnackbar.message,
isMultiLine: newSnackbar.isMultiLine || false,
key: uuidv4(),
type: newSnackbar.type ?? "info",
actions: newSnackbar.actions ?? [],
},
...prev.snackbars,
],
};
});
}

export function clearAllSnackbars() {
useSnackBarStore.getState().clearAllSnackbars();
}

export function useSnackbars() {
return useSnackBarStore((state) => state.snackbars);
}

export function dismissSnackbar(key: string) {
useSnackBarStore.setState((prev) => {
return {
snackbars: prev.snackbars.filter((item) => item.key !== key),
};
});
}

export function onSnackbarsChange(callback: (snackbars: ISnackbar[]) => void) {
return useSnackBarStore.subscribe((state) => state.snackbars, callback);
}

export function onNewSnackbar(callback: (snackbar: ISnackbar) => void) {
return useSnackBarStore.subscribe(
(state) => state.snackbars,
(snackbars, previousSnackbars) => {
const firstSnackbar = snackbars[0];
if (firstSnackbar) {
callback(firstSnackbar);
}
}
);
}

export function getNumberOfSnackbars() {
return useSnackBarStore.getState().snackbars.length;
}

export function getSnackbarIndex(key: string) {
return useSnackBarStore
.getState()
.snackbars.findIndex((item) => item.key === key);
}
23 changes: 23 additions & 0 deletions components/Snackbar/Snackbar.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ISnackbar } from "@components/Snackbar/Snackbar.types";
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";

export interface ISnackBarStore {
snackbars: ISnackbar[];
showSnackbar: (snackbar: ISnackbar) => void;
clearAllSnackbars: () => void;
}

export const useSnackBarStore = create<ISnackBarStore>()(
subscribeWithSelector((set, get) => ({
snackbars: [],
showSnackbar: (snackbar) => {
set((state) => ({
snackbars: [...state.snackbars, snackbar],
}));
},
clearAllSnackbars: () => {
set({ snackbars: [] });
},
}))
);
Loading
Loading