diff --git a/App.tsx b/App.tsx
index 11fdee059..fc027c8bb 100644
--- a/App.tsx
+++ b/App.tsx
@@ -29,6 +29,7 @@ import { KeyboardProvider } from "react-native-keyboard-controller";
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 {
@@ -161,6 +162,7 @@ export default function AppWithProviders() {
+
diff --git a/components/NewAccount/NewAccountScreenComp.tsx b/components/NewAccount/NewAccountScreenComp.tsx
index a20551d59..f2f9b3ee3 100644
--- a/components/NewAccount/NewAccountScreenComp.tsx
+++ b/components/NewAccount/NewAccountScreenComp.tsx
@@ -1,11 +1,11 @@
+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 (
}
+ );
+});
diff --git a/components/Snackbar/Snackbar.service.ts b/components/Snackbar/Snackbar.service.ts
new file mode 100644
index 000000000..b46d69e56
--- /dev/null
+++ b/components/Snackbar/Snackbar.service.ts
@@ -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> & {
+ 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);
+}
diff --git a/components/Snackbar/Snackbar.store.ts b/components/Snackbar/Snackbar.store.ts
new file mode 100644
index 000000000..a5fc0c3b4
--- /dev/null
+++ b/components/Snackbar/Snackbar.store.ts
@@ -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()(
+ subscribeWithSelector((set, get) => ({
+ snackbars: [],
+ showSnackbar: (snackbar) => {
+ set((state) => ({
+ snackbars: [...state.snackbars, snackbar],
+ }));
+ },
+ clearAllSnackbars: () => {
+ set({ snackbars: [] });
+ },
+ }))
+);
diff --git a/components/Snackbar/Snackbar.tsx b/components/Snackbar/Snackbar.tsx
new file mode 100644
index 000000000..702ed9cbf
--- /dev/null
+++ b/components/Snackbar/Snackbar.tsx
@@ -0,0 +1,269 @@
+import {
+ SNACKBARS_MAX_VISIBLE,
+ SNACKBAR_BOTTOM_OFFSET,
+ SNACKBAR_HEIGHT,
+ SNACKBAR_LARGE_TEXT_HEIGHT,
+} from "@components/Snackbar/Snackbar.constants";
+import {
+ getNumberOfSnackbars,
+ onSnackbarsChange,
+} from "@components/Snackbar/Snackbar.service";
+import { ISnackbar } from "@components/Snackbar/Snackbar.types";
+import { Button } from "@design-system/Button/Button";
+import { Center } from "@design-system/Center";
+import { AnimatedHStack, HStack } from "@design-system/HStack";
+import { IconButton } from "@design-system/IconButton/IconButton";
+import { Text } from "@design-system/Text";
+import { AnimatedVStack } from "@design-system/VStack";
+import { SICK_SPRING_CONFIG } from "@theme/animations";
+import { useAppTheme } from "@theme/useAppTheme";
+import { memo, useCallback, useEffect } from "react";
+import { useWindowDimensions } from "react-native";
+import { Gesture, GestureDetector } from "react-native-gesture-handler";
+import {
+ interpolate,
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+ withTiming,
+} from "react-native-reanimated";
+import { SNACKBAR_SPACE_BETWEEN_SNACKBARS } from "./Snackbar.constants";
+
+type SnackbarProps = {
+ snackbar: ISnackbar;
+ onDismiss: () => void;
+};
+
+export const Snackbar = memo(
+ function Snackbar(props: SnackbarProps) {
+ const { snackbar, onDismiss } = props;
+ const { theme } = useAppTheme();
+ const { width: windowWidth } = useWindowDimensions();
+
+ const isFirstSnack = getNumberOfSnackbars() === 1;
+ const initialBottomPosition = isFirstSnack
+ ? -SNACKBAR_HEIGHT / 2
+ : SNACKBAR_BOTTOM_OFFSET;
+
+ const snackbarIndexAV = useSharedValue(0);
+ const bottomAV = useSharedValue(initialBottomPosition);
+ const translateXAV = useSharedValue(0);
+ const isSwipingAV = useSharedValue(false);
+ const firstSnackbarRenderProgressAV = useSharedValue(
+ // We only want the first snackbar to animate in with a special animation
+ isFirstSnack ? 0 : 1
+ );
+
+ useEffect(() => {
+ bottomAV.value = withSpring(SNACKBAR_BOTTOM_OFFSET, SICK_SPRING_CONFIG);
+ firstSnackbarRenderProgressAV.value = withSpring(1, SICK_SPRING_CONFIG);
+ }, [bottomAV, firstSnackbarRenderProgressAV]);
+
+ useEffect(() => {
+ const unsubscribe = onSnackbarsChange((snackbars) => {
+ const snackbarIndex = snackbars.findIndex(
+ (item) => item.key === snackbar.key
+ );
+
+ // Set the new new index of the current snackbar
+ snackbarIndexAV.value = withSpring(snackbarIndex, SICK_SPRING_CONFIG);
+
+ if (snackbarIndex === -1) {
+ return;
+ }
+
+ const totalHeightBeforeThisSnackbar = snackbars
+ .slice(0, Math.min(snackbarIndex, SNACKBARS_MAX_VISIBLE))
+ .reduce((acc, item) => {
+ const snackbarHeight = item.isMultiLine
+ ? SNACKBAR_LARGE_TEXT_HEIGHT
+ : SNACKBAR_HEIGHT;
+ return acc + snackbarHeight + SNACKBAR_SPACE_BETWEEN_SNACKBARS;
+ }, 0);
+
+ bottomAV.value = withSpring(
+ SNACKBAR_BOTTOM_OFFSET + totalHeightBeforeThisSnackbar,
+ SICK_SPRING_CONFIG
+ );
+ });
+
+ return unsubscribe;
+ }, [snackbar.key, bottomAV, snackbarIndexAV]);
+
+ const dismissItem = useCallback(() => {
+ "worklet";
+ translateXAV.value = withTiming(
+ -windowWidth,
+ { duration: 250 },
+ (isFinished) => {
+ if (isFinished) {
+ runOnJS(onDismiss)();
+ }
+ }
+ );
+ }, [onDismiss, translateXAV, windowWidth]);
+
+ const gesture = Gesture.Pan()
+ .onBegin(() => {
+ isSwipingAV.value = true;
+ })
+ .onUpdate((event) => {
+ if (event.translationX <= 0) {
+ translateXAV.value = event.translationX;
+ }
+ })
+ .onEnd((event) => {
+ if (event.translationX < -50) {
+ dismissItem();
+ } else {
+ translateXAV.value = withSpring(0);
+ }
+ })
+ .onFinalize(() => {
+ isSwipingAV.value = false;
+ });
+
+ const containerAS = useAnimatedStyle(
+ () => ({
+ bottom: bottomAV.value + theme.spacing.md,
+ zIndex: withTiming(100 - snackbarIndexAV.value),
+ // Animate shadow radius based on snackbar position in stack
+ // - Starts at 16 for first snackbar and decreases by 2.5 for each subsequent one
+ // - Has minimum value of 2 to maintain some depth
+ shadowRadius: interpolate(snackbarIndexAV.value, [0, 10], [16, 2]),
+ // Animate shadow opacity based on snackbar position
+ // - First 3 snackbars have opacity of 1
+ // - After 3rd snackbar, opacity decreases linearly by 0.05
+ // - This creates a nice fading effect for stacked snackbars
+ shadowOpacity: interpolate(
+ snackbarIndexAV.value,
+ [0, 3, 10],
+ [1, 1, 0]
+ ),
+ // The content of the first two StackedToasts is visible
+ // The content of the other StackedToasts is hidden
+ opacity: interpolate(
+ snackbarIndexAV.value,
+ [0, SNACKBARS_MAX_VISIBLE - 1, SNACKBARS_MAX_VISIBLE],
+ [1, 1, 0]
+ ),
+ transform: [
+ // For the dragging animation
+ { translateX: translateXAV.value },
+ {
+ scale: interpolate(
+ firstSnackbarRenderProgressAV.value,
+ [0, 1],
+ [0.9, 1]
+ ),
+ },
+ ],
+ }),
+ []
+ );
+
+ const SnackContainer = snackbar.isMultiLine
+ ? AnimatedVStack
+ : AnimatedHStack;
+ const snackbarHeight = snackbar.isMultiLine
+ ? SNACKBAR_LARGE_TEXT_HEIGHT
+ : SNACKBAR_HEIGHT;
+
+ return (
+
+
+
+
+
+
+ {snackbar.message}
+
+
+
+
+ {snackbar.actions?.map((action) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+ },
+ // We don't need to rerender a snackbar
+ () => true
+);
diff --git a/components/Snackbar/Snackbar.types.ts b/components/Snackbar/Snackbar.types.ts
new file mode 100644
index 000000000..db335dbb6
--- /dev/null
+++ b/components/Snackbar/Snackbar.types.ts
@@ -0,0 +1,10 @@
+export type ISnackbar = {
+ key: string;
+ message: string | React.ReactNode;
+ type?: "error" | "success" | "info";
+ isMultiLine?: boolean;
+ actions?: {
+ label: string;
+ onPress: () => void;
+ }[];
+};
diff --git a/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.android.tsx b/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.android.tsx
new file mode 100644
index 000000000..46769f561
--- /dev/null
+++ b/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.android.tsx
@@ -0,0 +1,41 @@
+import { useSnackbars } from "@components/Snackbar/Snackbar.service";
+import { useGradientHeight } from "@components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.utils";
+import { AnimatedVStack } from "@design-system/VStack";
+import { LinearGradient } from "expo-linear-gradient";
+import React from "react";
+import { StyleSheet, useWindowDimensions } from "react-native";
+import { useAnimatedStyle, withTiming } from "react-native-reanimated";
+
+/**
+ * On Android, blur doesn't work
+ */
+
+export const Backdrop = () => {
+ const snackbars = useSnackbars();
+
+ const { height: windowHeight } = useWindowDimensions();
+
+ const gradientHeight = useGradientHeight();
+
+ const rAnimatedStyle = useAnimatedStyle(() => {
+ const top = windowHeight - gradientHeight;
+ return {
+ top: withTiming(top, {
+ duration: 500,
+ }),
+ };
+ }, [windowHeight, snackbars.length]);
+
+ return (
+
+
+
+ );
+};
diff --git a/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.tsx b/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.tsx
new file mode 100644
index 000000000..bf24d50d0
--- /dev/null
+++ b/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.tsx
@@ -0,0 +1,67 @@
+import { useSnackbars } from "@components/Snackbar/Snackbar.service";
+import { useGradientHeight } from "@components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.utils";
+// import MaskedView from "@react-native-masked-view/masked-view";
+import { SICK_SPRING_CONFIG } from "@theme/animations";
+import { LinearGradient } from "expo-linear-gradient";
+import { memo } from "react";
+import { StyleSheet, useWindowDimensions } from "react-native";
+import Animated, {
+ useAnimatedStyle,
+ withSpring,
+} from "react-native-reanimated";
+
+// The Backdrop component creates a visually appealing background for stacked snackbars.
+// It uses a combination of a masked view, linear gradient, and blur effect to create
+// a semi-transparent, animated backdrop that adjusts based on the number of snackbars.
+// The backdrop animates from the bottom of the screen, creating a smooth transition
+// as snackbars are added or removed.
+
+export const SnackbarBackdrop = memo(() => {
+ const snackbars = useSnackbars();
+ const { height: windowHeight } = useWindowDimensions();
+ const gradientHeight = useGradientHeight();
+
+ const rAnimatedStyle = useAnimatedStyle(() => {
+ const top = windowHeight - gradientHeight;
+ return {
+ top: withSpring(top, SICK_SPRING_CONFIG),
+ };
+ }, [windowHeight, snackbars.length]);
+
+ return (
+
+
+ {/* TODO: Maybe add back later. For now masked view can be a litte unstable and buggy */}
+ {/*
+ }
+ style={{
+ height: Math.min(gradientHeight * 1.5, SNACKBAR_BACKDROP_MAX_HEIGHT),
+ width: "100%",
+ }}
+ >
+
+ */}
+
+ );
+});
diff --git a/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.utils.ts b/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.utils.ts
new file mode 100644
index 000000000..a70af42fe
--- /dev/null
+++ b/components/Snackbar/SnackbarBackdrop/SnackbarBackdrop.utils.ts
@@ -0,0 +1,29 @@
+import {
+ SNACKBARS_MAX_VISIBLE,
+ SNACKBAR_BACKDROP_ADDITIONAL_HEIGHT,
+ SNACKBAR_BOTTOM_OFFSET,
+ SNACKBAR_HEIGHT,
+ SNACKBAR_LARGE_TEXT_HEIGHT,
+ SNACKBAR_SPACE_BETWEEN_SNACKBARS,
+} from "@components/Snackbar/Snackbar.constants";
+import { useSnackbars } from "@components/Snackbar/Snackbar.service";
+
+export const useGradientHeight = () => {
+ const snackbars = useSnackbars();
+
+ if (snackbars.length === 0) {
+ return 0;
+ }
+
+ const gradientHeight = snackbars
+ .slice(0, SNACKBARS_MAX_VISIBLE)
+ .reduce(
+ (acc, snackbar) =>
+ acc +
+ (snackbar.isMultiLine ? SNACKBAR_LARGE_TEXT_HEIGHT : SNACKBAR_HEIGHT) +
+ SNACKBAR_SPACE_BETWEEN_SNACKBARS,
+ SNACKBAR_BACKDROP_ADDITIONAL_HEIGHT + SNACKBAR_BOTTOM_OFFSET
+ );
+
+ return gradientHeight;
+};
diff --git a/components/Snackbar/Snackbars.tsx b/components/Snackbar/Snackbars.tsx
new file mode 100644
index 000000000..22ba9a0f7
--- /dev/null
+++ b/components/Snackbar/Snackbars.tsx
@@ -0,0 +1,34 @@
+import {
+ dismissSnackbar,
+ useSnackbars,
+} from "@components/Snackbar/Snackbar.service";
+import React, { memo } from "react";
+import { Snackbar } from "./Snackbar";
+import { SnackbarBackdrop } from "@components/Snackbar/SnackbarBackdrop/SnackbarBackdrop";
+import { ISnackbar } from "@components/Snackbar/Snackbar.types";
+
+export type InternalSnackbarContextType = {
+ snackbars: ISnackbar[];
+};
+
+export const InternalSnackbarContext =
+ React.createContext({
+ snackbars: [],
+ });
+
+export const Snackbars = memo(function Snackbars() {
+ const snackbars = useSnackbars();
+
+ return (
+ <>
+ {snackbars.map((snackbar) => (
+ dismissSnackbar(snackbar.key)}
+ />
+ ))}
+
+ >
+ );
+});
diff --git a/components/__tests__/__snapshots__/Button.test.tsx.snap b/components/__tests__/__snapshots__/Button.test.tsx.snap
index e7ac5ee86..71f2d47ac 100644
--- a/components/__tests__/__snapshots__/Button.test.tsx.snap
+++ b/components/__tests__/__snapshots__/Button.test.tsx.snap
@@ -66,10 +66,13 @@ exports[`Button Component Default (Android) Button renders correctly with danger
undefined,
[
{
- "0": [Function],
"color": "#FFFFFF",
"flexGrow": 0,
"flexShrink": 1,
+ "fontFamily": "SFProText-Regular",
+ "fontSize": 16,
+ "fontWeight": 400,
+ "lineHeight": 20,
"textAlign": "center",
"zIndex": 2,
},
@@ -169,10 +172,13 @@ exports[`Button Component Default (Android) Button renders correctly with picto
undefined,
[
{
- "0": [Function],
"color": "#FFFFFF",
"flexGrow": 0,
"flexShrink": 1,
+ "fontFamily": "SFProText-Regular",
+ "fontSize": 16,
+ "fontWeight": 400,
+ "lineHeight": 20,
"textAlign": "center",
"zIndex": 2,
},
@@ -254,10 +260,13 @@ exports[`Button Component iOS Button renders correctly with primary variant 1`]
undefined,
[
{
- "0": [Function],
"color": "#FFFFFF",
"flexGrow": 0,
"flexShrink": 1,
+ "fontFamily": "SFProText-Regular",
+ "fontSize": 16,
+ "fontWeight": 400,
+ "lineHeight": 20,
"textAlign": "center",
"zIndex": 2,
},
diff --git a/design-system/BottomSheet/BottomSheet.example.tsx b/design-system/BottomSheet/BottomSheet.example.tsx
new file mode 100644
index 000000000..3020ed0b4
--- /dev/null
+++ b/design-system/BottomSheet/BottomSheet.example.tsx
@@ -0,0 +1,118 @@
+import { useCallback } from "react";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { useAppTheme } from "@theme/useAppTheme";
+import { Button } from "@design-system/Button/Button";
+import { Text } from "@design-system/Text";
+import { VStack } from "../VStack";
+import { useBottomSheetModalRef } from "./BottomSheet.utils";
+import { BottomSheetContentContainer } from "./BottomSheetContentContainer";
+import { BottomSheetFlatList } from "./BottomSheetFlatList";
+import { BottomSheetHeader } from "./BottomSheetHeader";
+import { BottomSheetModal } from "./BottomSheetModal";
+
+export function BottomSheetExample() {
+ const { theme } = useAppTheme();
+ const basicBottomSheetRef = useBottomSheetModalRef();
+ const dynamicBottomSheetRef = useBottomSheetModalRef();
+ const listBottomSheetRef = useBottomSheetModalRef();
+
+ const handleBasicPress = useCallback(() => {
+ basicBottomSheetRef.current?.present();
+ }, [basicBottomSheetRef]);
+
+ const handleDynamicPress = useCallback(() => {
+ dynamicBottomSheetRef.current?.present();
+ }, [dynamicBottomSheetRef]);
+
+ const handleListPress = useCallback(() => {
+ listBottomSheetRef.current?.present();
+ }, [listBottomSheetRef]);
+
+ const data = Array.from({ length: 50 }, (_, i) => ({
+ id: i,
+ title: `Item ${i + 1}`,
+ }));
+
+ const insets = useSafeAreaInsets();
+
+ return (
+
+ {/* Basic Bottom Sheet */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Dynamic Bottom Sheet */}
+
+
+
+
+
+
+ This bottom sheet has many snap points for fine-grained control
+
+
+
+
+
+ {/* Bottom Sheet with List */}
+
+
+
+ item.id.toString()}
+ renderItem={({ item }) => (
+
+ {item.title}
+
+ )}
+ />
+
+
+ );
+}
diff --git a/design-system/BottomSheet/BottomSheetContentContainer.tsx b/design-system/BottomSheet/BottomSheetContentContainer.tsx
index 77a883bce..bde1f3dc8 100644
--- a/design-system/BottomSheet/BottomSheetContentContainer.tsx
+++ b/design-system/BottomSheet/BottomSheetContentContainer.tsx
@@ -2,6 +2,8 @@ import { BottomSheetView as GorhomBottomSheetView } from "@gorhom/bottom-sheet";
import { BottomSheetViewProps } from "@gorhom/bottom-sheet/lib/typescript/components/bottomSheetView/types";
import { memo } from "react";
+// Note: Don't use it when you use a BottomSheetFlatList. It seems to break the list scrolling.
+
export const BottomSheetContentContainer = memo(
function BottomSheetContentContainer(props: BottomSheetViewProps) {
return ;
diff --git a/design-system/Button/Button.example.tsx b/design-system/Button/Button.example.tsx
new file mode 100644
index 000000000..384e93338
--- /dev/null
+++ b/design-system/Button/Button.example.tsx
@@ -0,0 +1,55 @@
+import { VStack } from "@design-system/VStack";
+import { useAppTheme } from "@theme/useAppTheme";
+import { Button } from "./Button";
+
+type IExampleProps = {
+ onPress?: () => void;
+};
+
+export function ButtonExample(args: IExampleProps) {
+ const { onPress } = args;
+
+ const { theme } = useAppTheme();
+
+ return (
+
+ {/* Basic Variants */}
+
+
+
+
+ {/* Sizes */}
+
+ {/* */}
+
+
+ {/* States */}
+
+ {/* */}
+
+ {/* With Icons */}
+
+
+ {/* Custom Accessories */}
+ {/* (
+
+ )}
+ onPress={onPress}
+ /> */}
+
+ {/* i18n Example */}
+ {/* */}
+
+ {/* With Haptic Feedback */}
+
+
+ );
+}
diff --git a/design-system/Button/Button.props.ts b/design-system/Button/Button.props.ts
index e3afe1b8e..b88fedaa0 100644
--- a/design-system/Button/Button.props.ts
+++ b/design-system/Button/Button.props.ts
@@ -19,7 +19,7 @@ export type IButtonVariant =
| "secondary-danger"
| "text";
-export type IButtonSize = "md" | "lg";
+export type IButtonSize = "sm" | "md" | "lg";
export type IButtonAction = "primary" | "danger";
diff --git a/design-system/Button/Button.styles.ts b/design-system/Button/Button.styles.ts
index 3cc17878f..2f2ca092e 100644
--- a/design-system/Button/Button.styles.ts
+++ b/design-system/Button/Button.styles.ts
@@ -1,6 +1,10 @@
import { TextStyle, ViewStyle } from "react-native";
-import { Theme, ThemedStyle } from "../../theme/useAppTheme";
+import {
+ Theme,
+ ThemedStyle,
+ flattenThemedStyles,
+} from "../../theme/useAppTheme";
import { textPresets } from "./../Text/Text.presets";
import { IButtonAction, IButtonSize, IButtonVariant } from "./Button.props";
@@ -43,8 +47,10 @@ export const getButtonViewStyle =
alignItems: "center",
borderRadius: borderRadius.sm,
overflow: "hidden",
- paddingVertical: size === "md" ? spacing.xxs : spacing.xs,
- paddingHorizontal: size === "md" ? spacing.xs : spacing.sm,
+ paddingVertical:
+ size === "md" || size === "sm" ? spacing.xxs : spacing.xs,
+ paddingHorizontal:
+ size === "md" || size === "sm" ? spacing.xs : spacing.sm,
};
if (action === "primary") {
@@ -90,12 +96,23 @@ export const getButtonViewStyle =
};
export const getButtonTextStyle =
- ({ variant, action, pressed = false, disabled = false }: IButtonStyleProps) =>
+ ({
+ size,
+ variant,
+ action,
+ pressed = false,
+ disabled = false,
+ }: IButtonStyleProps) =>
(theme: Theme): TextStyle => {
const { colors } = theme;
const style: TextStyle = {
- ...textPresets.body,
+ // ...(size === "sm" ? textPresets.title.flat(3) : textPresets.body.flat(3)),
+ ...flattenThemedStyles({
+ styles:
+ size === "sm" || size === "md" ? textPresets.small : textPresets.body,
+ theme,
+ }),
textAlign: "center",
flexShrink: 1,
flexGrow: 0,
diff --git a/design-system/Examples.tsx b/design-system/Examples.tsx
new file mode 100644
index 000000000..27ff03145
--- /dev/null
+++ b/design-system/Examples.tsx
@@ -0,0 +1,93 @@
+import { Screen } from "@components/Screen/ScreenComp/Screen";
+import { SnackbarExample } from "@components/Snackbar/Snackbar.example";
+import { BottomSheetExample } from "@design-system/BottomSheet/BottomSheet.example";
+import { Button } from "@design-system/Button/Button";
+import { ButtonExample } from "@design-system/Button/Button.example";
+import { IconExample } from "@design-system/Icon/Icon.example";
+import { IconButtonExample } from "@design-system/IconButton/IconButton.example";
+import { TextExample } from "@design-system/Text/Text.example";
+import { TextFieldExample } from "@design-system/TextField/TextField.example";
+import { VStack } from "@design-system/VStack";
+import { useAppTheme } from "@theme/useAppTheme";
+import { useState } from "react";
+
+type IDesignSystemComponent =
+ | "buttons"
+ | "text"
+ | "icon-button"
+ | "text-field"
+ | "icon"
+ | "header"
+ | "bottom-sheet"
+ | "snackbar";
+
+export function Examples() {
+ const { theme } = useAppTheme();
+
+ const [selectedComponent, setSelectedComponent] =
+ useState(null);
+
+ return (
+
+ {selectedComponent ? (
+
+ setSelectedComponent(null)}
+ variant="link"
+ />
+
+ {selectedComponent === "buttons" && }
+ {selectedComponent === "text" && }
+ {selectedComponent === "icon-button" && }
+ {selectedComponent === "text-field" && }
+ {selectedComponent === "icon" && }
+ {selectedComponent === "snackbar" && }
+ {/* {selectedComponent === "header" && } */}
+ {selectedComponent === "bottom-sheet" && }
+
+
+ ) : (
+
+ setSelectedComponent("buttons")}
+ />
+ setSelectedComponent("text")} />
+ setSelectedComponent("icon-button")}
+ />
+ setSelectedComponent("text-field")}
+ />
+ setSelectedComponent("icon")} />
+ {/* setSelectedComponent("header")}
+ /> */}
+ setSelectedComponent("bottom-sheet")}
+ />
+ setSelectedComponent("snackbar")}
+ />
+
+ )}
+
+ );
+}
diff --git a/design-system/Header/Header.example.tsx b/design-system/Header/Header.example.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/design-system/Icon/Icon.example.tsx b/design-system/Icon/Icon.example.tsx
new file mode 100644
index 000000000..a6a6c1823
--- /dev/null
+++ b/design-system/Icon/Icon.example.tsx
@@ -0,0 +1,64 @@
+import { HStack } from "@design-system/HStack";
+import { Text } from "@design-system/Text";
+import { VStack } from "@design-system/VStack";
+import { useAppTheme } from "@theme/useAppTheme";
+import { ScrollView } from "react-native";
+import { Icon, iconRegistry } from "./Icon";
+
+type IExampleProps = {
+ onPress?: () => void;
+};
+
+export function IconExample(args: IExampleProps) {
+ const { theme } = useAppTheme();
+
+ return (
+
+
+ {/* Different Sizes */}
+ Icon Sizes
+
+
+
+
+
+
+
+ {/* Different Colors */}
+ Icon Colors
+
+
+
+
+
+
+
+ {/* All Available Icons */}
+ All Icons
+
+ {Object.entries(iconRegistry).map(([key, value]) => (
+
+
+ {key}
+
+ ))}
+
+
+
+ );
+}
diff --git a/design-system/Icon/Icon.tsx b/design-system/Icon/Icon.tsx
index 1256e03f6..d95dcc336 100644
--- a/design-system/Icon/Icon.tsx
+++ b/design-system/Icon/Icon.tsx
@@ -64,6 +64,7 @@ export function Icon(props: IIconProps) {
const { theme } = useAppTheme();
const defaultSize = useMemo(() => theme.iconSize.lg, [theme]);
+
const defaultColor = useMemo(() => theme.colors.fill.primary, [theme]);
const {
@@ -79,6 +80,7 @@ export function Icon(props: IIconProps) {
if (!icon && !picto) {
throw new Error("Either 'icon' or 'picto' must be provided");
}
+
if (icon && picto) {
logger.warn(
"Both 'icon' and 'picto' provided, 'icon' will take precedence"
@@ -88,8 +90,8 @@ export function Icon(props: IIconProps) {
const iconName = icon
? iconRegistry[icon]
: picto
- ? iconRegistry[picto]
- : null;
+ ? iconRegistry[picto]
+ : null;
if (!iconName) {
logger.warn(
diff --git a/design-system/Icon/Icon.types.ts b/design-system/Icon/Icon.types.ts
index 5ea669b44..e603cc5ea 100644
--- a/design-system/Icon/Icon.types.ts
+++ b/design-system/Icon/Icon.types.ts
@@ -1,4 +1,5 @@
import { ColorValue, StyleProp, ViewStyle } from "react-native";
+import { RequireExactlyOne } from "../../types/general";
export type IIconName =
| "xmark"
@@ -59,8 +60,16 @@ export type IIconName =
* @example
*
*/
-export type IIconProps = {
- icon?: IIconName;
+export type IIconProps = RequireExactlyOne<{
+ icon: IIconName;
+ /**
+ * @deprecated Use icon instead
+ * @example
+ * Before:
+ * After:
+ */
+ picto: IIconName;
+}> & {
style?: StyleProp;
color?: ColorValue;
/**
@@ -75,11 +84,4 @@ export type IIconProps = {
* @see Use different icon names for different weights instead.
*/
weight?: string;
- /**
- * @deprecated Use icon instead
- * @example
- * Before:
- * After:
- */
- picto?: IIconName;
};
diff --git a/design-system/IconButton/IconButton.example.tsx b/design-system/IconButton/IconButton.example.tsx
index 63eaff88c..6053f54ed 100644
--- a/design-system/IconButton/IconButton.example.tsx
+++ b/design-system/IconButton/IconButton.example.tsx
@@ -1,7 +1,7 @@
import { memo } from "react";
-
import { useAppTheme } from "../../theme/useAppTheme";
import { HStack } from "../HStack";
+import { Text } from "../Text/Text";
import { VStack } from "../VStack";
import { IconButton } from "./IconButton";
@@ -11,35 +11,58 @@ export const IconButtonExample = memo(function IconButtonExample() {
return (
-
- {/* Fill Variant */}
+ {/* Default variants */}
+
+ Default Size
-
-
+
+
+
- {/* Outline Variant */}
+ {/* Large size */}
+
+ Large Size
-
-
+
+
+
- {/* Ghost Variant */}
+ {/* Different actions */}
+
+ Actions
-
+
+
+
+
+
+
+ {/* Disabled state */}
+
+ Disabled
+
+
+
-
+
+
+
+ {/* Common use cases */}
+
+ Common Use Cases
+
+
+
+
+
diff --git a/design-system/IconButton/IconButton.tsx b/design-system/IconButton/IconButton.tsx
index d55883344..fc771bb12 100644
--- a/design-system/IconButton/IconButton.tsx
+++ b/design-system/IconButton/IconButton.tsx
@@ -1,4 +1,3 @@
-// IconButton.tsx
import React, { useCallback } from "react";
import {
GestureResponderEvent,
@@ -6,7 +5,6 @@ import {
StyleProp,
ViewStyle,
} from "react-native";
-
import { useAppTheme } from "../../theme/useAppTheme";
import { Icon } from "../Icon/Icon";
import { Pressable } from "../Pressable";
@@ -78,6 +76,7 @@ export function IconButton(props: IIconButtonProps) {
);
// For now until we fix Icon
+
const iconProps = useCallback(
({ pressed }: PressableStateCallbackType) =>
themed(
diff --git a/design-system/Text/Text.example.tsx b/design-system/Text/Text.example.tsx
new file mode 100644
index 000000000..8f8a97886
--- /dev/null
+++ b/design-system/Text/Text.example.tsx
@@ -0,0 +1,56 @@
+import { memo } from "react";
+import { useAppTheme } from "../../theme/useAppTheme";
+import { VStack } from "../VStack";
+import { Text } from "./Text";
+
+export const TextExample = memo(function TextExample() {
+ const { theme } = useAppTheme();
+
+ return (
+
+ {/* Preset examples */}
+
+ Title Preset
+ Body Preset - Main content text
+ Body Bold Preset
+ Small Preset
+ Smaller Preset
+
+
+ {/* Preset with color overrides */}
+
+
+ Colored Title
+
+
+ Colored Body Text
+
+
+ Warning Text
+
+
+
+ {/* Preset with weight overrides */}
+
+ Big Bold Text
+ Form Label Text
+ Form Helper Text
+
+
+ {/* Common use cases */}
+
+ Product Details
+
+ $99.99
+
+ High-quality product with amazing features
+ In stock - Ships in 2-3 days
+
+
+ );
+});
diff --git a/design-system/Text/Text.props.ts b/design-system/Text/Text.props.ts
index a1f10358c..d9e68fbfa 100644
--- a/design-system/Text/Text.props.ts
+++ b/design-system/Text/Text.props.ts
@@ -1,9 +1,10 @@
import { TextProps as RNTextProps, StyleProp, TextStyle } from "react-native";
+import { IColors } from "@theme/colorsLight";
+import { typography } from "@theme/typography";
+import { i18n, TxKeyPath } from "../../i18n";
import { IPresets } from "./Text.presets";
import { textSizeStyles } from "./Text.styles";
-import { i18n, TxKeyPath } from "../../i18n";
-import { IColors, typography } from "../../theme";
export type ISizes = keyof typeof textSizeStyles;
export type IWeights = keyof typeof typography.primary;
diff --git a/design-system/Text/Text.styles.ts b/design-system/Text/Text.styles.ts
index 840a36fd1..7c722bd03 100644
--- a/design-system/Text/Text.styles.ts
+++ b/design-system/Text/Text.styles.ts
@@ -1,8 +1,8 @@
import { TextStyle } from "react-native";
import { ITextColors, IWeights } from "./Text.props";
-import { typography } from "../../theme";
import { Theme, ThemedStyle } from "../../theme/useAppTheme";
+import { typography } from "@theme/typography";
export const textSizeStyles = {
xl: { fontSize: 32, lineHeight: 36 } satisfies TextStyle, // Made up, need to confirm with Andrew once we have the design
diff --git a/design-system/Text/Text.tsx b/design-system/Text/Text.tsx
index cc4937c6b..0941094a0 100644
--- a/design-system/Text/Text.tsx
+++ b/design-system/Text/Text.tsx
@@ -1,10 +1,10 @@
import React from "react";
import { Text as RNText, StyleProp, TextStyle } from "react-native";
-import { ITextProps } from "./Text.props";
-import { getTextStyle } from "./Text.utils";
import { translate } from "../../i18n";
import { useAppTheme } from "../../theme/useAppTheme";
+import { ITextProps } from "./Text.props";
+import { getTextStyle } from "./Text.utils";
export const Text = React.forwardRef((props, ref) => {
const {
@@ -16,6 +16,7 @@ export const Text = React.forwardRef((props, ref) => {
text,
children,
style: styleProp,
+ preset,
...rest
} = props;
@@ -29,7 +30,7 @@ export const Text = React.forwardRef((props, ref) => {
size,
color,
style: styleProp,
- ...props,
+ preset,
});
return (
diff --git a/design-system/Text/Text.utils.ts b/design-system/Text/Text.utils.ts
index 81782bd6e..de9ce1f2a 100644
--- a/design-system/Text/Text.utils.ts
+++ b/design-system/Text/Text.utils.ts
@@ -1,7 +1,7 @@
import { IThemed } from "@theme/useAppTheme";
import { StyleProp, TextStyle } from "react-native";
-import { IPresets, textPresets } from "./Text.presets";
+import { textPresets } from "./Text.presets";
import { ITextStyleProps } from "./Text.props";
import {
textColorStyle,
@@ -11,15 +11,20 @@ import {
export const getTextStyle = (
themed: IThemed,
- { weight, size, color, style: styleProp, ...props }: ITextStyleProps
+ {
+ weight,
+ size,
+ color,
+ style,
+ preset = "body",
+ }: Pick
): StyleProp => {
- const preset: IPresets = props.preset ?? "body";
const $styles: StyleProp = [
themed(textPresets[preset]),
weight && textFontWeightStyles[weight],
size && textSizeStyles[size],
color && themed((theme) => textColorStyle(theme, color)),
- styleProp,
+ style,
];
return $styles;
diff --git a/design-system/TextField/TextField.example.tsx b/design-system/TextField/TextField.example.tsx
index 76f0e4de5..a44f9dee1 100644
--- a/design-system/TextField/TextField.example.tsx
+++ b/design-system/TextField/TextField.example.tsx
@@ -1,20 +1,20 @@
import { memo } from "react";
-
import { Center } from "../Center";
import { IconButton } from "../IconButton/IconButton";
import { Text } from "../Text/Text";
import { VStack } from "../VStack";
import { TextField } from "./TextField";
import { TextFieldSimple } from "./TextFieldSimple";
+import { useAppTheme } from "@theme/useAppTheme";
export const TextFieldExample = memo(function TextFieldExample() {
+ const { theme } = useAppTheme();
+
return (
{
if (disabled) return;
input.current?.focus();
- }
+ }, [disabled]);
useImperativeHandle(ref, () => input.current as TextInput);
@@ -96,13 +98,16 @@ export const TextField = forwardRef(function TextField(
{!!LeftAccessory && (
{
- pressedInAV.value = withTiming(1, { duration: timing.veryFast });
- }, [pressedInAV]);
+ pressedInAV.value = withTiming(1, { duration: theme.timing.veryFast });
+ }, [pressedInAV, theme]);
const handlePressOut = useCallback(() => {
- pressedInAV.value = withTiming(0, { duration: timing.veryFast });
- }, [pressedInAV]);
+ pressedInAV.value = withTiming(0, { duration: theme.timing.veryFast });
+ }, [pressedInAV, theme]);
return {
pressedInAV,
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index fdc69dafd..15e908ccd 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -277,6 +277,8 @@ PODS:
- ExpoModulesCore
- ExpoKeepAwake (13.0.2):
- ExpoModulesCore
+ - ExpoLinearGradient (13.0.2):
+ - ExpoModulesCore
- ExpoLocalization (15.0.3):
- ExpoModulesCore
- ExpoModulesCore (1.12.23):
@@ -1691,8 +1693,6 @@ PODS:
- React-logger (= 0.74.5)
- React-perflogger (= 0.74.5)
- React-utils (= 0.74.5)
- - ReactNativeAvoidSoftinput (4.0.1):
- - React-Core
- ReactNativeIosContextMenu (2.5.1):
- ContextMenuAuxiliaryPreview (~> 0.3)
- DGSwiftUtilities
@@ -1882,6 +1882,7 @@ DEPENDENCIES:
- ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`)
- ExpoImagePicker (from `../node_modules/expo-image-picker/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
+ - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
- ExpoLocalization (from `../node_modules/expo-localization/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- ExpoSecureStore (from `../node_modules/expo-secure-store/ios`)
@@ -1966,7 +1967,6 @@ DEPENDENCIES:
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- - ReactNativeAvoidSoftinput (from `../node_modules/react-native-avoid-softinput`)
- ReactNativeIosContextMenu (from `../node_modules/react-native-ios-context-menu`)
- ReactNativeIosUtilities (from `../node_modules/react-native-ios-utilities`)
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
@@ -2088,6 +2088,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-image-picker/ios"
ExpoKeepAwake:
:path: "../node_modules/expo-keep-awake/ios"
+ ExpoLinearGradient:
+ :path: "../node_modules/expo-linear-gradient/ios"
ExpoLocalization:
:path: "../node_modules/expo-localization/ios"
ExpoModulesCore:
@@ -2245,8 +2247,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/utils"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
- ReactNativeAvoidSoftinput:
- :path: "../node_modules/react-native-avoid-softinput"
ReactNativeIosContextMenu:
:path: "../node_modules/react-native-ios-context-menu"
ReactNativeIosUtilities:
@@ -2326,6 +2326,7 @@ SPEC CHECKSUMS:
ExpoImageManipulator: aea99205c66043a00a0af90e345395637b9902fa
ExpoImagePicker: 12a420923383ae38dccb069847218f27a3b87816
ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08
+ ExpoLinearGradient: 8cec4a09426d8934c433e83cb36262d72c667fce
ExpoLocalization: f04eeec2e35bed01ab61c72ee1768ec04d093d01
ExpoModulesCore: e3c518e094990b3ad960edbd19c291f2a1b56b0a
ExpoSecureStore: 060cebcb956b80ddae09821610ac1aa9e1ac74cd
@@ -2417,7 +2418,6 @@ SPEC CHECKSUMS:
React-runtimescheduler: db7189185a2e5912b0d17194302e501f801a381e
React-utils: 3f1fcffc14893afb9a7e5b7c736353873cc5fc95
ReactCommon: f79ae672224dc1e6c2d932062176883c98eebd57
- ReactNativeAvoidSoftinput: a096aae8faf85c0f3225c2179bccdab00b8b6354
ReactNativeIosContextMenu: e5f972174bd78ab3a552bd6ee4745762ffaa42b3
ReactNativeIosUtilities: 8ea45df073a05d24b9fd75e4ec5fe1de19051466
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
@@ -2449,7 +2449,7 @@ SPEC CHECKSUMS:
web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959
XMTP: 09faa347569b092005997364f7fe787ccc33f3d5
XMTPReactNative: 0c92d55c576ac6ff0775357fa60e90ea8e73afc2
- Yoga: 1ab23c1835475da69cf14e211a560e73aab24cb0
+ Yoga: 33622183a85805e12703cd618b2c16bfd18bfffb
PODFILE CHECKSUM: 137cb0cd2dafbfb3e5b9343435f9db8e28690806
diff --git a/package.json b/package.json
index bc03d98fd..5f1514ccb 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"lint": "eslint --max-warnings 2500",
"lint:errors": "eslint --quiet",
"ios": "EXPO_ENV=dev expo start --ios",
+ "run:ios": "EXPO_ENV=dev expo run:ios",
"postinstall": "patch-package && node scripts/wasm.js && husky install && cross-os postinstall",
"start": "EXPO_ENV=dev expo start",
"start:preview": "EXPO_ENV=preview expo start",
@@ -116,6 +117,7 @@
"expo-image": "~1.12.15",
"expo-image-manipulator": "~12.0.5",
"expo-image-picker": "~15.0.7",
+ "expo-linear-gradient": "~13.0.2",
"expo-linking": "~6.3.1",
"expo-localization": "^15.0.3",
"expo-navigation-bar": "~3.0.7",
diff --git a/screens/Main.tsx b/screens/Main.tsx
index 0475572b5..f4607af12 100644
--- a/screens/Main.tsx
+++ b/screens/Main.tsx
@@ -112,6 +112,12 @@ const NavigationContent = () => {
useSelect(["notificationsPermissionStatus", "splashScreenHidden"])
);
+ // return (
+ //
+ //
+ //
+ // );
+
if (!splashScreenHidden) {
// TODO: Add a loading screen
return null;
diff --git a/screens/NewAccount/NewAccountPrivateKeyScreen.tsx b/screens/NewAccount/NewAccountPrivateKeyScreen.tsx
index 95e8fc864..07594b4fd 100644
--- a/screens/NewAccount/NewAccountPrivateKeyScreen.tsx
+++ b/screens/NewAccount/NewAccountPrivateKeyScreen.tsx
@@ -7,15 +7,17 @@ import { Button } from "../../design-system/Button/Button";
import { VStack } from "../../design-system/VStack";
import { translate } from "../../i18n";
import { useRouter } from "../../navigation/useNavigation";
-import { spacing } from "../../theme";
import { sentryTrackError } from "../../utils/sentry";
import { isMissingConverseProfile } from "../Onboarding/Onboarding.utils";
import {
PrivateKeyInput,
useLoginWithPrivateKey,
} from "../Onboarding/OnboardingPrivateKeyScreen";
+import { useAppTheme } from "@theme/useAppTheme";
export const NewAccountPrivateKeyScreen = memo(function () {
+ const { theme } = useAppTheme();
+
const { loading, loginWithPrivateKey } = useLoginWithPrivateKey();
const [privateKey, setPrivateKey] = useState("");
@@ -48,11 +50,11 @@ export const NewAccountPrivateKeyScreen = memo(function () {
-
+
diff --git a/screens/NewAccount/NewAccountUserProfileScreen.tsx b/screens/NewAccount/NewAccountUserProfileScreen.tsx
index 8905018bc..b0bd060e9 100644
--- a/screens/NewAccount/NewAccountUserProfileScreen.tsx
+++ b/screens/NewAccount/NewAccountUserProfileScreen.tsx
@@ -2,6 +2,7 @@ import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { memo, useCallback, useRef } from "react";
import { TextInput, View, useColorScheme } from "react-native";
+import { useAppTheme } from "@theme/useAppTheme";
import Avatar from "../../components/Avatar";
import Button from "../../components/Button/Button";
import { NewAccountScreenComp } from "../../components/NewAccount/NewAccountScreenComp";
@@ -11,7 +12,6 @@ import { Text } from "../../design-system/Text";
import { VStack } from "../../design-system/VStack";
import { translate } from "../../i18n";
import { textSecondaryColor } from "../../styles/colors";
-import { spacing } from "../../theme";
import { sentryTrackError } from "../../utils/sentry";
import { NavigationParamList } from "../Navigation/Navigation";
import {
@@ -27,6 +27,8 @@ export const NewAccountUserProfileScreen = memo(
) {
const { navigation } = props;
+ const { theme } = useAppTheme();
+
const colorScheme = useColorScheme();
const { profile, setProfile } = useProfile();
@@ -118,7 +120,7 @@ export const NewAccountUserProfileScreen = memo(
diff --git a/screens/Onboarding/OnboardingNotificationsScreen.tsx b/screens/Onboarding/OnboardingNotificationsScreen.tsx
index ef6ff373e..168462d0f 100644
--- a/screens/Onboarding/OnboardingNotificationsScreen.tsx
+++ b/screens/Onboarding/OnboardingNotificationsScreen.tsx
@@ -4,6 +4,7 @@ import * as Linking from "expo-linking";
import React from "react";
import { Platform } from "react-native";
+import { useAppTheme } from "@theme/useAppTheme";
import Button from "../../components/Button/Button";
import { OnboardingPictoTitleSubtitle } from "../../components/Onboarding/OnboardingPictoTitleSubtitle";
import { OnboardingPrimaryCtaButton } from "../../components/Onboarding/OnboardingPrimaryCtaButton";
@@ -12,7 +13,6 @@ import { useSettingsStore } from "../../data/store/accountsStore";
import { useAppStore } from "../../data/store/appStore";
import { setAuthStatus } from "../../data/store/authStore";
import { VStack } from "../../design-system/VStack";
-import { spacing } from "../../theme";
import { requestPushNotificationsPermissions } from "../../utils/notifications";
import { sentryTrackError } from "../../utils/sentry";
import { NavigationParamList } from "../Navigation/Navigation";
@@ -20,6 +20,8 @@ import { NavigationParamList } from "../Navigation/Navigation";
export function OnboardingNotificationsScreen(
props: NativeStackScreenProps
) {
+ const { theme } = useAppTheme();
+
const setNotificationsSettings = useSettingsStore(
(s) => s.setNotificationsSettings
);
@@ -56,7 +58,7 @@ export function OnboardingNotificationsScreen(
{
const { navigation } = props;
+ const { theme } = useAppTheme();
+
const colorScheme = useColorScheme();
const { profile, setProfile } = useProfile();
@@ -165,7 +167,7 @@ export const OnboardingUserProfileScreen = (
diff --git a/screens/ShareProfile.tsx b/screens/ShareProfile.tsx
index 7759116b7..69b3209e6 100644
--- a/screens/ShareProfile.tsx
+++ b/screens/ShareProfile.tsx
@@ -18,6 +18,7 @@ import {
} from "react-native";
import QRCode from "react-native-qrcode-svg";
+import { useAppTheme } from "@theme/useAppTheme";
import AndroidBackAction from "../components/AndroidBackAction";
import Avatar from "../components/Avatar";
import Button from "../components/Button/Button";
@@ -29,7 +30,6 @@ import {
useCurrentAccount,
useProfilesStore,
} from "../data/store/accountsStore";
-import { spacing } from "../theme";
import {
getPreferredAvatar,
getPreferredName,
@@ -195,6 +195,8 @@ export default function ShareProfileScreen({
const useStyles = () => {
const colorScheme = useColorScheme();
+ const { theme } = useAppTheme();
+
return StyleSheet.create({
shareProfile: {
flex: 1,
@@ -245,7 +247,7 @@ const useStyles = () => {
flex: 1,
justifyContent: "flex-end",
alignItems: "center",
- paddingHorizontal: spacing.lg,
+ paddingHorizontal: theme.spacing.lg,
},
shareButtonContainerCompact: {
flex: 0,
diff --git a/theme/animations.ts b/theme/animations.ts
index f6d079491..07ecf3d17 100644
--- a/theme/animations.ts
+++ b/theme/animations.ts
@@ -1,6 +1,7 @@
import { Easing, FadeInDown, FadeInUp } from "react-native-reanimated";
import { timing } from "./timing";
+import { SpringConfig } from "react-native-reanimated/lib/typescript/reanimated2/animation/springUtils";
export const SICK_EASE_OUT = Easing.out(Easing.cubic);
@@ -8,12 +9,45 @@ export const SICK_DAMPING = 80;
export const SICK_STIFFNESS = 200;
+export const SICK_SPRING_CONFIG: SpringConfig = {
+ damping: SICK_DAMPING,
+ stiffness: SICK_STIFFNESS,
+};
+
+const easings = {
+ // Ease In
+ easeInQuad: [0.55, 0.085, 0.68, 0.53],
+ easeInCubic: [0.55, 0.055, 0.675, 0.19],
+ easeInQuart: [0.895, 0.03, 0.685, 0.22],
+ easeInQuint: [0.755, 0.05, 0.855, 0.06],
+ easeInExpo: [0.95, 0.05, 0.795, 0.035],
+ easeInCirc: [0.6, 0.04, 0.98, 0.335],
+
+ // Ease Out
+ easeOutQuad: [0.25, 0.46, 0.45, 0.94],
+ easeOutCubic: [0.215, 0.61, 0.355, 1],
+ easeOutQuart: [0.165, 0.84, 0.44, 1],
+ easeOutQuint: [0.23, 1, 0.32, 1],
+ easeOutExpo: [0.19, 1, 0.22, 1],
+ easeOutCirc: [0.075, 0.82, 0.165, 1],
+
+ // Ease In Out
+ easeInOutQuad: [0.455, 0.03, 0.515, 0.955],
+ easeInOutCubic: [0.645, 0.045, 0.355, 1],
+ easeInOutQuart: [0.77, 0, 0.175, 1],
+ easeInOutQuint: [0.86, 0, 0.07, 1],
+ easeInOutExpo: [1, 0, 0, 1],
+ easeInOutCirc: [0.785, 0.135, 0.15, 0.86],
+};
+
export const animations = {
spring: {
damping: SICK_DAMPING,
stiffness: SICK_STIFFNESS,
},
+ easings,
+
fadeInDownSpring: () =>
FadeInDown.easing(SICK_EASE_OUT)
.stiffness(SICK_STIFFNESS)
diff --git a/theme/index.ts b/theme/index.ts
deleted file mode 100644
index 25eafc28f..000000000
--- a/theme/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from "./colorsLight";
-export * from "./spacing";
-export * from "./typography";
-export * from "./timing";
diff --git a/theme/shadow.ts b/theme/shadow.ts
new file mode 100644
index 000000000..61885e051
--- /dev/null
+++ b/theme/shadow.ts
@@ -0,0 +1,15 @@
+import { darkPalette } from "@theme/palette";
+
+export const shadow = {
+ big: {
+ shadowColor: darkPalette.alpha15,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 1,
+ shadowRadius: 16,
+ elevation: 16,
+ },
+};
+
+export type IShadow = typeof shadow;
+
+export type IShadowKey = keyof typeof shadow;
diff --git a/theme/useAppTheme.ts b/theme/useAppTheme.ts
index 63d5fb116..eabe153cc 100644
--- a/theme/useAppTheme.ts
+++ b/theme/useAppTheme.ts
@@ -14,6 +14,7 @@ import {
} from "react";
import { StyleProp, useColorScheme } from "react-native";
+import { IShadow, shadow } from "@theme/shadow";
import { IAvatarSize, avatarSize } from "./avatar";
import { IBorderRadius, borderRadius } from "./border-radius";
import { IBorderWidth, borderWidth } from "./borders";
@@ -36,6 +37,7 @@ export interface Theme {
iconSize: IIconSize;
typography: ITypography;
timing: Timing;
+ shadow: IShadow;
isDark: boolean;
}
@@ -49,6 +51,7 @@ export const lightTheme: Theme = {
avatarSize,
iconSize,
timing,
+ shadow,
isDark: false,
};
export const darkTheme: Theme = {
@@ -60,6 +63,7 @@ export const darkTheme: Theme = {
avatarSize,
iconSize,
timing,
+ shadow,
isDark: true,
};
@@ -205,3 +209,20 @@ export const useAppTheme = (): UseAppThemeValue => {
themed,
};
};
+
+export function flattenThemedStyles(args: {
+ styles: ThemedStyle | StyleProp | ThemedStyleArray;
+ theme: Theme;
+}): T {
+ const { styles, theme } = args;
+ const flatStyles = [styles].flat(3);
+
+ const processedStyles = flatStyles.map((style) => {
+ if (typeof style === "function") {
+ return (style as ThemedStyle)(theme);
+ }
+ return style;
+ });
+
+ return Object.assign({}, ...processedStyles);
+}
diff --git a/types/general.ts b/types/general.ts
index 7fc5e3ac8..62e77f7cb 100644
--- a/types/general.ts
+++ b/types/general.ts
@@ -74,3 +74,8 @@ export type MyReactNode =
| (() => ReactElement)
| number
| string;
+
+export type RequireExactlyOne = {
+ [K in Keys]: Required> & { [P in Exclude]?: undefined };
+}[Keys] &
+ Omit;
diff --git a/yarn.lock b/yarn.lock
index 44a6b7b50..8381ea5e4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12066,6 +12066,11 @@ expo-keep-awake@~13.0.2:
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-13.0.2.tgz#5ef31311a339671eec9921b934fdd90ab9652b0e"
integrity sha512-kKiwkVg/bY0AJ5q1Pxnm/GvpeB6hbNJhcFsoOWDh2NlpibhCLaHL826KHUM+WsnJRbVRxJ+K9vbPRHEMvFpVyw==
+expo-linear-gradient@~13.0.2:
+ version "13.0.2"
+ resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-13.0.2.tgz#21bd7bc7c71ef4f7c089521daa16db729d2aec5f"
+ integrity sha512-EDcILUjRKu4P1rtWcwciN6CSyGtH7Bq4ll3oTRV7h3h8oSzSilH1g6z7kTAMlacPBKvMnkkWOGzW6KtgMKEiTg==
+
expo-linking@~6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-6.3.1.tgz#05aef8a42bd310391d0b00644be40d80ece038d9"