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

refactor: quick transfer UI, profile tabs rendering, profile transfer modal #502

Merged
merged 11 commits into from
Jan 29, 2025
Merged
54 changes: 54 additions & 0 deletions src/sections/finance/components/finance-display-profile-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import {FC} from "react";
import {Profile} from "@lens-protocol/api-bindings";
import {truncateAddress} from "@src/utils/wallet.ts";

interface FinanceDisplayNameProps {
mode: 'profile' | 'wallet';
initialList?: Profile[];
carousel: any;
}

/**
* FinanceDisplayName is a functional component responsible for rendering the display name
* of a selected profile from a provided list. If no list is provided or the list is empty,
* the component returns null. Otherwise, it displays the name of the currently selected
* profile in a styled container.
*
* Props:
* @param {Object} initialList - Array of profiles containing details such as the local name.
* @param {Object} carousel - Object containing the current index used to determine the selected profile.
* @param {string} mode - Determines whether to display the profile name or wallet address.
*
* Behavior:
* - If initialList is empty or null, the component does not render anything.
* - It selects a profile based on the carousel's currentIndex and renders the localName of that profile.
* - If no profile is selected, it falls back to a default message ('No profile selected').
*/
const FinanceDisplayProfileInfo: FC<FinanceDisplayNameProps> = ({initialList, carousel, mode}) => {
// If the initial list is empty, return
if (!initialList?.length) {
return null;
}

const selectedProfile = initialList?.[carousel.currentIndex];
return (
<Box sx={{ textAlign: 'center',mt:-2, mb: 1 }}>
{
mode === 'profile' ?
(<Box component="span" sx={{ flexGrow: 1, typography: 'subtitle1' }}>
{selectedProfile?.metadata?.displayName ?? 'No profile selected'}
</Box>) : null
}
{
mode === 'wallet' ?
<Typography variant="body2" color="text.secondary">
{truncateAddress(selectedProfile?.ownedBy?.address)}
</Typography> : null
}
</Box>
);
}

export default FinanceDisplayProfileInfo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";

/**
* FinanceNoFollowingsQuickTransfer is a React functional component that serves as a user interface element
* to guide users when there are no followings available for quick transfers.
*
* Features:
* - Displays a message prompting the user to follow someone in order to make transfers.
* - Includes a visual divider with an "OR" indicator for alternate actions.
* - Provides instructions to perform a search to initiate a transfer.
*
* Styling:
* - The component utilizes a centrally aligned design with spacing between elements.
* - A dashed line and a circular indicator are used as visual cues.
* - Background color and opacity settings enhance readability and focus.
*/
const FinanceNoFollowingsQuickTransfer = () => {
return (
<Box sx={{ textAlign: 'center', mb: 3, backgroundColor: 'rgba(0,0,0,0.1)', p: 1, borderRadius: 1 }}>
<Stack direction="column" alignItems="center" spacing={1}>
<Typography variant="body2" color="text.primary">
Here appear your followings. Follow one to transfer.
</Typography>
<Box sx={{ opacity:0.7, position: 'relative', display: 'flex', alignItems: 'center', my: 2 }}>
<Box
sx={{
position: 'absolute',
top: '50%',
transform: 'translateX(-50%)',
width: 200,
height: 25,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderTop: '1px dashed',
borderColor: 'text.secondary',
}} />
<Box
sx={{
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: 'rgb(44,46,51)',
borderRadius: '50%',
width: 30,
height: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px dashed',
borderColor: 'text.secondary',
}}
>
<Typography color="text.secondary" sx={{ fontSize: 12 }}>
OR
</Typography>
</Box>
</Box>
<Typography variant="body2" color="text.primary">
Perform a search to start a transfer.
</Typography>
</Stack>
</Box>
);
}

export default FinanceNoFollowingsQuickTransfer;
46 changes: 36 additions & 10 deletions src/sections/finance/components/finance-quick-transfer-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import { useSelector } from 'react-redux';

// MUI components
Expand All @@ -21,7 +21,7 @@ import LoadingButton from '@mui/lab/LoadingButton';
import { supabase } from '@src/utils/supabase';
import { useNotificationPayload } from '@src/hooks/use-notification-payload.ts';
import { useNotifications } from '@src/hooks/use-notifications.ts';
import { InputAmountProps } from '@src/components/input-amount.tsx';
import {InputAmount, InputAmountProps} from '@src/components/input-amount.tsx';
import { useTransfer } from '@src/hooks/use-transfer.ts';

