Skip to content

Commit

Permalink
feat(wallet): rebrand send visual asset (#2065)
Browse files Browse the repository at this point in the history
* feat: refine components

* feat: update NftImage component

* feat: rebrand asset details and improvements

* feat: rebrand send asset page

* feat: improve classes

* feat: improvement

* feat: add truncate to links and use button instead of link

* feat: clenaup

* feat: cleanp
  • Loading branch information
evavirseda authored Aug 30, 2024
1 parent c846fd6 commit f6373e8
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 222 deletions.
152 changes: 27 additions & 125 deletions apps/wallet/src/ui/app/components/address-input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,33 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { Text } from '_app/shared/text';
import { Alert } from '_components';
import { useIotaClient } from '@iota/dapp-kit';
import { QrCode, X12 } from '@iota/icons';
import { isValidIotaAddress } from '@iota/iota-sdk/utils';
import { useQuery } from '@tanstack/react-query';
import { cx } from 'class-variance-authority';
import { useField, useFormikContext } from 'formik';
import { useCallback, useMemo } from 'react';
import type { ChangeEventHandler } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { useIotaAddressValidation } from '@iota/core';
import { Input, InputType } from '@iota/apps-ui-kit';
import { Close } from '@iota/ui-icons';

export interface AddressInputProps {
disabled?: boolean;
placeholder?: string;
name: string;
}

enum RecipientWarningType {
Object = 'OBJECT',
Empty = 'EMPTY',
label?: string;
}

export function AddressInput({
disabled: forcedDisabled,
placeholder = '0x...',
name = 'to',
label = 'Enter Recipient Address',
}: AddressInputProps) {
const [field, meta] = useField(name);

const client = useIotaClient();
const { data: warningData } = useQuery({
queryKey: ['address-input-warning', field.value],
queryFn: async () => {
// We assume this validation will happen elsewhere:
if (!isValidIotaAddress(field.value)) {
return null;
}

const object = await client.getObject({ id: field.value });

if (object && 'data' in object) {
return RecipientWarningType.Object;
}

const [fromAddr, toAddr] = await Promise.all([
client.queryTransactionBlocks({
filter: { FromAddress: field.value },
limit: 1,
}),
client.queryTransactionBlocks({
filter: { ToAddress: field.value },
limit: 1,
}),
]);

if (fromAddr.data?.length === 0 && toAddr.data?.length === 0) {
return RecipientWarningType.Empty;
}

return null;
},
enabled: !!field.value,
gcTime: 10 * 1000,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchInterval: false,
});

const { isSubmitting, setFieldValue } = useFormikContext();
const iotaAddressValidation = useIotaAddressValidation();

const disabled = forcedDisabled !== undefined ? forcedDisabled : isSubmitting;
const handleOnChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
const handleOnChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
(e) => {
const address = e.currentTarget.value;
setFieldValue(name, iotaAddressValidation.cast(address));
Expand All @@ -92,80 +44,30 @@ export function AddressInput({
setFieldValue('to', '');
}, [setFieldValue]);

const hasWarningOrError = meta.touched && (meta.error || warningData);

return (
<>
<div
className={cx(
'focus-within:border-steel box-border flex h-max w-full overflow-hidden rounded-2lg border border-solid bg-white transition-all',
hasWarningOrError ? 'border-issue' : 'border-gray-45',
)}
>
<div className="flex min-h-[42px] w-full items-center py-2 pl-3">
<TextareaAutosize
data-testid="address-input"
maxRows={3}
minRows={1}
disabled={disabled}
placeholder={placeholder}
value={formattedValue}
onChange={handleOnChange}
onBlur={field.onBlur}
className={cx(
'placeholder:text-steel-dark w-full resize-none border-none bg-white font-mono text-bodySmall font-medium leading-100 placeholder:font-mono placeholder:font-normal',
hasWarningOrError ? 'text-issue' : 'text-gray-90',
)}
name={name}
/>
</div>

<div
onClick={clearAddress}
className="bg-gray-40 right-0 ml-4 flex w-11 max-w-[20%] cursor-pointer items-center justify-center"
>
{meta.touched && field.value ? (
<X12 className="text-steel-darker h-3 w-3" />
) : (
<QrCode className="text-steel-darker h-5 w-5" />
)}
</div>
</div>

{meta.touched ? (
<div className="mt-2.5 w-full">
<Alert
noBorder
rounded="lg"
mode={meta.error || warningData ? 'issue' : 'success'}
>
{warningData === RecipientWarningType.Object ? (
<>
<Text variant="pBody" weight="semibold">
This address is an Object
</Text>
<Text variant="pBodySmall" weight="medium">
Once sent, the funds cannot be recovered. Please make sure you
want to send coins to this address.
</Text>
</>
) : warningData === RecipientWarningType.Empty ? (
<>
<Text variant="pBody" weight="semibold">
This address has no prior transactions
</Text>
<Text variant="pBodySmall" weight="medium">
Please make sure you want to send coins to this address.
</Text>
</>
) : (
<Text variant="pBodySmall" weight="medium">
{meta.error || 'Valid address'}
</Text>
)}
</Alert>
</div>
) : null}
<Input
type={InputType.Text}
disabled={disabled}
placeholder={placeholder}
value={formattedValue}
name={name}
onBlur={field.onBlur}
label={label}
onChange={handleOnChange}
errorMessage={meta.error}
trailingElement={
formattedValue ? (
<button
onClick={clearAddress}
type="button"
className="flex items-center justify-center"
>
<Close />
</button>
) : undefined
}
/>
</>
);
}
74 changes: 15 additions & 59 deletions apps/wallet/src/ui/app/components/nft-display/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { Heading } from '_app/shared/heading';
import { Loading, NftImage } from '_components';
import { useFileExtensionType } from '_hooks';
import { isKioskOwnerToken, useGetNFTMeta, useGetObject, useKioskClient } from '@iota/core';
import { formatAddress } from '@iota/iota-sdk/utils';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';

import { useResolveVideo } from '../../hooks/useResolveVideo';
import { Text } from '../../shared/text';
import { Kiosk } from './Kiosk';

const nftDisplayCardStyles = cva('flex flex-nowrap items-center h-full relative', {
Expand All @@ -20,17 +16,12 @@ const nftDisplayCardStyles = cva('flex flex-nowrap items-center h-full relative'
true: 'group',
},
wideView: {
true: 'bg-gray-40 p-2.5 rounded-lg gap-2.5 flex-row-reverse justify-between',
true: 'gap-2 flex-row-reverse justify-between',
false: '',
},
orientation: {
horizontal: 'flex truncate',
vertical: 'flex-col',
},
},
defaultVariants: {
wideView: false,
orientation: 'vertical',
},
});

Expand All @@ -45,66 +36,31 @@ export function NFTDisplayCard({
hideLabel,
wideView,
isHoverable,
orientation,
}: NFTDisplayCardProps) {
const { data: objectData } = useGetObject(objectId);
const { data: nftMeta, isPending } = useGetNFTMeta(objectId);
const nftName = nftMeta?.name || formatAddress(objectId);
const nftImageUrl = nftMeta?.imageUrl || '';
const video = useResolveVideo(objectData);
const fileExtensionType = useFileExtensionType(nftImageUrl);
const kioskClient = useKioskClient();
const isOwnerToken = isKioskOwnerToken(kioskClient.network, objectData);

return (
<div className={nftDisplayCardStyles({ isHoverable, wideView, orientation })}>
<div className={nftDisplayCardStyles({ isHoverable, wideView })}>
<Loading loading={isPending}>
{objectData?.data && isOwnerToken ? (
<Kiosk object={objectData} />
) : (
<NftImage
title={nftName}
src={nftImageUrl}
isHoverable={isHoverable ?? false}
video={video}
/>
)}
{wideView && (
<div className="ml-1 flex min-w-0 flex-1 flex-col gap-1">
<Heading variant="heading6" color="gray-90" truncate>
{nftName}
</Heading>
<div className="text-gray-75 text-body font-medium">
{nftImageUrl ? (
`${fileExtensionType.name} ${fileExtensionType.type}`
) : (
<span className="text-bodySmall font-normal uppercase">
NO MEDIA
</span>
)}
</div>
</div>
)}

{orientation === 'horizontal' ? (
<div className="text-steel-dark ml-2 max-w-full flex-1 overflow-hidden">
{nftName}
</div>
) : !isOwnerToken && !hideLabel ? (
<div className="absolute bottom-2 left-1/2 flex w-10/12 -translate-x-1/2 items-center justify-center rounded-lg bg-white/90 opacity-0 group-hover:opacity-100">
<div className="mt-0.5 overflow-hidden px-2 py-1">
<Text
variant="subtitleSmall"
weight="semibold"
mono
color="steel-darker"
truncate
>
{nftName}
</Text>
</div>
</div>
) : null}
<div className="flex flex-col justify-center gap-sm text-center">
{objectData?.data && isOwnerToken ? (
<Kiosk object={objectData} />
) : (
<NftImage
title={nftName}
src={nftImageUrl}
isHoverable={isHoverable ?? false}
video={video}
/>
)}
{wideView && <span className="text-title-lg text-neutral-10">{nftName}</span>}
</div>
</Loading>
</div>
);
Expand Down
45 changes: 11 additions & 34 deletions apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import BottomMenuLayout, { Content, Menu } from '_app/shared/bottom-menu-layout';
import { Button } from '_app/shared/ButtonUI';
import { Text } from '_app/shared/text';
import { AddressInput } from '_components';
import { ampli } from '_src/shared/analytics/ampli';
import { getSignerOperationErrorMessage } from '_src/ui/app/helpers/errorMessages';
Expand All @@ -18,14 +15,14 @@ import {
useIotaNSEnabled,
} from '@iota/core';
import { useIotaClient } from '@iota/dapp-kit';
import { ArrowRight16 } from '@iota/icons';
import { TransactionBlock } from '@iota/iota-sdk/transactions';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Field, Form, Formik } from 'formik';
import { toast } from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';

import { useTransferKioskItem } from './useTransferKioskItem';
import { Button, ButtonHtmlType } from '@iota/apps-ui-kit';

interface TransferNFTFormProps {
objectId: string;
Expand Down Expand Up @@ -120,36 +117,16 @@ export function TransferNFTForm({ objectId, objectType }: TransferNFTFormProps)
>
{({ isValid }) => (
<Form autoComplete="off" className="h-full">
<BottomMenuLayout className="h-full">
<Content>
<div className="flex flex-col gap-2.5">
<div className="px-2.5 tracking-wider">
<Text variant="caption" color="steel" weight="semibold">
Enter Recipient Address
</Text>
</div>
<div className="relative flex w-full flex-col items-center">
<Field
component={AddressInput}
allowNegative={false}
name="to"
placeholder="Enter Address"
/>
</div>
</div>
</Content>
<Menu stuckClass="sendCoin-cta" className="mx-0 w-full gap-2.5 px-0 pb-0">
<Button
type="submit"
variant="primary"
loading={transferNFT.isPending}
disabled={!isValid}
size="tall"
text="Send NFT Now"
after={<ArrowRight16 />}
/>
</Menu>
</BottomMenuLayout>
<div className="flex h-full flex-col justify-between">
<Field
component={AddressInput}
allowNegative={false}
name="to"
placeholder="Enter Address"
/>

<Button htmlType={ButtonHtmlType.Submit} disabled={!isValid} text="Send" />
</div>
</Form>
)}
</Formik>
Expand Down
Loading

0 comments on commit f6373e8

Please sign in to comment.