diff --git a/packages/browser-wallet/src/assets/svgX/fat-arrow-up.svg b/packages/browser-wallet/src/assets/svgX/fat-arrow-up.svg new file mode 100644 index 000000000..acf04808b --- /dev/null +++ b/packages/browser-wallet/src/assets/svgX/fat-arrow-up.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index b2ce9afbf..b9b27451f 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -133,6 +133,15 @@ export const relativeRoutes = { path: 'private-key/:account', }, }, + createAccount: { + path: 'create-account', + confirm: { + path: 'confirm/:identityProviderIndex/:identityIndex', + }, + config: { + backTitle: '', + }, + }, seedPhrase: { path: 'seedPhrase', config: { 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 04c5850fb..18f98b862 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 from 'react'; +import React, { useCallback } 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'; @@ -112,12 +112,14 @@ function AccountListItem({ credential }: AccountListItemProps) { export default function Accounts() { const { t } = useTranslation('x', { keyPrefix: 'accounts' }); const accounts = useAtomValue(credentialsAtom); + const nav = useNavigate(); + const navToCreateAccount = useCallback(() => nav(absoluteRoutes.settings.createAccount.path), []); return ( } /> } /> - } /> + } onClick={navToCreateAccount} /> {accounts.map((item) => ( diff --git a/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccount.scss b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccount.scss new file mode 100644 index 000000000..90297346d --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccount.scss @@ -0,0 +1,19 @@ +.create-account-x { + .id-card-button { + margin-top: rem(14px); + padding: unset; + background-color: unset; + border: none; + text-align: left; + cursor: pointer; + } + + .justify-content-center { + display: flex; + justify-content: center; + } + + .page__footer { + gap: rem(24px) !important; + } +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccount.tsx b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccount.tsx new file mode 100644 index 000000000..4c1832d93 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccount.tsx @@ -0,0 +1,73 @@ +import React, { useMemo } from 'react'; +import Page from '@popup/popupX/shared/Page'; +import Text from '@popup/popupX/shared/Text'; +import { useTranslation } from 'react-i18next'; +import { identitiesAtom } from '@popup/store/identity'; +import { useAtomValue } from 'jotai'; +import { ConfirmedIdentity, CreationStatus } from '@shared/storage/types'; +import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard'; +import Button from '@popup/popupX/shared/Button'; +import { generatePath, useNavigate } from 'react-router-dom'; +import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import { compareYearMonth, getCurrentYearMonth } from 'wallet-common-helpers'; + +/** + * Get the valid identities, which is Identities that are confirmed by the ID provider and are not + * expired. + * This is not recomputed by change of current time, meaning the returned identities might become + * expired over time. + */ +function useValidIdentities(): ConfirmedIdentity[] { + const identities = useAtomValue(identitiesAtom); + return useMemo(() => { + const now = getCurrentYearMonth(); + return identities.flatMap((id) => { + if (id.status !== CreationStatus.Confirmed) { + return []; + } + // Negative number is indicating that `validTo` is before `now`, therefore expired. + const isExpired = compareYearMonth(id.idObject.value.attributeList.validTo, now) < 0; + if (isExpired) { + return []; + } + return [id]; + }); + }, [identities]); +} + +export default function CreateAccount() { + const { t } = useTranslation('x', { keyPrefix: 'createAccount' }); + const nav = useNavigate(); + const navToCreateAccountConfirm = (identity: ConfirmedIdentity) => () => + nav( + generatePath(absoluteRoutes.settings.createAccount.confirm.path, { + identityProviderIndex: identity.providerIndex.toString(), + identityIndex: identity.index.toString(), + }) + ); + const validIdentities = useValidIdentities(); + + return ( + + + + {t('selectIdentityDescription')} + {validIdentities.length === 0 ? ( +

+ {t('noValidIdentities')} +

+ ) : ( + validIdentities.map((id) => ( + + + + )) + )} +
+
+ ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccountConfirm.tsx b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccountConfirm.tsx new file mode 100644 index 000000000..38522d74d --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/CreateAccountConfirm.tsx @@ -0,0 +1,137 @@ +import React, { useCallback } from 'react'; +import Page from '@popup/popupX/shared/Page'; +import { useTranslation } from 'react-i18next'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; +import Button from '@popup/popupX/shared/Button'; +import { identitiesAtom, identityProvidersAtom } from '@popup/store/identity'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard'; +import { ConfirmedIdentity, CreationStatus } from '@shared/storage/types'; +import FatArrowUp from '@assets/svgX/fat-arrow-up.svg'; +import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; +import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers'; +import { addToastAtom } from '@popup/state'; +import { creatingCredentialRequestAtom, credentialsAtom } from '@popup/store/account'; +import { getGlobal, getNet } from '@shared/utils/network-helpers'; +import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; +import { getNextEmptyCredNumber } from '@popup/shared/utils/account-helpers'; +import { popupMessageHandler } from '@popup/shared/message-handler'; +import { InternalMessageType } from '@messaging'; +import { CredentialDeploymentBackgroundResponse } from '@shared/utils/types'; +import { absoluteRoutes } from '@popup/popupX/constants/routes'; + +/** + * Hook providing function for sending credential deployments. + */ +function useSendCredentialDeployment() { + const providers = useAtomValue(identityProvidersAtom); + const credentials = useAtomValue(credentialsAtom); + const network = useAtomValue(networkConfigurationAtom); + const addToast = useSetAtom(addToastAtom); + const seedPhrase = useDecryptedSeedPhrase((e) => addToast(e.message)); + const client = useAtomValue(grpcClientAtom); + + const loading = !seedPhrase || !network || !providers || !credentials; + const sendCredentialDeployment = useCallback( + async (identity: ConfirmedIdentity) => { + if (loading) { + throw new Error('Still loading relevant information'); + } + + const identityProvider = providers.find((p) => p.ipInfo.ipIdentity === identity.providerIndex); + if (!identityProvider) { + throw new Error('provider not found'); + } + const global = await getGlobal(client); + + // Make request + const credsOfCurrentIdentity = credentials.filter(isIdentityOfCredential(identity)); + const credNumber = getNextEmptyCredNumber(credsOfCurrentIdentity); + + const response: CredentialDeploymentBackgroundResponse = await popupMessageHandler.sendInternalMessage( + InternalMessageType.SendCredentialDeployment, + { + globalContext: global, + ipInfo: identityProvider.ipInfo, + arsInfos: identityProvider.arsInfos, + seedAsHex: seedPhrase, + net: getNet(network), + idObject: identity.idObject.value, + revealedAttributes: [], + identityIndex: identity.index, + credNumber, + } + ); + return response; + }, + [seedPhrase, network, providers, credentials, loading] + ); + + return { loading, sendCredentialDeployment }; +} + +type CreateAccountConfirmProps = { + identityProviderIndex: number; + identityIndex: number; +}; + +function ConfirmInfo({ identityProviderIndex, identityIndex }: CreateAccountConfirmProps) { + const { t } = useTranslation('x', { keyPrefix: 'createAccount' }); + const identities = useAtomValue(identitiesAtom); + const identity = identities.find((id) => id.providerIndex === identityProviderIndex && id.index === identityIndex); + const [creatingCredentialRequest, setCreatingRequest] = useAtom(creatingCredentialRequestAtom); + const deployment = useSendCredentialDeployment(); + const nav = useNavigate(); + const onCreateAccount = useCallback(async () => { + if (identity === undefined || identity.status !== CreationStatus.Confirmed) { + throw new Error(`Invalid identity: ${identity}`); + } + setCreatingRequest(true); + deployment + .sendCredentialDeployment(identity) + .catch(() => {}) + .then(() => { + nav(absoluteRoutes.home.path); + }) + .finally(() => { + setCreatingRequest(false); + }); + }, [deployment.sendCredentialDeployment]); + + if (identity === undefined) { + return null; + } + if (identity.status !== CreationStatus.Confirmed) { + return ; + } + const loading = creatingCredentialRequest.loading || creatingCredentialRequest.value || deployment.loading; + return ( + <> +
+ +
+ + + + ); +} + +export default function CreateAccountConfirm() { + const params = useParams(); + if (params.identityProviderIndex === undefined || params.identityIndex === undefined) { + // No account address passed in the url. + return ; + } + const identityIndex = parseInt(params.identityIndex, 10); + const identityProviderIndex = parseInt(params.identityProviderIndex, 10); + if (Number.isNaN(identityProviderIndex) || Number.isNaN(identityIndex)) { + return ; + } + return ( + + + + + + ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/i18n/en.ts new file mode 100644 index 000000000..22eebf578 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/i18n/en.ts @@ -0,0 +1,11 @@ +const t = { + selectIdentity: 'Select an identity', + noValidIdentities: + 'There are currently no confirmed and non-expired identities. Please issue a new identity first.', + selectIdentityDescription: `The ID Documents (e.g. Passport pictures) that are used for the ID verification, are held exclusively by our trusted, third-party identity providers in their own off-chain records. This means your ID data will not be share on-chain. + +Which identity do you want to use to create the account?`, + confirmButton: 'Create account', +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/index.ts b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/index.ts new file mode 100644 index 000000000..97344cc1b --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/index.ts @@ -0,0 +1,2 @@ +export { default } from './CreateAccount'; +export { default as CreateAccountConfirm } from './CreateAccountConfirm'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/useSendCredentialDeployment.tsx b/packages/browser-wallet/src/popup/popupX/pages/CreateAccount/useSendCredentialDeployment.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx index 59259965a..e95a8c8c2 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx @@ -20,6 +20,7 @@ import { cpStakingCooldown } from '@shared/utils/chain-parameters-helpers'; import { submittedTransactionRoute } from '@popup/popupX/constants/routes'; import Text from '@popup/popupX/shared/Text'; import { useSelectedAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; +import ErrorMessage from '@popup/popupX/shared/Form/ErrorMessage'; import { isRange, showCommissionRate, @@ -27,7 +28,6 @@ import { showValidatorOpenStatus, showValidatorRestake, } from '../util'; -import ErrorMessage from '@popup/popupX/shared/Form/ErrorMessage'; export type ValidationResultLocationState = { payload: ConfigureBakerPayload; 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 53de3f8f8..aa4be8cc6 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx @@ -1,75 +1,12 @@ -import React, { useMemo } from 'react'; +import React 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'; - -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 ( - - ); -} +import { identitiesAtom } from '@popup/store/identity'; +import { useAtom } from 'jotai'; +import { CreationStatus } from '@shared/storage/types'; +import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard'; export default function IdCards() { const { t } = useTranslation('x', { keyPrefix: 'idCards' }); diff --git a/packages/browser-wallet/src/popup/popupX/shared/IdCard/ConfirmedIdCard.tsx b/packages/browser-wallet/src/popup/popupX/shared/IdCard/ConfirmedIdCard.tsx new file mode 100644 index 000000000..93dbe9e5d --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/IdCard/ConfirmedIdCard.tsx @@ -0,0 +1,87 @@ +import React, { useMemo } from 'react'; +import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers'; +import { identityProvidersAtom } from '@popup/store/identity'; +import { useAtomValue } from 'jotai'; +import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; +import { ConfirmedIdentity, WalletCredential } from '@shared/storage/types'; +import { compareAttributes, displayAsCcd } from 'wallet-common-helpers'; +import { credentialsAtomWithLoading } from '@popup/store/account'; +import { AttributeKey } from '@concordium/web-sdk'; +import { displayNameAndSplitAddress } from '@popup/shared/utils/account-helpers'; +import IdCard, { IdCardAccountInfo, IdCardAttributeInfo } from './IdCard'; + +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}`; +} + +export type ConfirmedIdentityProps = { + /** Identity to show. */ + identity: ConfirmedIdentity; + /** Disable showing accounts created with this identity. */ + hideAccounts?: boolean; + /** Limit the shown attributes to the following list, when undefined all attributes will be shown. */ + shownAttributes?: AttributeKey[]; + /** Callback when user changes the name of the identity, no edit button is show when not defined. */ + onNewName?: (name: string) => void; +}; + +export default function ConfirmedIdCard({ + identity, + hideAccounts, + shownAttributes, + 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) + .filter(([key]) => + shownAttributes === undefined ? true : shownAttributes.includes(key as AttributeKey) + ) + .sort(([left], [right]) => compareAttributes(left, right)) + .map(([key, value]) => ({ + key: getAttributeName(key as AttributeKey), + value: displayAttribute(key, value), + })), + [identity] + ); + const rowsConnectedAccounts = useMemo(() => { + if (hideAccounts) { + return undefined; + } + const connectedAccounts = credentials.value.flatMap((cred): IdCardAccountInfo[] => + cred.identityIndex !== identity.index + ? [] + : [ + { + address: displayNameAndSplitAddress(cred), + amount: , + }, + ] + ); + return connectedAccounts.length === 0 ? undefined : connectedAccounts; + }, [credentials, identity, hideAccounts]); + return ( + + ); +} 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 45961bef6..cb901de54 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.tsx @@ -27,7 +27,7 @@ export type IdCardProps = { export default function IdCard({ idProviderName, identityName, - rowsIdInfo = [], + rowsIdInfo, rowsConnectedAccounts, onNewName, identityNameFallback, @@ -49,14 +49,16 @@ export default function IdCard({ )} {t('idCard.verifiedBy', { idProviderName })} - - {rowsIdInfo.map((info) => ( - - {info.key} - {info.value} - - ))} - + {rowsIdInfo && ( + + {rowsIdInfo.map((info) => ( + + {info.key} + {info.value} + + ))} + + )} {rowsConnectedAccounts && ( {rowsConnectedAccounts.map((account) => ( diff --git a/packages/browser-wallet/src/popup/popupX/shared/IdCard/index.ts b/packages/browser-wallet/src/popup/popupX/shared/IdCard/index.ts index 568233bd0..b8ec25862 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/IdCard/index.ts +++ b/packages/browser-wallet/src/popup/popupX/shared/IdCard/index.ts @@ -1 +1,2 @@ export { default } from './IdCard'; +export { default as ConfirmedIdCard } from './ConfirmedIdCard'; diff --git a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx index a94d8cdae..cfa5b6fc1 100644 --- a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx @@ -10,6 +10,7 @@ import TransactionDetails from '@popup/popupX/pages/TransactionDetails'; import { TokenDetails, TokenDetailsCcd, TokenRaw } from '@popup/popupX/pages/TokenDetails'; import IdCards from '@popup/popupX/pages/IdCards'; import Accounts from '@popup/popupX/pages/Accounts'; +import CreateAccount, { CreateAccountConfirm } from '@popup/popupX/pages/CreateAccount'; import SeedPhrase from 'src/popup/popupX/pages/SeedPhrase'; import ChangePasscode from 'src/popup/popupX/pages/ChangePasscode'; import { Web3IdCredentials, Web3IdImport } from '@popup/popupX/pages/Web3Id'; @@ -99,6 +100,13 @@ export default function Routes({ messagePromptHandlers }: { messagePromptHandler /> } path={relativeRoutes.settings.accounts.privateKey.path} /> + + } /> + } + /> + } path={relativeRoutes.settings.seedPhrase.path} /> } path={relativeRoutes.settings.passcode.path} /> diff --git a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss index a179996a2..5dc233dea 100644 --- a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss +++ b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss @@ -3,6 +3,7 @@ @import '../pages/ReceiveFunds/ReceiveFunds'; @import '../pages/IdCards/IdCards'; @import '../pages/Accounts/Accounts'; +@import '../pages/CreateAccount/CreateAccount'; @import '../pages/ConnectedSites/ConnectedSites'; @import '../pages/PrivateKey/PrivateKey'; @import '../pages/NetworkSettings/NetworkSettings'; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index 661424548..076e06b94 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -33,6 +33,7 @@ import onboarding from '@popup/popupX/pages/Onboarding/i18n/en'; import receiveFunds from '@popup/popupX/pages/ReceiveFunds/i18n/en'; import idCards from '@popup/popupX/pages/IdCards/i18n/en'; import accounts from '@popup/popupX/pages/Accounts/i18n/en'; +import createAccount from '@popup/popupX/pages/CreateAccount/i18n/en'; import mainPage from '@popup/popupX/pages/MainPage/i18n/en'; import tokenDetails from '@popup/popupX/pages/TokenDetails/i18n/en'; import restore from '@popup/popupX/pages/Restore/i18n/en'; @@ -87,6 +88,7 @@ const t = { receiveFunds, idCards, accounts, + createAccount, mainPage, tokenDetails, restore, diff --git a/packages/browser-wallet/src/wallet-common-helpers/utils/timeHelpers.ts b/packages/browser-wallet/src/wallet-common-helpers/utils/timeHelpers.ts index 9ac56629d..7caf9609a 100644 --- a/packages/browser-wallet/src/wallet-common-helpers/utils/timeHelpers.ts +++ b/packages/browser-wallet/src/wallet-common-helpers/utils/timeHelpers.ts @@ -55,6 +55,32 @@ export function getCurrentYearMonth(): YearMonth { return date.getFullYear() + month; } +/** + * Function comparing two YearMonth strings. + * + * Returns a number where: + * + * - A negative value indicates that `left` is before `right`. + * - A positive value indicates that `left` is after `right`. + * - Zero indicates that `left` and `right` are equal. + */ +export function compareYearMonth(left: YearMonth, right: YearMonth): number { + const leftYear = parseInt(left.slice(0, 4), 10); + const rightYear = parseInt(right.slice(0, 4), 10); + if (Number.isNaN(leftYear) || Number.isNaN(rightYear)) { + throw new Error('Invalid input for compareYearMonth, unable to parse integer representing the year'); + } + if (leftYear !== rightYear) { + return leftYear - rightYear; + } + const leftMonth = parseInt(left.slice(4, 6), 10); + const rightMonth = parseInt(right.slice(4, 6), 10); + if (Number.isNaN(leftMonth) || Number.isNaN(rightMonth)) { + throw new Error('Invalid input for compareYearMonth, unable to parse integer representing the month'); + } + return leftMonth - rightMonth; +} + /** * Converts a unix timestamp to a Date type. * @param timestamp the unix timestamp, in seconds or milliseconds.