diff --git a/apps/ui-kit/src/lib/components/molecules/card/CardBody.tsx b/apps/ui-kit/src/lib/components/molecules/card/CardBody.tsx index 3684cbbbbf3..d5332f77231 100644 --- a/apps/ui-kit/src/lib/components/molecules/card/CardBody.tsx +++ b/apps/ui-kit/src/lib/components/molecules/card/CardBody.tsx @@ -1,9 +1,11 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { ReactNode } from 'react'; + export type CardBodyProps = { title: string; - subtitle?: string; + subtitle?: string | ReactNode; clickableAction?: React.ReactNode; }; diff --git a/apps/wallet/src/ui/app/components/DAppInfoCard.tsx b/apps/wallet/src/ui/app/components/DAppInfoCard.tsx index aa6717ce2c0..05dc89a0d9f 100644 --- a/apps/wallet/src/ui/app/components/DAppInfoCard.tsx +++ b/apps/wallet/src/ui/app/components/DAppInfoCard.tsx @@ -5,13 +5,14 @@ import { type PermissionType } from '_src/shared/messaging/messages/payloads/permissions'; import { getValidDAppUrl } from '_src/shared/utils'; import { useAccountByAddress } from '../hooks/useAccountByAddress'; -import { Heading } from '../shared/heading'; -import { Link } from '../shared/Link'; import { AccountIcon } from './accounts/AccountIcon'; import { AccountItem } from './accounts/AccountItem'; import { useUnlockAccount } from './accounts/UnlockAccountContext'; -import { DAppPermissionsList } from './DAppPermissionsList'; +import { DAppPermissionList } from './DAppPermissionList'; import { SummaryCard } from './SummaryCard'; +import { Link } from 'react-router-dom'; +import { Card, CardBody, CardImage, CardType, ImageShape, ImageType } from '@iota/apps-ui-kit'; +import { ImageIcon } from '../shared/image-icon'; export interface DAppInfoCardProps { name: string; @@ -29,7 +30,6 @@ export function DAppInfoCard({ permissions, }: DAppInfoCardProps) { const validDAppUrl = getValidDAppUrl(url); - const appHostname = validDAppUrl?.hostname ?? url; const { data: account } = useAccountByAddress(connectedAddress); const { unlockAccount, lockAccount } = useUnlockAccount(); function handleLockAndUnlockClick() { @@ -41,28 +41,24 @@ export function DAppInfoCard({ } } return ( -
-
-
- {iconUrl ? {name} : null} -
-
-
- - {name} - -
-
+
+ + + + + -
-
-
+ to={validDAppUrl?.toString() ?? url} + target="_blank" + rel="noopener noreferrer" + > + {validDAppUrl?.toString() ?? url} + + } + /> + {connectedAddress && account ? ( } @@ -76,7 +72,7 @@ export function DAppInfoCard({ {permissions?.length ? ( } + body={} boxShadow /> ) : null} diff --git a/apps/wallet/src/ui/app/components/DAppPermissionList.tsx b/apps/wallet/src/ui/app/components/DAppPermissionList.tsx new file mode 100644 index 00000000000..3f5d4b33d6c --- /dev/null +++ b/apps/wallet/src/ui/app/components/DAppPermissionList.tsx @@ -0,0 +1,29 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { type PermissionType } from '_src/shared/messaging/messages/payloads/permissions'; +import { SummaryListItem } from './SummaryListItem'; +import { Checkmark } from '@iota/ui-icons'; + +export interface DAppPermissionListProps { + permissions: PermissionType[]; +} + +const PERMISSION_TYPE_TO_TEXT: Record = { + viewAccount: 'Share wallet address', + suggestTransactions: 'Suggest transactions to approve', +}; + +export function DAppPermissionList({ permissions }: DAppPermissionListProps) { + return ( +
+ {permissions.map((permissionKey) => ( + } + text={PERMISSION_TYPE_TO_TEXT[permissionKey]} + /> + ))} +
+ ); +} diff --git a/apps/wallet/src/ui/app/components/DAppPermissionsList.tsx b/apps/wallet/src/ui/app/components/DAppPermissionsList.tsx deleted file mode 100644 index df2a85aef4f..00000000000 --- a/apps/wallet/src/ui/app/components/DAppPermissionsList.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import type { PermissionType } from '_messages/payloads/permissions'; -import { CheckFill12 } from '@iota/icons'; - -import { Text } from '../shared/text'; - -export interface DAppPermissionsListProps { - permissions: PermissionType[]; -} - -const PERMISSION_TYPE_TO_TEXT: Record = { - viewAccount: 'Share wallet address', - suggestTransactions: 'Suggest transactions to approve', -}; - -export function DAppPermissionsList({ permissions }: DAppPermissionsListProps) { - return ( -
    - {permissions.map((aPermission) => ( -
  • - - - {PERMISSION_TYPE_TO_TEXT[aPermission]} - -
  • - ))} -
- ); -} diff --git a/apps/wallet/src/ui/app/components/SummaryListItem.tsx b/apps/wallet/src/ui/app/components/SummaryListItem.tsx new file mode 100644 index 00000000000..d849b71a534 --- /dev/null +++ b/apps/wallet/src/ui/app/components/SummaryListItem.tsx @@ -0,0 +1,16 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +interface SummaryListItemProps { + icon: React.ReactNode; + text: string; +} + +export function SummaryListItem({ icon, text }: SummaryListItemProps) { + return ( +
+ {icon} + {text} +
+ ); +} diff --git a/apps/wallet/src/ui/app/components/SummaryPanel.tsx b/apps/wallet/src/ui/app/components/SummaryPanel.tsx new file mode 100644 index 00000000000..c16c3f743b0 --- /dev/null +++ b/apps/wallet/src/ui/app/components/SummaryPanel.tsx @@ -0,0 +1,23 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Title, TitleSize } from '@iota/apps-ui-kit'; +import { type ReactNode } from 'react'; + +interface SummaryPanelProps { + title: string; + body: ReactNode; +} + +export function SummaryPanel({ title, body }: SummaryPanelProps) { + return ( +
+
+
+ + </div> + {body} + </div> + </div> + ); +} diff --git a/apps/wallet/src/ui/app/components/WalletListSelect.tsx b/apps/wallet/src/ui/app/components/WalletListSelect.tsx index e3be7e2d962..67a96dc882c 100644 --- a/apps/wallet/src/ui/app/components/WalletListSelect.tsx +++ b/apps/wallet/src/ui/app/components/WalletListSelect.tsx @@ -3,33 +3,33 @@ // SPDX-License-Identifier: Apache-2.0 import { cx } from 'class-variance-authority'; -import { useMemo } from 'react'; - import { useAccounts } from '../hooks/useAccounts'; -import { Link } from '../shared/Link'; -import { SummaryCard } from './SummaryCard'; -import { WalletListSelectItem, type WalletListSelectItemProps } from './WalletListSelectItem'; +import { useMemo } from 'react'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { Checkbox } from '@iota/apps-ui-kit'; export interface WalletListSelectProps { - title: string; values: string[]; visibleValues?: string[]; - mode?: WalletListSelectItemProps['mode']; + mode?: WalletListSelectMode; disabled?: boolean; onChange: (values: string[]) => void; boxShadow?: boolean; } +enum WalletListSelectMode { + Select = 'select', + Disconnect = 'disconnect', +} + export function WalletListSelect({ - title, values, visibleValues, - mode = 'select', - disabled = false, + disabled, onChange, - boxShadow = false, }: WalletListSelectProps) { const { data: accounts } = useAccounts(); + const filteredAccounts = useMemo(() => { if (!accounts) { return []; @@ -39,69 +39,48 @@ export function WalletListSelect({ } return accounts; }, [accounts, visibleValues]); + + function onAccountClick(address: string) { + if (disabled) { + return; + } + const newValues = []; + let found = false; + for (const anAddress of values) { + if (anAddress === address) { + found = true; + continue; + } + newValues.push(anAddress); + } + if (!found) { + newValues.push(address); + } + onChange(newValues); + } + return ( - <SummaryCard - header={title} - body={ - <ul - className={cx( - 'm-0 flex flex-1 list-none flex-col items-stretch self-stretch p-0', - disabled ? 'opacity-70' : '', - )} - > - {filteredAccounts.map(({ address }) => ( - <li - key={address} - onClick={() => { - if (disabled) { - return; - } - const newValues = []; - let found = false; - for (const anAddress of values) { - if (anAddress === address) { - found = true; - continue; - } - newValues.push(anAddress); - } - if (!found) { - newValues.push(address); - } - onChange(newValues); - }} + <div className="flex flex-col gap-y-sm"> + {filteredAccounts.map(({ address }) => { + const accountAddress = formatAddress(address); + return ( + <div + key={address} + className="flex cursor-default flex-row items-center justify-start gap-x-xs py-xxxs" + onClick={() => onAccountClick(address)} + > + <Checkbox name={address} isChecked={values.includes(address)} /> + <span + className={cx( + 'cursor-default text-body-md text-neutral-40', + disabled && 'text-opacity-40', + )} > - <WalletListSelectItem - address={address} - selected={values.includes(address)} - mode={mode} - disabled={disabled} - /> - </li> - ))} - </ul> - } - footer={ - mode === 'select' ? ( - <div className="flex flex-row flex-nowrap justify-between self-stretch"> - <div> - {filteredAccounts.length > 1 ? ( - <Link - color="heroDark" - weight="medium" - text="Select all" - disabled={disabled} - onClick={() => - onChange(filteredAccounts.map(({ address }) => address)) - } - /> - ) : null} - </div> + {accountAddress} + </span> </div> - ) : null - } - minimalPadding - boxShadow={boxShadow} - /> + ); + })} + </div> ); } diff --git a/apps/wallet/src/ui/app/components/index.ts b/apps/wallet/src/ui/app/components/index.ts index 4468c91c44f..6cb12c93937 100644 --- a/apps/wallet/src/ui/app/components/index.ts +++ b/apps/wallet/src/ui/app/components/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export * from './DAppInfoCard'; -export * from './DAppPermissionsList'; +export * from './DAppPermissionList'; export * from './HideShowDisplayBox'; export * from './IconButton'; export * from './LabelValueItem'; diff --git a/apps/wallet/src/ui/app/components/iota-apps/DisconnectApp.tsx b/apps/wallet/src/ui/app/components/iota-apps/DisconnectApp.tsx index a472d532e83..292389bfe0a 100644 --- a/apps/wallet/src/ui/app/components/iota-apps/DisconnectApp.tsx +++ b/apps/wallet/src/ui/app/components/iota-apps/DisconnectApp.tsx @@ -2,13 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { - Overlay, - DAppInfoCard, - DAppPermissionsList, - SummaryCard, - WalletListSelect, -} from '_components'; +import { Overlay, DAppInfoCard, WalletListSelect } from '_components'; import { useAppSelector } from '_hooks'; import { permissionsSelectors } from '_redux/slices/permissions'; import { ampli } from '_src/shared/analytics/ampli'; @@ -18,10 +12,14 @@ import { useEffect, useMemo, useState } from 'react'; import { toast } from 'react-hot-toast'; import { useBackgroundClient } from '../../hooks/useBackgroundClient'; -import { Button } from '../../shared/ButtonUI'; -import { Text } from '../../shared/text'; import { type DAppEntry } from './IotaApp'; +import { CircleEmitter } from '@iota/ui-icons'; +import { Button, ButtonType } from '@iota/apps-ui-kit'; +import { SummaryPanel } from '../SummaryPanel'; +import { SummaryListItem } from '../SummaryListItem'; +import { DAppPermissionList } from '../DAppPermissionList'; + export interface DisconnectAppProps extends Omit<DAppEntry, 'description' | 'tags'> { permissionID: string; setShowDisconnectApp: (showModal: boolean) => void; @@ -74,36 +72,53 @@ function DisconnectApp({ return null; } return ( - <Overlay showModal setShowModal={setShowDisconnectApp} title="Connection Active"> - <div className="flex flex-1 flex-col flex-nowrap items-stretch gap-3.75"> + <Overlay + showBackButton + showModal + setShowModal={setShowDisconnectApp} + title="Active Connection" + > + <div className="flex max-w-full flex-1 flex-col flex-nowrap items-stretch gap-y-md"> <DAppInfoCard name={name} iconUrl={icon} url={link} /> - <SummaryCard - header="Permissions given" - body={<DAppPermissionsList permissions={permission.permissions} />} + + <SummaryPanel + title="Permissions requested" + body={ + <div className="px-md"> + <DAppPermissionList permissions={permission.permissions} /> + </div> + } /> - {connectedAccounts.length > 1 ? ( - <WalletListSelect - title="Connected Accounts" - visibleValues={connectedAccounts} - values={accountsToDisconnect} - onChange={setAccountsToDisconnect} - mode="disconnect" - disabled={disconnectMutation.isPending} - /> - ) : ( - <SummaryCard - header="Connected Account" - body={ - <Text variant="body" color="steel-dark" weight="semibold" mono> - {connectedAccounts[0] ? formatAddress(connectedAccounts[0]) : null} - </Text> - } - /> - )} - <div className="sticky -bottom-5 flex flex-1 items-end bg-white pb-5 pt-1"> + + <SummaryPanel + title={'Connected Account' + (connectedAccounts.length > 1 ? 's' : '')} + body={ + <div className="px-md"> + {connectedAccounts.length > 1 ? ( + <WalletListSelect + visibleValues={connectedAccounts} + values={accountsToDisconnect} + onChange={setAccountsToDisconnect} + disabled={disconnectMutation.isPending} + /> + ) : ( + <SummaryListItem + icon={<CircleEmitter className="h-5 w-5 text-neutral-10" />} + text={ + connectedAccounts[0] + ? formatAddress(connectedAccounts[0]) + : '' + } + /> + )} + </div> + } + /> + + <div className="sticky bottom-0 flex flex-1 items-end pt-xs"> <Button - size="tall" - variant="warning" + type={ButtonType.Secondary} + fullWidth text={ connectedAccounts.length === 1 ? 'Disconnect' @@ -112,7 +127,7 @@ function DisconnectApp({ ? 'Disconnect All' : 'Disconnect Selected' } - loading={disconnectMutation.isPending} + disabled={disconnectMutation.isPending} onClick={() => disconnectMutation.mutate()} /> </div>