// Notifications
Expand All @@ -30,6 +30,8 @@ import { SUCCESS } from '@notifications/success.ts';
import { ERRORS } from '@notifications/errors.ts';
import {dicebear} from "@src/utils/dicebear.ts";
import AvatarProfile from "@src/components/avatar/avatar.tsx";
import {MAX_POOL} from "@src/sections/finance/components/finance-quick-transfer.tsx";
import {handleAmountConstraints} from "@src/utils/format-number.ts";

type TConfirmTransferDialogProps = InputAmountProps & DialogProps;

Expand All @@ -55,6 +57,12 @@ function FinanceQuickTransferModal({
const { sendNotification } = useNotifications();
const [message, setMessage] = useState('');

// For transfer button is clicked in some profile
const balance = useSelector((state: any) => state.auth.balance);
const MAX_AMOUNT = balance;
const [value, setValue] = useState(0);
const [canContinue, setCanContinue] = useState(true);

// Check if we have a valid profile or not
const hasProfile = !!contactInfo;

Expand Down Expand Up @@ -100,7 +108,7 @@ function FinanceQuickTransferModal({

const handleConfirmTransfer = async () => {
try {
await transfer({ amount, recipient: address ?? '' });
await transfer({ amount: amount > 0 ? amount: value, recipient: address ?? '' });

onFinish();

Expand All @@ -123,7 +131,7 @@ function FinanceQuickTransferModal({
// Store transaction in supabase
await storeTransactionInSupabase(contactInfo?.id ?? address, senderId, {
address: contactInfo?.ownedBy?.address ?? address,
amount,
amount: amount > 0 ? amount : value,
message,
...notificationPayload,
});
Expand All @@ -143,6 +151,16 @@ function FinanceQuickTransferModal({
}
};

const handleChangeInput = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(event.target.value);
handleAmountConstraints({value, MAX_AMOUNT, MAX_POOL, setAmount: setValue, setCanContinue});
}, [MAX_AMOUNT]);


const handleBlur = useCallback(() => {
handleAmountConstraints({value, MAX_AMOUNT, MAX_POOL, setAmount: setValue, setCanContinue});
}, [value, MAX_AMOUNT]);

const RainbowEffect = transferLoading ? NeonPaper : Box;

return (
Expand All @@ -165,11 +183,19 @@ function FinanceQuickTransferModal({
</Stack>

<Stack direction="column" spacing={0} sx={{ py: 2, flexGrow: 1 }}>
<ListItemText
primary="Amount:"
secondary={`${amount} MMC`}
secondaryTypographyProps={{ component: 'span', mt: 0.5 }}
/>
{amount > 0 ? (

<ListItemText
primary="Amount:"
secondary={`${amount} MMC`}
secondaryTypographyProps={{ component: 'span', mt: 0.5 }}
/>
) : <InputAmount
max={MAX_POOL}
amount={value}
onBlur={handleBlur}
onChange={handleChangeInput}
/>}
</Stack>
</Stack>

Expand All @@ -190,7 +216,7 @@ function FinanceQuickTransferModal({
variant="contained"
sx={{ backgroundColor: '#fff' }}
onClick={handleConfirmTransfer}
disabled={transferLoading}
disabled={transferLoading || !canContinue}
loading={transferLoading}
>
Confirm
Expand Down
77 changes: 15 additions & 62 deletions src/sections/finance/components/finance-quick-transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useState } from 'react';

// REDUX IMPORTS
import { useDispatch, useSelector } from 'react-redux';
import { storeAddress, toggleRainbow } from '@redux/address';
import { storeAddress } from '@redux/address';

// LENS IMPORTS
import { Profile } from '@lens-protocol/api-bindings';
Expand All @@ -17,7 +17,6 @@ import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Slider from '@mui/material/Slider';
import Tooltip from '@mui/material/Tooltip';
import TextField from '@mui/material/TextField';
import Box from '@mui/material/Box';
import CardHeader from '@mui/material/CardHeader';
import { CardProps } from '@mui/material/Card';
Expand All @@ -30,13 +29,17 @@ import { InputAmount } from '@src/components/input-amount.tsx';
import FinanceQuickTransferModal from '@src/sections/finance/components/finance-quick-transfer-modal.tsx';
import FinanceSearchProfileModal from '@src/sections/finance/components/finance-search-profile-modal.tsx';
import AvatarProfile from "@src/components/avatar/avatar.tsx";
import FinanceNoFollowingsQuickTransfer
from "@src/sections/finance/components/finance-no-followings-quick-transfer";
import FinanceDisplayProfileInfo from "@src/sections/finance/components/finance-display-profile-info";
import {handleAmountConstraints} from "@src/utils/format-number.ts";

// ----------------------------------------------------------------------

const STEP = 50;
const MIN_AMOUNT = 0;
// A thousand millions allowed in the pool
const MAX_POOL: number = 1000000000;
export const MAX_POOL: number = 1000000000;

interface Props extends CardProps {
title?: string;
Expand Down Expand Up @@ -67,7 +70,6 @@ export default function FinanceQuickTransfer({

// Local states
const [walletAddress, setWalletAddress] = useState(storedAddress.address ?? '');
const [addressError, setAddressError] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const [addressFiltered, setAddressFiltered] = useState<boolean>(false);
const [initialized, setInitialized] = useState(false);
Expand Down Expand Up @@ -182,27 +184,6 @@ export default function FinanceQuickTransfer({
}
}, [initialList]);

// Handle changes in the input field for the wallet address
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setWalletAddress(value);
dispatch(storeAddress({ address: value, profileId: getContactInfo?.id ?? '' }));

// If it's a valid address, let the next effect handle searching in the list
if (isValidAddress(value)) {
setAddressFiltered(true); // We set a flag that we typed a valid address
dispatch(toggleRainbow());
setAddressError(false);
} else {
setAddressError(true);
}

// Rainbow effect trigger
setTimeout(() => {
dispatch(toggleRainbow());
}, 1400);
};

// Handle changes in the slider
const handleChangeSlider = useCallback((_event: Event, newValue: number | number[]) => {
setAmount(newValue as number);
Expand All @@ -211,31 +192,14 @@ export default function FinanceQuickTransfer({
}
}, [MAX_AMOUNT]);

// Helper function to handle amount constraints
const handleAmountConstraints = (value: number, MAX_AMOUNT: number) => {
if (value > MAX_POOL) {
value = MAX_POOL; // Truncate to a thousand millions
}
if (value < 0) {
value = 0; // Set amount to 0 if lower than 0
}
setAmount(value);
setCanContinue(value <= MAX_AMOUNT);

// If amount is greater than balance, allow input but setCanContinue to false
if (value > MAX_AMOUNT) {
setCanContinue(false);
}
};

const handleChangeInput = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(event.target.value);
handleAmountConstraints(value, MAX_AMOUNT);
handleAmountConstraints({value, MAX_AMOUNT, MAX_POOL, setAmount, setCanContinue});
}, [MAX_AMOUNT]);


const handleBlur = useCallback(() => {
handleAmountConstraints(amount, MAX_AMOUNT);
handleAmountConstraints({value: amount, MAX_AMOUNT, MAX_POOL, setAmount, setCanContinue});
}, [amount, MAX_AMOUNT]);


Expand Down Expand Up @@ -293,21 +257,6 @@ export default function FinanceQuickTransfer({
// We pick the contactInfo to pass to the modal. If currentIndex is -1, there's no matched profile
const contactInfoToPass = currentIndex === -1 ? undefined : getContactInfo;

// Render the wallet address input
const renderWalletInput = (
<Box sx={{ mb: 3 }}>
<TextField
fullWidth
label="Wallet Address"
value={walletAddress}
onChange={handleInputChange}
placeholder="Enter wallet address"
error={addressError}
helperText={addressError ? 'Invalid wallet address' : ''}
/>
</Box>
);

// Render the carousel of profiles
const renderCarousel = (
<Box sx={{ position: 'relative', mb: 3 }}>
Expand Down Expand Up @@ -418,13 +367,14 @@ export default function FinanceQuickTransfer({
disabled={amount === 0 || !isValidAddress(walletAddress) || !canContinue}
onClick={confirm.onTrue}
>
Transfer Now
Quick transfer
</Button>
</Stack>
);

const Wrapper = showRainbow ? NeonPaper : Box;

console.log(list);
return (
<>
<Wrapper
Expand All @@ -442,15 +392,17 @@ export default function FinanceQuickTransfer({
{...other}
>
<CardHeader
sx={{ p: '12px 16px 0 0' }}
title={title}
subheader={subheader}
action={<FinanceSearchProfileModal onSelectProfile={handleSelectProfile} />}
/>

{/* Content */}
<Stack sx={{ p: 3 }}>
{renderWalletInput}
{!!list?.length && renderCarousel}
<FinanceDisplayProfileInfo mode={'profile'} initialList={initialList} carousel={carousel} />
{list?.length > 0 ? renderCarousel : <FinanceNoFollowingsQuickTransfer />}
<FinanceDisplayProfileInfo mode={'wallet'} initialList={initialList} carousel={carousel} />
{renderInput}
</Stack>
</Stack>
Expand All @@ -471,3 +423,4 @@ export default function FinanceQuickTransfer({
</>
);
}

Loading
Loading