Skip to content

Commit

Permalink
Merge pull request #535 from Peersyst/feat/main-button
Browse files Browse the repository at this point in the history
Main Bottom Bar button
AgustinMJ authored Oct 14, 2024
2 parents 580d4c9 + 972f243 commit 360347a
Showing 16 changed files with 369 additions and 61 deletions.
4 changes: 3 additions & 1 deletion src/locale/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -353,5 +353,7 @@
"URL": "URL",
"add": "Add",
"addToFavourites": "Add to favourites",
"removeFavourites": "Remove from favourites"
"removeFavourites": "Remove from favourites",
"scanQR": "Scan QR",
"actions": "Actions"
}
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import "@peersyst/react-native-components";
declare module "@peersyst/react-native-components" {
export interface ButtonVariantOverrides {
primary: true;
primarySoft: true;
secondary: true;
tertiary: true;
quaternary: true;
9 changes: 8 additions & 1 deletion src/module/common/component/input/Button/Button.styles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button } from "@peersyst/react-native-components";
import styled from "@peersyst/react-native-styled";
import { ButtonProps } from "./Button.types";
import { emphasize } from "@peersyst/react-utils";
import { alpha, emphasize } from "@peersyst/react-utils";

export const ButtonRoot = styled(Button)<ButtonProps>(({ theme, rounded = true }) => {
const themeMode = theme.palette.mode;
@@ -29,6 +29,10 @@ export const ButtonRoot = styled(Button)<ButtonProps>(({ theme, rounded = true }
backgroundColor: theme.palette.primary,
color: "#FFFFFF",
},
primarySoft: {
backgroundColor: alpha(theme.palette.primary, 0.12),
color: theme.palette.primary,
},
secondary: {
backgroundColor: "#FFFFFF",
color: "#000000",
@@ -71,6 +75,9 @@ export const ButtonRoot = styled(Button)<ButtonProps>(({ theme, rounded = true }
primary: {
backgroundColor: emphasize(theme.palette.primary, 0.15),
},
primarySoft: {
backgroundColor: emphasize(alpha(theme.palette.primary, 0.12), 0.3),
},
secondary: {
backgroundColor: emphasize("#FFFFFF", 0.02),
},
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { DatabaseIcon } from "module/common/icons/DatabaseIcon";
import useTranslate from "module/common/hook/useTranslate";
import { capitalize } from "@peersyst/react-utils";
import { config } from "config";
import BottomBarQRScanner from "./BottomBarQRScanner/BottomBarQRScanner";
import QuickActionsBottomBarItem from "./QuickActionsBottomBarItem/QuickActionsBottomBarItem";
import { MainScreens } from "../MainNavigatorGroup/MainScreens";

type BottomBarProps = Pick<BottomTabBarProps, "state" | "navigation">;
@@ -35,7 +35,7 @@ const BottomBar = ({ state, navigation }: BottomBarProps): JSX.Element => {
label={capitalize(translate("staking"))}
Icon={<DatabaseIcon />}
/>
{config.signerFeature.enabled && <BottomBarQRScanner />}
<QuickActionsBottomBarItem />
{config.signerFeature.enabled && (
<BottomBarItem
onPress={() => handleNavigation(MainScreens.DAPPS)}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import styled from "@peersyst/react-native-styled";
import Button from "module/common/component/input/Button/Button";
import { Animated, Pressable } from "react-native";
import { classify } from "@peersyst/react-utils";
import { LinearGradient } from "expo-linear-gradient";

export const MainBottomBarItemRoot = styled(Button)(() => ({
lg: {
width: 70,
height: 70,
},
export const MainBottomBarItemRoot = styled(Pressable)(() => ({
width: 64,
height: 64,
borderRadius: 10000,
}));

export const MainBottomBarItemBackgroundGradiend = styled(Animated.createAnimatedComponent(classify(LinearGradient)))(() => ({
position: "absolute",
top: 0,
left: 0,
zIndex: -1,
elevation: -1,
width: "100%",
height: "100%",
borderRadius: 10000,
}));
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
import { ButtonProps } from "module/common/component/input/Button/Button.types";
import { MainBottomBarItemRoot } from "./MainBottomBarItem.styles";
import { Col, SvgIconProps } from "@peersyst/react-native-components";
import Typography from "module/common/component/display/Typography/Typography";
import { JSXElementConstructor } from "react";
import { MainBottomBarItemBackgroundGradiend, MainBottomBarItemRoot } from "./MainBottomBarItem.styles";
import { Col } from "@peersyst/react-native-components";
import { useEffect, useRef } from "react";
import useThemeMode from "module/common/hook/useThemeMode";
import { theme } from "config/theme/theme";
import useWalletGradient from "module/wallet/hook/useWalletGradient";
import { Animated, ViewStyle } from "react-native";
import { NearIcon } from "icons";

export interface MainBottomBarItemProps extends ButtonProps {
Icon: JSXElementConstructor<SvgIconProps>;
label?: string;
export interface MainBottomBarItemProps {
onPress: () => void;
style?: ViewStyle;
}

const MainBottomBarItem = ({ Icon, label, ...rest }: MainBottomBarItemProps): JSX.Element => {
const MainBottomBarItem = (props: MainBottomBarItemProps): JSX.Element => {
const mode = useThemeMode();

const textColor = mode === "light" ? theme.palette.background : theme.palette.text;

const walletGradient = useWalletGradient();

const gradientAnim = useRef(new Animated.Value(1)).current;

useEffect(() => {
Animated.timing(gradientAnim, {
toValue: 0,
duration: 300,
useNativeDriver: false,
}).start();
}, []);

const backgroundColor = walletGradient[0];
const secondaryBackgroundColor = walletGradient[1];

return (
<MainBottomBarItemRoot variant="contrast" {...rest}>
<MainBottomBarItemRoot {...props}>
<Col flex={1} justifyContent="center" alignItems="center">
<Icon style={{ color: textColor }} />
{label && (
<Typography variant="body4Strong" style={{ color: textColor }}>
{label}
</Typography>
)}
<NearIcon style={{ color: textColor, fontSize: 28 }} />
</Col>
<MainBottomBarItemBackgroundGradiend
colors={[backgroundColor, secondaryBackgroundColor]}
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
style={{
transform: [{ translateX: gradientAnim }],
}}
/>
</MainBottomBarItemRoot>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import MainBottomBarItem from "../MainBottomBarItem/MainBottomBarItem";
import QuickActionsModal from "module/home/component/feedback/QuickActionsModal";
import { useQuickActionsBottomBarItem } from "./hooks/useQuickActionsBottomBarItem";

const QuickActionsBottomBarItem = (): JSX.Element => {
const { modals, actions, open, showModal, hideModal } = useQuickActionsBottomBarItem();

return (
<>
{modals}
<MainBottomBarItem style={{ marginTop: -10 }} onPress={showModal} />
<QuickActionsModal quickActions={actions} open={open} onClose={hideModal} />
</>
);
};

export default QuickActionsBottomBarItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import useNavigation from "module/common/hook/useNavigation";
import { MainScreens } from "../../../MainNavigatorGroup/MainScreens";

export interface UseBuyQuickActionReturn {
handleBuyPress: () => void;
}

export function useBuyQuickAction(): UseBuyQuickActionReturn {
const navigate = useNavigation();

function handleBuyPress() {
navigate.navigate(MainScreens.FIAT_ORDERS);
}

return {
handleBuyPress,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { capitalize } from "@peersyst/react-utils";
import { ResourceKey } from "i18next";
import { ArrowSendIcon, ArrowReceiveIcon, SwapIcon, BuyIcon } from "icons";
import { useModalState } from "module/common/hook/useModalState";
import useTranslate from "module/common/hook/useTranslate";
import { QRCodeIcon } from "module/common/icons/QRCodeIcon";
import { QuickAction } from "module/home/component/feedback/QuickActionsModal.types";
import ReceiveModal from "module/transaction/component/core/ReceiveModal/ReceiveModal";
import SendModal from "module/transaction/component/core/SendModal/SendModal";
import { Fragment } from "react";
import QrScanner from "module/common/component/input/QrScanner/QrScanner";
import { useScanQuickAction } from "./useScanQuickAction";
import { useSwapQuickAction } from "./useSwapQuickAction";
import { useBuyQuickAction } from "./useBuyQuickAction";
import { useConfig } from "@peersyst/react-native-components";

export interface UseQuickActionsBottomBarItemReturn {
actions: QuickAction[];
modals: JSX.Element;
open: boolean;
showModal: () => void;
hideModal: () => void;
}

export function useQuickActionsBottomBarItem(): UseQuickActionsBottomBarItemReturn {
const { open, showModal, hideModal } = useModalState();
const translate = useTranslate();
const signerFeatureConfig = useConfig("signerFeature");
const { handleSwapPress } = useSwapQuickAction();
const { handleBuyPress } = useBuyQuickAction();
const { scanOpened, showScanModal, handleScan, hideScanModal } = useScanQuickAction();
const { open: sendOpened, showModal: showSendModal, hideModal: hideSendModal } = useModalState();
const { open: receiveOpened, showModal: showReceiveModal, hideModal: hideReceiveModal } = useModalState();

function getCapitalizedTranslation(key: ResourceKey): string {
return capitalize(translate(key));
}

function withHandleOnPress(callback: () => void) {
return () => {
hideModal();
callback();
};
}

const actions: QuickAction[] = [
{
Icon: QRCodeIcon,
label: getCapitalizedTranslation("scanQR"),
onPress: withHandleOnPress(showScanModal),
variant: "primary",
},
{
Icon: ArrowSendIcon,
label: getCapitalizedTranslation("send"),
onPress: withHandleOnPress(showSendModal),
variant: "soft",
},
{
Icon: ArrowReceiveIcon,
label: getCapitalizedTranslation("receive"),
onPress: withHandleOnPress(showReceiveModal),
variant: "soft",
},
...(signerFeatureConfig.enabled
? [
{
Icon: SwapIcon,
label: getCapitalizedTranslation("swap"),
onPress: withHandleOnPress(handleSwapPress),
variant: "soft",
} as const,
]
: []),
{
Icon: BuyIcon,
label: getCapitalizedTranslation("buy"),
onPress: withHandleOnPress(handleBuyPress),
variant: "soft",
},
];

return {
actions,
modals: (
<Fragment>
<QrScanner open={scanOpened} onClose={hideScanModal} onScan={({ data }) => handleScan(data)} />
<SendModal open={sendOpened} onClose={hideSendModal} />
<ReceiveModal open={receiveOpened} onClose={hideReceiveModal} />
</Fragment>
),
open,
showModal,
hideModal,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useModalState } from "module/common/hook/useModalState";
import useTranslate from "module/common/hook/useTranslate";
import { parseSignerDeepLinkData } from "module/signer/utils/parseSignerDeepLinkData";
import useSignerModal from "module/signer/hooks/useSignerModal";
import { useToast } from "@peersyst/react-native-components";

export interface UseScanQuickActionReturn {
handleScan: (data: string) => void;
scanOpened: boolean;
showScanModal: () => void;
hideScanModal: () => void;
}

export function useScanQuickAction(): UseScanQuickActionReturn {
const translateError = useTranslate("error");
const { showSignerModal } = useSignerModal();
const { showToast } = useToast();
const { open: scanOpened, showModal: showScanModal, hideModal: hideScanModal } = useModalState();

const handleScan = (data: string) => {
hideScanModal();
const signerData = parseSignerDeepLinkData(data);
if (!signerData) showToast(translateError("invalidSignerRequest"), { type: "error" });
else showSignerModal(signerData!.type, signerData!.id);
};

return {
handleScan,
scanOpened,
showScanModal,
hideScanModal,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import useGetSwapLink from "module/common/hook/useGetSwapLink";
import useNavigation from "module/common/hook/useNavigation";
import { MainScreens } from "../../../MainNavigatorGroup/MainScreens";
import { DAppScreens } from "module/dapp/navigator/DAppsNavigator.types";

export interface UseSwapQuickActionReturn {
handleSwapPress: () => void;
}

export function useSwapQuickAction(): UseSwapQuickActionReturn {
const navigate = useNavigation();
const uriSwap = useGetSwapLink();

function handleSwapPress(): void {
// We need to cast the params of the navigate.navigate to any because the React Navigation types are not working properly
navigate.navigate(MainScreens.DAPPS, { screen: DAppScreens.WEBVIEW, params: { url: uriSwap } } as any);
}

return {
handleSwapPress,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ElementStyler } from "@peersyst/react-native-components";
import styled from "@peersyst/react-native-styled";
import { View } from "react-native";

export const QuickActionsButtonPrimaryIcon = styled(ElementStyler)(({ theme }) => ({
color: theme.palette.white,
fontSize: 24,
}));

export const QuickActionsButtonPrimaryIconPositioner = styled(View)(() => ({
position: "absolute",
left: 0,
zIndex: 2,
}));

export const QuickActionsButtonSoftIcon = styled(ElementStyler)(({ theme }) => ({
color: theme.palette.primary,
fontSize: 24,
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Button from "module/common/component/input/Button/Button";
import { QuickAction } from "../QuickActionsModal.types";
import Typography from "module/common/component/display/Typography/Typography";
import { Row } from "@peersyst/react-native-components";
import {
QuickActionsButtonPrimaryIcon,
QuickActionsButtonPrimaryIconPositioner,
QuickActionsButtonSoftIcon,
} from "./QuickActionButton.styles";

export interface QuickActionButtonProps {
quickAction: QuickAction;
}

export function QuickActionButton({ quickAction: { variant, Icon, label, ...rest } }: QuickActionButtonProps): JSX.Element {
if (variant === "primary") {
return (
<Button variant="primary" {...rest}>
<Row flex={1} justifyContent="center">
<QuickActionsButtonPrimaryIconPositioner>
<QuickActionsButtonPrimaryIcon>
<Icon />
</QuickActionsButtonPrimaryIcon>
</QuickActionsButtonPrimaryIconPositioner>
<Typography variant="body2Strong" color="white">
{label}
</Typography>
</Row>
</Button>
);
} else {
return (
<Button variant="primarySoft" {...rest}>
<Row flex={1} justifyContent="center" gap={10}>
<QuickActionsButtonSoftIcon>
<Icon />
</QuickActionsButtonSoftIcon>
<Typography variant="body2Strong" color="primary">
{label}
</Typography>
</Row>
</Button>
);
}
}
43 changes: 43 additions & 0 deletions src/module/home/component/feedback/QuickActionsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import CardModal from "module/common/component/feedback/CardModal/CardModal";
import { CardModalProps } from "module/common/component/feedback/CardModal/CardModal.types";
import ModalHeader from "module/common/component/navigation/ModalHeader/ModalHeader";
import useTranslate from "module/common/hook/useTranslate";
import { QuickAction } from "./QuickActionsModal.types";
import { Col } from "@peersyst/react-native-components";
import { QuickActionButton } from "./QuickActionButton/QuickActionButton";

export type QuickActionsModalProps = Omit<CardModalProps, "children"> & {
quickActions: QuickAction[];
};

const QuickActionsModal = ({ onClose, quickActions, ...rest }: QuickActionsModalProps): JSX.Element => {
const translate = useTranslate();

return (
<>
<CardModal {...rest} onClose={onClose}>
{(open, setOpen) => ({
header: (
<ModalHeader
title={translate("actions").toUpperCase()}
dismissal="close"
onDismiss={() => {
setOpen(false);
if (open !== undefined) onClose?.();
}}
/>
),
body: (
<Col gap={12}>
{quickActions.map((quickAction, index) => (
<QuickActionButton key={index} quickAction={quickAction} />
))}
</Col>
),
})}
</CardModal>
</>
);
};

export default QuickActionsModal;
9 changes: 9 additions & 0 deletions src/module/home/component/feedback/QuickActionsModal.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SvgIconProps } from "@peersyst/react-native-components";
import { JSXElementConstructor } from "react";

export interface QuickAction {
label: string;
Icon: JSXElementConstructor<SvgIconProps>;
onPress: () => void;
variant: "primary" | "soft";
}

0 comments on commit 360347a

Please sign in to comment.