diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index dab5219b..3198cc86 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -107,7 +107,7 @@ export const relativeRoutes = { settings: { path: 'settings', idCards: { - path: 'idCards', + path: 'id-cards', }, about: { path: 'about', diff --git a/packages/browser-wallet/src/popup/popupX/pages/Accounts/Accounts.tsx b/packages/browser-wallet/src/popup/popupX/pages/Accounts/Accounts.tsx index eda81b9b..04c5850f 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/Accounts/Accounts.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/Accounts/Accounts.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, KeyboardEvent, useState } from 'react'; +import React from 'react'; import Plus from '@assets/svgX/plus.svg'; import Arrows from '@assets/svgX/arrows-down-up.svg'; import MagnifyingGlass from '@assets/svgX/magnifying-glass.svg'; @@ -21,71 +21,7 @@ import { WalletCredential } from '@shared/storage/types'; import { displaySplitAddress, useIdentityName, useWritableSelectedAccount } from '@popup/shared/utils/account-helpers'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; import { displayAsCcd } from 'wallet-common-helpers'; - -type EditableAccountNameProps = { - currentName: string; - fallbackName: string; - onNewName: (newName: string) => void; -}; - -function EditableAccountName({ currentName, fallbackName, onNewName }: EditableAccountNameProps) { - const [isEditingName, setIsEditingName] = useState(false); - const [editedName, setEditedName] = useState(currentName); - // Using editedName instead of currentName to avoid flickering after completing. - const displayName = editedName === '' ? fallbackName : editedName; - const onAbort = () => { - setIsEditingName(false); - setEditedName(currentName); - }; - const onComplete = () => { - onNewName(editedName.trim()); - setIsEditingName(false); - }; - const onEdit = () => { - setEditedName(currentName); - setIsEditingName(true); - }; - const onInputChange = (event: ChangeEvent) => { - setEditedName(event.target.value); - }; - const onKeyUp = (event: KeyboardEvent) => { - if (event.key === 'Enter') { - event.preventDefault(); - onComplete(); - } - }; - if (isEditingName) { - return ( - <> - - - -
- } - onClick={onComplete} - /> - } onClick={onAbort} /> -
- - ); - } - return ( - <> - {displayName} - } onClick={onEdit} /> - - ); -} +import useEditableValue from '@popup/popupX/shared/EditableValue'; type AccountListItemProps = { credential: WalletCredential; @@ -108,14 +44,28 @@ function AccountListItem({ credential }: AccountListItemProps) { const ccdBalance = accountInfo === undefined ? 'Loading' : displayAsCcd(accountInfo.accountAmount.microCcdAmount, false); const onNewAccountName = (newName: string) => setAccount({ credName: newName }); + const editable = useEditableValue(accountName, fallbackName, onNewAccountName); + return ( - + {editable.value} + {editable.isEditing ? ( +
+ } + onClick={editable.onComplete} + /> + } + onClick={editable.onAbort} + /> +
+ ) : ( + } onClick={editable.onEdit} /> + )}
{address} diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.scss b/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.scss index b934f6c1..1f35cd3a 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.scss +++ b/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.scss @@ -1,2 +1,5 @@ .id-cards-x { + .page__main { + gap: rem(16px); + } } diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx b/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx index e5787eca..53de3f8f 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx @@ -1,35 +1,108 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import Plus from '@assets/svgX/plus.svg'; import Button from '@popup/popupX/shared/Button'; import Page from '@popup/popupX/shared/Page'; import { useTranslation } from 'react-i18next'; import IdCard from '@popup/popupX/shared/IdCard'; +import { identitiesAtom, identityProvidersAtom } from '@popup/store/identity'; +import { useAtom, useAtomValue } from 'jotai'; +import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers'; +import { CreationStatus, ConfirmedIdentity, WalletCredential } from '@shared/storage/types'; +import { AttributeKey } from '@concordium/web-sdk'; +import { IdCardAccountInfo, IdCardAttributeInfo } from '@popup/popupX/shared/IdCard/IdCard'; +import { credentialsAtomWithLoading } from '@popup/store/account'; +import { displayNameAndSplitAddress } from '@popup/shared/utils/account-helpers'; +import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; +import { compareAttributes, displayAsCcd } from 'wallet-common-helpers'; -const rowsIdInfo: [string, string][] = [ - ['Identity document type', 'Drivers licence'], - ['Identity document number', 'BXM680515'], - ['First name', 'Lewis'], - ['Last name', 'Hamilton'], - ['Date of birth', '13 August 1992'], - ['Identity document issuer', 'New Zeland'], - ['ID valid until', '30 October 2051'], -]; +function CcdBalance({ credential }: { credential: WalletCredential }) { + const accountInfo = useAccountInfo(credential); + const balance = + accountInfo === undefined ? '' : displayAsCcd(accountInfo.accountAmount.microCcdAmount, false, true); + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{balance}; +} + +function fallbackIdentityName(index: number) { + return `Identity ${index + 1}`; +} + +type ConfirmedIdentityProps = { identity: ConfirmedIdentity; onNewName: (name: string) => void }; + +function ConfirmedIdCard({ identity, onNewName }: ConfirmedIdentityProps) { + const displayAttribute = useDisplayAttributeValue(); + const getAttributeName = useGetAttributeName(); + const providers = useAtomValue(identityProvidersAtom); + const credentials = useAtomValue(credentialsAtomWithLoading); + const provider = providers.find((p) => p.ipInfo.ipIdentity === identity.providerIndex); + const providerName = provider?.ipInfo.ipDescription.name ?? 'Unknown'; + const rowsIdInfo: IdCardAttributeInfo[] = useMemo( + () => + Object.entries(identity.idObject.value.attributeList.chosenAttributes) + .sort(([left], [right]) => compareAttributes(left, right)) + .map(([key, value]) => ({ + key: getAttributeName(key as AttributeKey), + value: displayAttribute(key, value), + })), + [identity] + ); + const rowsConnectedAccounts = useMemo(() => { + const connectedAccounts = credentials.value.flatMap((cred): IdCardAccountInfo[] => + cred.identityIndex !== identity.index + ? [] + : [ + { + address: displayNameAndSplitAddress(cred), + amount: , + }, + ] + ); + return connectedAccounts.length === 0 ? undefined : connectedAccounts; + }, [credentials, identity]); + return ( + + ); +} -const rowsConnectedAccounts: [string, string][] = [ - ['Accout 1 / 6gk...Fk7o', '4,227.38 USD'], - ['Accout 2 / tt2...50eo', '1,195.41 USD'], - ['Accout 3 / bnh...JJ76', '123.38 USD'], - ['Accout 4 / rijf...8h7T', '7,200.41 USD'], -]; export default function IdCards() { const { t } = useTranslation('x', { keyPrefix: 'idCards' }); + const [identities, setIdentities] = useAtom(identitiesAtom); + const onNewName = (index: number) => (newName: string) => { + const identitiesClone = [...identities]; + identitiesClone[index] = { ...identities[index], name: newName }; + setIdentities(identitiesClone); + }; return ( } /> - + {identities.map((id, index) => { + switch (id.status) { + case CreationStatus.Confirmed: + return ( + + ); + case CreationStatus.Pending: + return null; + case CreationStatus.Rejected: + return null; + default: + return <>Unsupported; + } + })} ); diff --git a/packages/browser-wallet/src/popup/popupX/pages/Onboarding/IdSubmitted.tsx b/packages/browser-wallet/src/popup/popupX/pages/Onboarding/IdSubmitted.tsx index 1cd89b86..3f705e4e 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/Onboarding/IdSubmitted.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/Onboarding/IdSubmitted.tsx @@ -5,10 +5,11 @@ import Page from '@popup/popupX/shared/Page'; import Text from '@popup/popupX/shared/Text'; import { useTranslation } from 'react-i18next'; import IdCard from '@popup/popupX/shared/IdCard'; +import { IdCardAttributeInfo } from '@popup/popupX/shared/IdCard/IdCard'; -const rowsIdInfo: [string, string][] = [ - ['Identity document type', 'Drivers licence'], - ['Identity document number', 'BXM680515'], +const rowsIdInfo: IdCardAttributeInfo[] = [ + { key: 'Identity document type', value: 'Drivers licence' }, + { key: 'Identity document number', value: 'BXM680515' }, ]; export default function IdSubmitted() { @@ -20,7 +21,7 @@ export default function IdSubmitted() { {t('idSubmitInfo')} - + navToNext()} /> diff --git a/packages/browser-wallet/src/popup/popupX/pages/Restore/RestoreResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/Restore/RestoreResult.tsx index b7e40212..065df770 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/Restore/RestoreResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/Restore/RestoreResult.tsx @@ -1,10 +1,11 @@ -import React, { ReactNode } from 'react'; +import React from 'react'; import ArrowRight from '@assets/svgX/arrow-right.svg'; import Button from '@popup/popupX/shared/Button'; import Page from '@popup/popupX/shared/Page'; import Text from '@popup/popupX/shared/Text'; import { useTranslation } from 'react-i18next'; import IdCard from '@popup/popupX/shared/IdCard'; +import { IdCardAttributeInfo } from '@popup/popupX/shared/IdCard/IdCard'; function AccountLink({ account, balance }: { account: string; balance: string }) { return ( @@ -16,9 +17,9 @@ function AccountLink({ account, balance }: { account: string; balance: string }) ); } -const rowsIdInfo: [string, ReactNode][] = [ - ['', ], - ['', ], +const rowsIdInfo: IdCardAttributeInfo[] = [ + { key: '', value: }, + { key: '', value: }, ]; export default function RestoreResult() { @@ -29,7 +30,7 @@ export default function RestoreResult() { {t('recoveredIds')} - + diff --git a/packages/browser-wallet/src/popup/popupX/shared/EditableValue/EditableValue.scss b/packages/browser-wallet/src/popup/popupX/shared/EditableValue/EditableValue.scss new file mode 100644 index 00000000..75e78031 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/EditableValue/EditableValue.scss @@ -0,0 +1,14 @@ +input.editable-value-x { + background: none; + color: inherit; + border: none; + font-weight: inherit; + font-size: inherit; + font-family: inherit; + padding: 0; + margin: 0; + + &:focus { + outline: none; + } +} diff --git a/packages/browser-wallet/src/popup/popupX/shared/EditableValue/EditableValue.tsx b/packages/browser-wallet/src/popup/popupX/shared/EditableValue/EditableValue.tsx new file mode 100644 index 00000000..07c512a7 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/EditableValue/EditableValue.tsx @@ -0,0 +1,43 @@ +import React, { ChangeEvent, useState, KeyboardEvent, useCallback } from 'react'; + +export default function useEditableValue(current: string, fallback: string, onNewValue: (newValue: string) => void) { + const [isEditing, setIsEditing] = useState(false); + const [edited, setEdited] = useState(current); + // Using edited instead of currentValue to avoid flickering after completing. + const displayName = edited === '' ? fallback : edited; + const onAbort = useCallback(() => { + setIsEditing(false); + setEdited(current); + }, [current]); + const onComplete = useCallback(() => { + onNewValue(edited.trim()); + setIsEditing(false); + }, [edited]); + const onEdit = useCallback(() => { + setEdited(current); + setIsEditing(true); + }, [current]); + const onInputChange = (event: ChangeEvent) => { + setEdited(event.target.value); + }; + const onKeyUp = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + onComplete(); + } + }; + const value = isEditing ? ( + + ) : ( + displayName + ); + return { value, isEditing, onAbort, onComplete, onEdit }; +} diff --git a/packages/browser-wallet/src/popup/popupX/shared/EditableValue/index.ts b/packages/browser-wallet/src/popup/popupX/shared/EditableValue/index.ts new file mode 100644 index 00000000..d8b00cb9 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/EditableValue/index.ts @@ -0,0 +1 @@ +export { default } from './EditableValue'; diff --git a/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.tsx b/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.tsx index 29dcebcb..45961bef 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.tsx @@ -2,35 +2,67 @@ import React, { ReactNode } from 'react'; import Card from '@popup/popupX/shared/Card'; import Text from '@popup/popupX/shared/Text'; import Button from '@popup/popupX/shared/Button'; +import useEditableValue from '@popup/popupX/shared/EditableValue'; +import { useTranslation } from 'react-i18next'; -interface IdCardProps { - rowsIdInfo?: [string, string | ReactNode][]; - rowsConnectedAccounts?: [string, string][]; - onEditName?: () => void; -} +export type IdCardAttributeInfo = { + key: string; + value: string | ReactNode; +}; + +export type IdCardAccountInfo = { + address: string; + amount: ReactNode; +}; + +export type IdCardProps = { + idProviderName: string; + identityName: string; + rowsIdInfo?: IdCardAttributeInfo[]; + rowsConnectedAccounts?: IdCardAccountInfo[]; + onNewName?: (newName: string) => void; + identityNameFallback?: string; +}; + +export default function IdCard({ + idProviderName, + identityName, + rowsIdInfo = [], + rowsConnectedAccounts, + onNewName, + identityNameFallback, +}: IdCardProps) { + const editable = useEditableValue(identityName, identityNameFallback ?? '', onNewName ?? (() => {})); + const { t } = useTranslation('x', { keyPrefix: 'sharedX' }); -export default function IdCard({ rowsIdInfo = [], rowsConnectedAccounts, onEditName }: IdCardProps) { return ( - Identity 1 - {onEditName && } + {editable.value} + {editable.isEditing ? ( + <> + + + + ) : ( + onNewName && + )} - Verified by NotaBene + {t('idCard.verifiedBy', { idProviderName })} - {rowsIdInfo.map(([key, value]) => ( - - {key} - {value} + {rowsIdInfo.map((info) => ( + + {info.key} + {info.value} ))} {rowsConnectedAccounts && ( - {rowsConnectedAccounts.map(([key, value]) => ( - - {key} - {value} + {rowsConnectedAccounts.map((account) => ( + + {account.address} + {account.amount} ))} diff --git a/packages/browser-wallet/src/popup/popupX/shared/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/shared/i18n/en.ts index 896b3714..49b2586a 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/i18n/en.ts +++ b/packages/browser-wallet/src/popup/popupX/shared/i18n/en.ts @@ -32,6 +32,14 @@ const t = { zero: 'Amount may not be zero', }, }, + idCard: { + name: { + edit: 'Edit Name', + save: 'Save', + abort: 'Abort', + }, + verifiedBy: 'Verified by {{idProviderName}}', + }, }; export default t; diff --git a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss index c7a48be4..1d11dc03 100644 --- a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss +++ b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss @@ -45,3 +45,4 @@ @import '../shared/Form/Form'; @import '../shared/Form/TokenAmount/TokenAmount'; @import '../shared/FullscreenNotice/FullscreenNotice'; +@import '../shared/EditableValue/EditableValue'; diff --git a/packages/browser-wallet/src/wallet-common-helpers/utils/ccd.ts b/packages/browser-wallet/src/wallet-common-helpers/utils/ccd.ts index d50b61f6..027d4a70 100644 --- a/packages/browser-wallet/src/wallet-common-helpers/utils/ccd.ts +++ b/packages/browser-wallet/src/wallet-common-helpers/utils/ccd.ts @@ -81,5 +81,5 @@ export function displayAsCcd(amount: bigint | string | CcdAmount.Type, ccdPrefix const negative = microCcdAmount < 0n ? '-' : ''; const abs = microCcdAmount < 0n ? -microCcdAmount : microCcdAmount; const formatted = addThousandSeparators(formatCcdString(microCcdToCcd(abs))); - return `${negative}${ccdPrefix ? getCcdSymbol() : ''}${formatted}${ccdPostfix ? ` CCD` : ''}`; + return `${negative}${ccdPrefix ? getCcdSymbol() : ''}${formatted}${ccdPostfix ? `\u00A0CCD` : ''}`; }