diff --git a/packages/browser-wallet/src/background/identity-issuance.ts b/packages/browser-wallet/src/background/identity-issuance.ts index 6b07bead1..ad72829d5 100644 --- a/packages/browser-wallet/src/background/identity-issuance.ts +++ b/packages/browser-wallet/src/background/identity-issuance.ts @@ -1,7 +1,7 @@ -import { createIdentityRequest, IdentityRequestInput } from '@concordium/web-sdk'; +import { createIdentityRequest } from '@concordium/web-sdk'; import { IdentityIssuanceBackgroundResponse } from '@shared/utils/identity-helpers'; import { ExtensionMessageHandler, InternalMessageType } from '@messaging'; -import { BackgroundResponseStatus } from '@shared/utils/types'; +import { BackgroundResponseStatus, IdentityIssuanceRequestPayload } from '@shared/utils/types'; import { sessionIdpTab, sessionPendingIdentity, storedCurrentNetwork } from '@shared/storage/access'; import { CreationStatus, PendingIdentity } from '@shared/storage/types'; import { buildURLwithSearchParameters } from '@shared/utils/url-helpers'; @@ -150,10 +150,7 @@ function launchExternalIssuance(url: string) { }); } -async function startIdentityIssuance({ - baseUrl, - ...identityRequestInputs -}: IdentityRequestInput & { baseUrl: string }) { +async function startIdentityIssuance({ baseUrl, ...identityRequestInputs }: IdentityIssuanceRequestPayload) { const idObjectRequest = createIdentityRequest(identityRequestInputs); const params = { diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index b9b27451f..f8f9cbb1b 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -115,8 +115,30 @@ export const relativeRoutes = { }, settings: { path: 'settings', - idCards: { - path: 'id-cards', + identities: { + path: 'identities', + create: { + path: 'create', + externalFlow: { + path: 'external-flow', + config: { + hideMenu: true, + }, + }, + submitted: { + path: 'submitted', + config: { + hideMenu: true, + hideBackArrow: true, + }, + }, + failed: { + path: 'failed', + config: { + hideMenu: true, + }, + }, + }, }, about: { path: 'about', @@ -279,6 +301,9 @@ export const relativeRoutes = { connectionRequest: { path: 'connectionRequest', }, + endIdentityIssuance: { + path: 'end-identity-issuance', + }, }, }; diff --git a/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/components/MenuTiles.tsx b/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/components/MenuTiles.tsx index 0430413e2..df9f6e9d9 100644 --- a/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/components/MenuTiles.tsx +++ b/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/components/MenuTiles.tsx @@ -37,7 +37,7 @@ export default function MenuTiles({ menuOpen, setMenuOpen }: MenuTilesProps) { className="main-header__menu-tiles_container" onClick={() => setMenuOpen(false)} > - + {t('identities')} 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 18f98b862..f1b60aae0 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/Accounts/Accounts.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/Accounts/Accounts.tsx @@ -34,7 +34,7 @@ function AccountListItem({ credential }: AccountListItemProps) { nav(generatePath(absoluteRoutes.settings.accounts.privateKey.path, { account: credential.address })); const navToConnectedSites = () => nav(generatePath(absoluteRoutes.settings.accounts.connectedSites.path, { account: credential.address })); - const navToIdCards = () => nav(absoluteRoutes.settings.idCards.path); + const navToIdCards = () => nav(absoluteRoutes.settings.identities.path); const identityName = useIdentityName(credential); const accountInfo = useAccountInfo(credential); const setAccount = useWritableSelectedAccount(credential.address); 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 6695bce71..0d4633fa2 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx @@ -1,15 +1,20 @@ import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useAtom } from 'jotai'; + 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 { identitiesAtom } from '@popup/store/identity'; -import { useAtom } from 'jotai'; import { CreationStatus } from '@shared/storage/types'; import { ConfirmedIdCard, RejectedIdCard } from '@popup/popupX/shared/IdCard'; +import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import PendingIdCard from '@popup/popupX/shared/IdCard/PendingIdCard'; export default function IdCards() { const { t } = useTranslation('x', { keyPrefix: 'idCards' }); + const nav = useNavigate(); const [identities, setIdentities] = useAtom(identitiesAtom); const onNewName = (index: number) => (newName: string) => { const identitiesClone = [...identities]; @@ -19,7 +24,7 @@ export default function IdCards() { return ( - } /> + } onClick={() => nav(absoluteRoutes.settings.identities.create.path)} /> {identities.map((id, index) => { @@ -33,15 +38,9 @@ export default function IdCards() { /> ); case CreationStatus.Pending: - return null; + return ; case CreationStatus.Rejected: - return ( - - ); + return ; default: return <>Unsupported; } diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/ExternalFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/ExternalFlow.tsx new file mode 100644 index 000000000..c5d7fcdcc --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/ExternalFlow.tsx @@ -0,0 +1,83 @@ +import React, { useCallback, useEffect } from 'react'; +import { Location, useLocation, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useSetAtom } from 'jotai'; + +import { InternalMessageType } from '@messaging'; +import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import { LoaderInline } from '@popup/popupX/shared/Loader'; +import Page from '@popup/popupX/shared/Page'; +import Text from '@popup/popupX/shared/Text'; +import { popupMessageHandler } from '@popup/shared/message-handler'; +import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers'; +import { logError } from '@shared/utils/log-helpers'; +import { IdentityIssuanceRequestPayload } from '@shared/utils/types'; +import { pendingIdentityAtom } from '@popup/store/identity'; +import { getNet } from '@shared/utils/network-helpers'; +import Button from '@popup/popupX/shared/Button'; + +import { IdIssuanceFailedLocationState } from './Failed'; +import { IdIssuanceExternalFlowLocationState } from './util'; + +export default function IdIssuanceExternalFlow() { + const { state } = useLocation() as Location & { state: IdIssuanceExternalFlowLocationState }; + const { t } = useTranslation('x', { keyPrefix: 'idIssuance.externalFlow' }); + const updatePendingIdentity = useSetAtom(pendingIdentityAtom); + const nav = useNavigate(); + + const handleError = useCallback( + (message: string) => { + const messageState: IdIssuanceFailedLocationState = { message, backState: state }; + nav(absoluteRoutes.settings.identities.create.failed.path, { state: messageState, replace: true }); + }, + [state] + ); + + const seedPhrase = useDecryptedSeedPhrase((e) => handleError(e.message)); + + const start = useCallback(async () => { + if (seedPhrase === undefined) throw new Error('Seed phrase not available'); + + updatePendingIdentity(state.pendingIdentity); + + const issuanceRequest: IdentityIssuanceRequestPayload = { + globalContext: state.global, + ipInfo: state.provider.ipInfo, + arsInfos: state.provider.arsInfos, + net: getNet(state.pendingIdentity.network), + identityIndex: state.pendingIdentity.identity.index, + arThreshold: Math.min(Object.keys(state.provider.arsInfos).length - 1, 255), + baseUrl: state.provider.metadata.issuanceStart, + seed: seedPhrase, + }; + + const response = await popupMessageHandler.sendInternalMessage( + InternalMessageType.StartIdentityIssuance, + issuanceRequest + ); + + if (!response) { + logError('Failed to issue identity due to internal error'); + handleError('Internal error, please try again.'); + } else { + nav(absoluteRoutes.settings.identities.path, { replace: true }); + } + }, [state, seedPhrase, handleError]); + + useEffect(() => { + if (state !== null && seedPhrase !== undefined) { + start(); + } + }, [start]); + + return ( + + + {t('description')} + + + + + + ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/Failed.tsx b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/Failed.tsx new file mode 100644 index 000000000..39bad57e8 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/Failed.tsx @@ -0,0 +1,36 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Location, useLocation, useNavigate } from 'react-router-dom'; + +import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import Button from '@popup/popupX/shared/Button'; +import Page from '@popup/popupX/shared/Page'; +import Text from '@popup/popupX/shared/Text'; + +import { IdIssuanceExternalFlowLocationState } from './util'; + +export type IdIssuanceFailedLocationState = { message: string; backState?: IdIssuanceExternalFlowLocationState }; + +export default function IdIssuanceFailed() { + const { t } = useTranslation('x', { keyPrefix: 'idIssuance.failed' }); + const { state } = useLocation() as Location & { state: IdIssuanceFailedLocationState }; + const nav = useNavigate(); + + const handleRetry = useCallback(() => { + if (state.backState !== undefined) { + nav(absoluteRoutes.settings.identities.create.externalFlow.path, { state: state.backState, replace: true }); + } else { + nav(absoluteRoutes.settings.identities.create.path, { replace: true }); + } + }, [state.backState]); + + return ( + + + {state.message} + + + + + ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/IdIssuance.scss b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/IdIssuance.scss new file mode 100644 index 000000000..57381c515 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/IdIssuance.scss @@ -0,0 +1,27 @@ +.id-issuance { + &__issuer-btn { + display: flex; + color: $color-white; + align-items: center; + padding: rem(16px); + gap: rem(16px); + border: unset; + border-radius: rem(12px); + background: $color-transaction-bg; + width: 100%; + + &:not(:first-child) { + margin-top: rem(8px); + } + + .identity-provider-icon { + width: rem(64px); + height: rem(32px); + object-position: center; + object-fit: contain; + background: $color-white; + border-radius: rem(4px); + padding: rem(4px); + } + } +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/IdIssuance.tsx b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/IdIssuance.tsx new file mode 100644 index 000000000..71ac10396 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/IdIssuance.tsx @@ -0,0 +1,107 @@ +import { useAtom, useAtomValue } from 'jotai'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import Button from '@popup/popupX/shared/Button'; +import Page from '@popup/popupX/shared/Page'; +import Text from '@popup/popupX/shared/Text'; +import IdentityProviderIcon from '@popup/shared/IdentityProviderIcon'; +import { getIdentityProviders } from '@popup/shared/utils/wallet-proxy'; +import { identitiesAtom, identityProvidersAtom, pendingIdentityAtom } from '@popup/store/identity'; +import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; +import { CreationStatus, IdentityProvider, SessionPendingIdentity } from '@shared/storage/types'; +import { getGlobal } from '@shared/utils/network-helpers'; +import { logErrorMessage } from '@shared/utils/log-helpers'; + +import { IdIssuanceExternalFlowLocationState } from './util'; + +export default function IdIssuance() { + const { t } = useTranslation('x', { keyPrefix: 'idIssuance.idIssuer' }); + const [providers, setProviders] = useAtom(identityProvidersAtom); + const network = useAtomValue(networkConfigurationAtom); + const identities = useAtomValue(identitiesAtom); + const client = useAtomValue(grpcClientAtom); + const nav = useNavigate(); + const [buttonDisabled, setButtonDisabled] = useState(false); + const [pendingIdentity, setPendingIdentity] = useAtom(pendingIdentityAtom); + + useEffect(() => { + // TODO: only load once per session? + getIdentityProviders() + .then(setProviders) + // eslint-disable-next-line no-console + .catch(() => logErrorMessage('Unable to update identity provider list')); + }, []); + + const startIssuance = useCallback( + async (provider: IdentityProvider) => { + setButtonDisabled(true); + try { + if (!network) { + throw new Error('Network is not specified'); + } + + const global = await getGlobal(client); + const providerIndex = provider.ipInfo.ipIdentity; + const identityIndex = identities.reduce( + (maxIndex, identity) => + identity.providerIndex === providerIndex ? Math.max(maxIndex, identity.index + 1) : maxIndex, + 0 + ); + + const identity: SessionPendingIdentity = { + identity: { + status: CreationStatus.Pending, + index: identityIndex, + name: `Identity ${identities.length + 1}`, + providerIndex, + }, + network, + }; + + const issuanceParams: IdIssuanceExternalFlowLocationState = { + global, + provider, + pendingIdentity: identity, + }; + nav(absoluteRoutes.settings.identities.create.externalFlow.path, { state: issuanceParams }); + } catch { + setButtonDisabled(false); + } + }, + [network] + ); + + return ( + + + {pendingIdentity !== undefined ? ( + {t('descriptionOngoing')} + ) : ( + {t('description')} + )} + {pendingIdentity === undefined && ( +
+ {providers.map((p) => ( + startIssuance(p)} + > + + {p.metadata.display ?? p.ipInfo.ipDescription.name} + + ))} +
+ )} + {pendingIdentity !== undefined && ( + + setPendingIdentity(undefined)} /> + + )} +
+ ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/Submitted.tsx b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/Submitted.tsx new file mode 100644 index 000000000..42c37dcea --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/Submitted.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAtomValue } from 'jotai'; + +import Button from '@popup/popupX/shared/Button'; +import { ConfirmedIdCard, RejectedIdCard, PendingIdCard } from '@popup/popupX/shared/IdCard'; +import Page from '@popup/popupX/shared/Page'; +import Text from '@popup/popupX/shared/Text'; +import { identitiesAtomWithLoading } from '@popup/store/identity'; +import { CreationStatus } from '@shared/storage/types'; +import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import { isSpawnedWindow } from '@popup/shared/window-helpers'; +import { useNavigate } from 'react-router-dom'; + +export default function IdIssuanceSubmitted() { + const { t } = useTranslation('x', { keyPrefix: 'idIssuance.submitted' }); + const { loading, value: identities } = useAtomValue(identitiesAtomWithLoading); + const nav = useNavigate(); + + const handleDone = () => { + if (isSpawnedWindow) { + window.close(); + } else { + nav(absoluteRoutes.settings.identities.path, { replace: true }); + } + }; + + if (loading) { + return null; + } + + const identity = identities.slice(-1)[0]; + + return ( + + + {t('description')} +
+ {identity.status === CreationStatus.Pending && } + {identity.status === CreationStatus.Rejected && } + {identity.status === CreationStatus.Confirmed && } +
+ + + +
+ ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/i18n/en.ts new file mode 100644 index 000000000..e05858fd1 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/i18n/en.ts @@ -0,0 +1,34 @@ +const en = { + idIssuer: { + title: 'Request an identity', + description: + '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.\n\nChoose one of the identity providers below to request a Concordium Identity and create an account.', + descriptionOngoing: + 'An identity issuance process is ongoing in the browser. Please follow the steps to complete the process for the identity provider.\n\nIf you wish to abort the flow, or try again, press "Reset" below.', + buttonContinue: 'Request Identity', + buttonReset: 'Reset', + }, + externalFlow: { + description: + 'Your request is being built. Please do not close the browser. When a new tab opens from the identity provider please follow their process to create your identity.', + descriptionOngoing: + 'An identity issuance process is ongoing in the browser. Please follow the steps to complete the process for the identity provider.\n\nIf you wish to abort the flow, or try again, press "Reset" below.', + buttonReset: 'Reset', + }, + failed: { + title: 'Error', + buttonRetry: 'Try again', + }, + submitted: { + title: 'Your Concordium identity', + description: + 'Your request has been submitted to the identity provider. It may take a little while for them to confirm your identity.\n\nOnce your identity has been verified, you will be able to open an account with it.', + buttonContinue: 'Done', + }, + aborted: { + message: + 'The identity request was aborted. If you did not abort the process, please try again, or contact support.', + }, +}; + +export default en; diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/index.ts b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/index.ts new file mode 100644 index 000000000..7951af6ca --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/index.ts @@ -0,0 +1 @@ +export { default } from './IdIssuance'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/util.ts b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/util.ts new file mode 100644 index 000000000..e0e05117e --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/IdIssuance/util.ts @@ -0,0 +1,9 @@ +import { CryptographicParameters } from '@concordium/web-sdk'; +import { IdentityProvider, SessionPendingIdentity } from '@shared/storage/types'; + +/** The necessary state for the {@linkcode IdIssuanceExternalFlow} page. */ +export type IdIssuanceExternalFlowLocationState = { + global: CryptographicParameters; + provider: IdentityProvider; + pendingIdentity: SessionPendingIdentity; +}; diff --git a/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.tsx b/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.tsx index 9ae5560a9..a4dc8446a 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.tsx @@ -221,7 +221,7 @@ function MainPage({ credential }: MainPageProps) { case CreationStatus.Rejected: return <>Account Creation was rejected; default: - throw new Error(`Unexpected status for credential: ${credential.status}`); + throw new Error(`Unexpected status for credential`); } } diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/EndIdentityIssuance.tsx b/packages/browser-wallet/src/popup/popupX/pages/prompts/EndIdentityIssuance.tsx new file mode 100644 index 000000000..72a335b2c --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/EndIdentityIssuance.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Location, Navigate, useLocation } from 'react-router-dom'; + +import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import { IdentityIssuanceBackgroundResponse } from '@shared/utils/identity-helpers'; +import { BackgroundResponseStatus } from '@shared/utils/types'; +import { useTranslation } from 'react-i18next'; +import { WalletEvent } from '@messaging'; + +export default function EndIdentityIssuance() { + const { + state: { payload }, + } = useLocation() as Location & { + state: WalletEvent & { payload: IdentityIssuanceBackgroundResponse }; + }; + const { t } = useTranslation('x', { keyPrefix: 'idIssuance.aborted' }); + + switch (payload.status) { + case BackgroundResponseStatus.Success: + return ; + case BackgroundResponseStatus.Error: + return ( + + ); + case BackgroundResponseStatus.Aborted: + return ( + + ); + default: + throw new Error('Unsupported status from background'); + } +} diff --git a/packages/browser-wallet/src/popup/popupX/shared/Button/Button.scss b/packages/browser-wallet/src/popup/popupX/shared/Button/Button.scss index 635075a8e..9c0a7951a 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Button/Button.scss +++ b/packages/browser-wallet/src/popup/popupX/shared/Button/Button.scss @@ -1,6 +1,7 @@ .button { &__base { cursor: pointer; + font-family: $t-font-family-satoshi; &:focus-visible { outline: 1px solid $color-white; diff --git a/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.scss b/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.scss index b54a88223..b3ef98b88 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.scss +++ b/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.scss @@ -32,6 +32,7 @@ .card-x { &.grey { + padding: rem(16px); margin-top: rem(16px); background: $color-grey-1; 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 541a43549..7b8d959ba 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/IdCard/IdCard.tsx @@ -1,17 +1,20 @@ import React, { ReactNode } from 'react'; +import { ClassName } from 'wallet-common-helpers'; +import clsx from 'clsx'; + import Card from '@popup/popupX/shared/Card'; import Text from '@popup/popupX/shared/Text'; -export type IdCardBaseProps = { +export type IdCardBaseProps = ClassName & { title: ReactNode; subtitle: ReactNode; titleAction?: ReactNode; children?: ReactNode; }; -export default function IdCard({ title, subtitle, titleAction, children }: IdCardBaseProps) { +export default function IdCard({ title, subtitle, titleAction, children, className }: IdCardBaseProps) { return ( - + {title} {titleAction} diff --git a/packages/browser-wallet/src/popup/popupX/shared/IdCard/PendingIdCard.tsx b/packages/browser-wallet/src/popup/popupX/shared/IdCard/PendingIdCard.tsx new file mode 100644 index 000000000..e1747d3c5 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/IdCard/PendingIdCard.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { identityProvidersAtom } from '@popup/store/identity'; +import { useAtomValue } from 'jotai'; +import { PendingIdentity } from '@shared/storage/types'; +import { useTranslation } from 'react-i18next'; +import Text from '@popup/popupX/shared/Text'; +import IdCard from './IdCard'; + +export type PendingIdentityProps = { + /** Identity to show. */ + identity: PendingIdentity; +}; + +export default function PendingIdCard({ identity }: PendingIdentityProps) { + const { t } = useTranslation('x', { keyPrefix: 'sharedX' }); + const providers = useAtomValue(identityProvidersAtom); + const provider = providers.find((p) => p.ipInfo.ipIdentity === identity.providerIndex); + const idProviderName = provider?.ipInfo.ipDescription.name ?? 'Unknown'; + return ( + + + + ... + + + ... + + + + ); +} 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 4c637214b..53926a3d1 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/IdCard/index.ts +++ b/packages/browser-wallet/src/popup/popupX/shared/IdCard/index.ts @@ -1,3 +1,4 @@ export { default } from './IdCard'; export { default as ConfirmedIdCard } from './ConfirmedIdCard'; export { default as RejectedIdCard } from './RejectedIdCard'; +export { default as PendingIdCard } from './PendingIdCard'; 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 e5accb532..7ffe726c1 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/i18n/en.ts +++ b/packages/browser-wallet/src/popup/popupX/shared/i18n/en.ts @@ -40,6 +40,7 @@ const t = { }, verifiedBy: 'Verified by {{idProviderName}}', rejectedBy: 'Rejected by {{idProviderName}}', + pendingBy: 'Pending verification by {{idProviderName}}', itentityRejected: 'This identity has been rejected', }, }; diff --git a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx index cfa5b6fc1..53b475972 100644 --- a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx @@ -45,6 +45,11 @@ import { } from '../pages/EarningRewards/Validator/TransactionFlow'; import ValidationResult from '../pages/EarningRewards/Validator/Result/ValidationResult'; import UpdateValidator from '../pages/EarningRewards/Validator/Update'; +import IdIssuance from '../pages/IdIssuance'; +import IdIssuanceSubmitted from '../pages/IdIssuance/Submitted'; +import IdIssuanceExternalFlow from '../pages/IdIssuance/ExternalFlow'; +import IdIssuanceFailed from '../pages/IdIssuance/Failed'; +import EndIdentityIssuance from '../pages/prompts/EndIdentityIssuance'; export default function Routes({ messagePromptHandlers }: { messagePromptHandlers: MessagePromptHandlersType }) { const { handleConnectionResponse } = messagePromptHandlers; @@ -91,7 +96,24 @@ export default function Routes({ messagePromptHandlers }: { messagePromptHandler } path={relativeRoutes.settings.path}> - } path={relativeRoutes.settings.idCards.path} /> + + } /> + + } index /> + } + path={relativeRoutes.settings.identities.create.externalFlow.path} + /> + } + path={relativeRoutes.settings.identities.create.submitted.path} + /> + } + path={relativeRoutes.settings.identities.create.failed.path} + /> + + } /> } /> + } /> diff --git a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss index 5dc233dea..785bd9bff 100644 --- a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss +++ b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss @@ -2,6 +2,7 @@ @import '../pages/Restore/Restore'; @import '../pages/ReceiveFunds/ReceiveFunds'; @import '../pages/IdCards/IdCards'; +@import '../pages/IdIssuance/IdIssuance'; @import '../pages/Accounts/Accounts'; @import '../pages/CreateAccount/CreateAccount'; @import '../pages/ConnectedSites/ConnectedSites'; 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 076e06b94..b684910b0 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -52,6 +52,7 @@ import mangeTokens from '@popup/popupX/pages/ManageTokens/i18n/en'; import connectionRequestX from '@popup/popupX/pages/prompts/ConnectionRequest/i18n/en'; import submittedTransaction from '@popup/popupX/pages/SubmittedTransaction/i18n/en'; import nft from '@popup/popupX/pages/Nft/i18n/en'; +import idIssuance from '@popup/popupX/pages/IdIssuance/i18n/en'; const t = { shared, @@ -87,6 +88,7 @@ const t = { onboarding, receiveFunds, idCards, + idIssuance, accounts, createAccount, mainPage, diff --git a/packages/browser-wallet/src/shared/utils/types.ts b/packages/browser-wallet/src/shared/utils/types.ts index dc1f2d3cc..eb43ba008 100644 --- a/packages/browser-wallet/src/shared/utils/types.ts +++ b/packages/browser-wallet/src/shared/utils/types.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-types */ import { SchemaWithContext } from '@concordium/browser-wallet-api-helpers'; -import type { SchemaVersion, AccountTransactionType } from '@concordium/web-sdk'; +import type { SchemaVersion, AccountTransactionType, IdentityRequestInput } from '@concordium/web-sdk'; import { RefAttributes } from 'react'; /** * @description @@ -86,3 +86,5 @@ export type BackgroundSendTransactionPayload = { schemaVersion?: SchemaVersion; url: string; }; + +export type IdentityIssuanceRequestPayload = IdentityRequestInput & { baseUrl: string };