diff --git a/.buildignore b/.buildignore index 51289604a..1824a52a0 100644 --- a/.buildignore +++ b/.buildignore @@ -3,4 +3,4 @@ tsconfig.json yarn.lock README.md .gitignore -*.tar.gz \ No newline at end of file +*.tar.gz diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f0de91639 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig: https://EditorConfig.org + +[*] +charset = utf-8 +insert_final_newline = true # Type of newline is managed by git in .gitattributes +trim_trailing_whitespace = true + +[{*.{Dockerfile,css,js,jsx,ts,tsx},Dockerfile}] +indent_style = tab + +[*.{yml,yaml}] # YAML does not allow tab indentation +indent_style = space +indent_size = 2 diff --git a/.env.template b/.env.template index 64e2d73e6..7d91723cb 100644 --- a/.env.template +++ b/.env.template @@ -11,5 +11,6 @@ REACT_APP_FIREBASE_STORAGE_BUCKET= REACT_APP_FIREBASE_MESSAGING_SENDER_ID= REACT_APP_FIREBASE_APP_ID= REACT_APP_FIREBASE_MEASUREMENT_ID= +REACT_APP_DID_KEY_VERSION=jwk_jcs-pub REACT_APP_VERSION=$npm_package_version -REACT_APP_DEV_CONSOLE_TYPES=info,warn,error \ No newline at end of file +REACT_APP_DISPLAY_CONSOLE=true diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml new file mode 100644 index 000000000..d7142ab16 --- /dev/null +++ b/.github/workflows/code-formatting.yml @@ -0,0 +1,26 @@ +# This name is shown in status badges +name: code-formatting + +on: + push: + branches-ignore: + - 'tmp**' + pull_request: + branches-ignore: + - 'tmp**' + +jobs: + editorconfig: + name: Check EditorConfig compliance + + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up editorconfig-checker + uses: editorconfig-checker/action-editorconfig-checker@v2 + + - name: Check code formatting + run: editorconfig-checker diff --git a/.gitignore b/.gitignore index 5157b62be..56d4b421c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,4 @@ src/config/config.dev.ts src/config/config.prod.js ssl_keys/* *.tar.gz -.npmrc \ No newline at end of file +.npmrc diff --git a/.htaccess b/.htaccess index 152dce1fe..62d0312c1 100644 --- a/.htaccess +++ b/.htaccess @@ -1,4 +1,4 @@ Options -MultiViews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f -RewriteRule ^ index.html [QSA,L] \ No newline at end of file +RewriteRule ^ index.html [QSA,L] diff --git a/.vscode/settings.json b/.vscode/settings.json index 4a664bbc1..e70c03c9d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,4 @@ "editor.tabSize": 2, "editor.detectIndentation": false, "editor.insertSpaces": false -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile index cdc2ec3a6..c3dcb8950 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /home/node/app COPY . . RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ - yarn cache clean -f && yarn install && yarn build + yarn cache clean -f && yarn install && yarn build FROM nginx:alpine as deploy @@ -17,4 +17,4 @@ COPY --from=builder /home/node/app/build/ . EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 5c98cf20f..bc2d9b4f3 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Our Web Wallet provides a range of features tailored to enhance the credential m ```bash git clone https://github.com/your-username/wallet-frontend.git ``` - + - **Option 2: Using SSH** ```bash git clone git@github.com:your-username/wallet-frontend.git @@ -63,14 +63,14 @@ The project uses environment variables to manage different configurations. A `.e - REACT_APP_WS_URL: The URL of the websocket service. - REACT_APP_WALLET_BACKEND_URL: The URL of your backend service. - REACT_APP_LOGIN_WITH_PASSWORD: A Boolean value which show/hide the classic login/signup. - - REACT_APP_FIREBASE_API_KEY: Your API key for Firebase. + - REACT_APP_FIREBASE_API_KEY: Your API key for Firebase. - REACT_APP_FIREBASE_AUTH_DOMAIN: Your Firebase authentication domain. - REACT_APP_FIREBASE_PROJECT_ID: Your Firebase project ID. - REACT_APP_FIREBASE_STORAGE_BUCKET: Your Firebase storage bucket. - REACT_APP_FIREBASE_MESSAGING_SENDER_ID: Your Firebase Messaging Sender ID. - - REACT_APP_FIREBASE_APP_ID: Your Firebase App ID. + - REACT_APP_FIREBASE_APP_ID: Your Firebase App ID. - REACT_APP_FIREBASE_MEASUREMENT_ID: Your Firebase Measurement ID. - - REACT_APP_DEV_CONSOLE_TYPES: Enable console logs (info, warn, error) separated by commas or leave empty for none. + - REACT_APP_DISPLAY_CONSOLE: Handle console logs (`true` or `false`). If left empty, it will be handled as `true`. 4. Install dependencies: ```bash @@ -101,7 +101,7 @@ The PRF (Pseudo Random Function) extension in WebAuthn enables the evaluation of | Windows | ✔ | ✔ | ❌ | ✔ | | ✔ | | MacOS | ✔ | ✔ | ❌ | ✔ | ❌ | ✔ | | Android | ✔ | ✔ | ❌ | ✔ | | ✔ | -| iOS | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| iOS | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ### PRF Compatibility Scenarios @@ -123,7 +123,7 @@ The PRF (Pseudo Random Function) extension in WebAuthn enables the evaluation of | iOS | FIDO Security Key | NFC | ❌ | -***Note:** In this table, we use the term "FIDO Security Key" to refer to compatible security keys. It's important to understand that any security key should work with the hmac-secret extension, provided it supports this feature. +***Note:** In this table, we use the term "FIDO Security Key" to refer to compatible security keys. It's important to understand that any security key should work with the hmac-secret extension, provided it supports this feature. For a detailed list of security key models that support hmac-secret, you can refer to the [FIDO MDS Explorer](https://opotonniee.github.io/fido-mds-explorer/), where hmac-secret support is listed under metadataStatement > authenticatorGetInfo > extensions.* The wwWallet is committed to delivering a secure and adaptable authentication experience with an emphasis on PRF extension compatibility. @@ -162,7 +162,7 @@ We welcome contributions from the community to help improve the wwWallet Fronten 1. **Create a New Branch:** Create a new branch for your feature or bug fix - ```bash + ```bash git checkout -b my-feature ``` Replace my-feature with a descriptive name. @@ -172,14 +172,14 @@ We welcome contributions from the community to help improve the wwWallet Fronten 3. **Commit Changes:** Commit your changes with a descriptive commit message: - ```bash + ```bash git commit -m "Add new feature" ``` 4. **Push Changes:** Push your changes to your new branrch: - ```bash + ```bash git push --set-upstream origin my-feature - ``` + ``` 5. **Create a Pull Request:** Open a pull request on the original repository. Provide a detailed description of your changes and their purpose. diff --git a/development.Dockerfile b/development.Dockerfile index 2e7b892cd..4e7008f8a 100644 --- a/development.Dockerfile +++ b/development.Dockerfile @@ -5,7 +5,7 @@ WORKDIR /dependencies # Install dependencies first so rebuild of these layers is only needed when dependencies change COPY package.json yarn.lock . RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ - yarn install && yarn cache clean -f + yarn install && yarn cache clean -f FROM node:16-bullseye-slim as development diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 38a61e01f..f03614a8d 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -8,4 +8,4 @@ server { } # Add any additional Nginx configuration here as needed -} \ No newline at end of file +} diff --git a/package.json b/package.json index 9154b342e..fe8cfea46 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,21 @@ "private": true, "dependencies": { "@cef-ebsi/key-did-resolver": "^1.1.0", + "@sd-jwt/core": "^0.2.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@wwwallet/ssi-sdk": "^1.0.7", "autoprefixer": "^10.4.14", "axios": "^1.4.0", + "did-resolver": "^4.1.0", "firebase": "^10.1.0", "i18next": "^23.2.11", "jose": "^4.14.4", "js-cookie": "^3.0.5", "jsqr": "^1.4.0", + "key-did-resolver": "^3.0.0", + "multiformats": "^12.1.3", "postcss": "^8.4.25", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -26,6 +30,7 @@ "react-scripts": "5.0.1", "react-slick": "^0.29.0", "react-snowfall": "^1.2.1", + "react-transition-group": "^4.4.5", "react-webcam": "^7.2.0", "reactour": "^1.19.2", "slick-carousel": "^1.8.1", diff --git a/postcss.config.js b/postcss.config.js index 9a5389100..5c1449894 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ module.exports = { require('tailwindcss'), require('autoprefixer'), ], -}; \ No newline at end of file +}; diff --git a/public/index.html b/public/index.html index 4cb5b182d..727977286 100644 --- a/public/index.html +++ b/public/index.html @@ -4,26 +4,10 @@ - - - - + + + - wwWallet diff --git a/public/manifest.json b/public/manifest.json index 2932a7fad..f0a051b5e 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,5 +1,5 @@ { - "short_name": "ediplomas DW", + "short_name": "wwWallet", "name": "wwWallet", "icons": [ { @@ -7,11 +7,6 @@ "sizes": "16x16", "type": "image/png" }, - { - "src": "wallet_24.png", - "sizes": "24x24", - "type": "image/png" - }, { "src": "wallet_32.png", "sizes": "32x32", @@ -26,7 +21,6 @@ "src": "wallet_192.png", "sizes": "192x192", "type": "image/png" - }, { "src": "wallet_512.png", @@ -36,6 +30,11 @@ ], "start_url": ".", "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" + "orientation": "any", + "theme_color": "#003476", + "description": "wwWallet enables secure storage and management of verifiable credentials.", + "background_color": "#ffffff", + "scope": "/", + "dir": "ltr", + "lang": "en" } diff --git a/src/App.js b/src/App.js index aa0f57628..b85e1c1fc 100644 --- a/src/App.js +++ b/src/App.js @@ -11,17 +11,19 @@ import handleServerMessagesGuard from './hoc/handleServerMessagesGuard'; import HandlerNotification from './components/HandlerNotification'; import Snowfalling from './components/ChistmasAnimation/Snowfalling' -const Settings = React.lazy(() => import('./pages/Settings/Settings')); +import Home from './pages/Home/Home'; +import History from './pages/History/History'; +import Settings from './pages/Settings/Settings'; +import AddCredentials from './pages/AddCredentials/AddCredentials'; +import SendCredentials from './pages/SendCredentials/SendCredentials'; + const Login = React.lazy(() => import('./pages/Login/Login')); -const Home = React.lazy(() => import('./pages/Home/Home')); -const AddCredentials = React.lazy(() => import('./pages/AddCredentials/AddCredentials')); -const SendCredentials = React.lazy(() => import('./pages/SendCredentials/SendCredentials')); -const History = React.lazy(() => import('./pages/History/History')); const NotFound = React.lazy(() => import('./pages/NotFound/NotFound')); const PrivateRoute = React.lazy(() => import('./components/PrivateRoute')); const CredentialDetail = React.lazy(() => import('./pages/Home/CredentialDetail')); -const Popup = React.lazy(() => import('./components/Popup')); -const PinInputPopup = React.lazy(() => import('./components/PinInputPopup')); +const SelectCredentialsPopup = React.lazy(() => import('./components/Popups/SelectCredentials')); +const PinInputPopup = React.lazy(() => import('./components/Popups/PinInput')); +const MessagePopup = React.lazy(() => import('./components/Popups/MessagePopup')); const VerificationResult = React.lazy(() => import('./pages/VerificationResult/VerificationResult')); @@ -44,13 +46,17 @@ function App() { const url = window.location.href; const { - isValidURL, - showPopup, - setShowPopup, + showSelectCredentialsPopup, + setShowSelectCredentialsPopup, setSelectionMap, conformantCredentialsMap, - showPinPopup, - setShowPinPopup, + showPinInputPopup, + setShowPinInputPopup, + verifierDomainName, + showMessagePopup, + setMessagePopup, + textMessagePopup, + typeMessagePopup, } = useCheckURL(url); useEffect(() => { @@ -92,11 +98,14 @@ function App() { } /> } /> - {showPopup && - + {showSelectCredentialsPopup && + + } + {showPinInputPopup && + } - {showPinPopup && - + {showMessagePopup && + setMessagePopup(false)} /> } diff --git a/src/ConsoleBehavior.js b/src/ConsoleBehavior.js index eaa9381aa..8e2039057 100644 --- a/src/ConsoleBehavior.js +++ b/src/ConsoleBehavior.js @@ -1,32 +1,16 @@ -function isMethodAllowed(method) { - if (process.env.NODE_ENV === 'production') { - return false; - } - - const allowedMethodsEnv = process.env.REACT_APP_DEV_CONSOLE_TYPES?.split(',') || []; - const methodCategories = { - log: 'info', - info: 'info', - warn: 'warn', - error: 'error', - assert: 'error' - }; - - return allowedMethodsEnv.includes(methodCategories[method]); -} +//ConsoleBehavior.js function ConsoleBehavior() { - const originalConsole = { ...console }; + // If displayConsole is undefined, proceed as true + const displayConsole = process.env.REACT_APP_DISPLAY_CONSOLE; - Object.keys(console).forEach(method => { - if (typeof console[method] === 'function') { - console[method] = (...args) => { - if (isMethodAllowed(method)) { - originalConsole[method](...args); - } - }; - } - }); + if (displayConsole === 'false') { + Object.keys(console).forEach(method => { + if (typeof console[method] === 'function') { + console[method] = () => { }; + } + }); + } } -export default ConsoleBehavior; \ No newline at end of file +export default ConsoleBehavior; diff --git a/src/api/index.ts b/src/api/index.ts index 1c05fbc42..f21292d5e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -4,8 +4,8 @@ import { Err, Ok, Result } from 'ts-results'; import { jsonParseTaggedBinary, jsonStringifyTaggedBinary, toBase64Url } from '../util'; import { CachedUser, LocalStorageKeystore, makePrfExtensionInputs } from '../services/LocalStorageKeystore'; import { UserData, Verifier } from './types'; -import { useMemo } from 'react'; -import { useClearStorages, useSessionStorage } from '../components/useStorage'; +import { useEffect, useMemo } from 'react'; +import { UseStorageHandle, useClearStorages, useSessionStorage } from '../components/useStorage'; const walletBackendUrl = process.env.REACT_APP_WALLET_BACKEND_URL; @@ -29,6 +29,13 @@ type SignupWebauthnError = ( ); type SignupWebauthnRetryParams = { beginData: any, credential: PublicKeyCredential }; + +export type ClearSessionEvent = {}; +export const CLEAR_SESSION_EVENT = 'clearSession'; +export type ApiEventType = typeof CLEAR_SESSION_EVENT; +const events: EventTarget = new EventTarget(); + + export interface BackendApi { del(path: string): Promise, get(path: string): Promise, @@ -59,6 +66,11 @@ export interface BackendApi { promptForPrfRetry: () => Promise, retryFrom?: SignupWebauthnRetryParams, ): Promise>, + + addEventListener(type: ApiEventType, listener: EventListener, options?: boolean | AddEventListenerOptions): void, + removeEventListener(type: ApiEventType, listener: EventListener, options?: boolean | EventListenerOptions): void, + /** Register a storage hook handle to be cleared when `useApi().clearSession()` is invoked. */ + useClearOnClearSession(storageHandle: UseStorageHandle): UseStorageHandle, } export function useApi(): BackendApi { @@ -137,6 +149,7 @@ export function useApi(): BackendApi { function clearSession(): void { clearSessionStorage(); + events.dispatchEvent(new CustomEvent(CLEAR_SESSION_EVENT)); } function setSession(response: AxiosResponse, credential: PublicKeyCredential | null, authenticationType: 'signup' | 'login', showWelcome: boolean): void { @@ -410,6 +423,29 @@ export function useApi(): BackendApi { } } + function addEventListener(type: ApiEventType, listener: EventListener, options?: boolean | AddEventListenerOptions): void { + events.addEventListener(type, listener, options); + } + + function removeEventListener(type: ApiEventType, listener: EventListener, options?: boolean | EventListenerOptions): void { + events.removeEventListener(type, listener, options); + } + + function useClearOnClearSession(storageHandle: UseStorageHandle): UseStorageHandle { + const [, , clearHandle] = storageHandle; + useEffect( + () => { + const listener = () => { clearHandle(); }; + addEventListener(CLEAR_SESSION_EVENT, listener); + return () => { + removeEventListener(CLEAR_SESSION_EVENT, listener); + }; + }, + [clearHandle] + ); + return storageHandle; + } + return { del, get, @@ -430,6 +466,10 @@ export function useApi(): BackendApi { loginWebauthn, signupWebauthn, + + addEventListener, + removeEventListener, + useClearOnClearSession, } }, [ diff --git a/src/components/BottomNav.js b/src/components/BottomNav.js new file mode 100644 index 000000000..a6a9abe34 --- /dev/null +++ b/src/components/BottomNav.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { FaWallet, FaUserCircle } from "react-icons/fa"; +import { IoIosTime, IoIosAddCircle, IoIosSend } from "react-icons/io"; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +const BottomNav = ({ isOpen, toggle }) => { + const location = useLocation(); + const navigate = useNavigate(); + const { t } = useTranslation(); + + const navItems = [ + { icon: , path: '/', label: `${t("common.navItemCredentials")}` }, + { icon: , path: '/history', label: `${t("common.navItemHistory")}` }, + { icon: , path: '/add', label: `${t("common.navItemAddCredentialsSimple")}` }, + { icon: , path: '/send', label: `${t("common.navItemSendCredentialsSimple")}` }, + ]; + + const handleNavigate = (path) => { + if (isOpen) { + toggle(); + } + + if (location.pathname !== path) { + navigate(path); + } + }; + + return ( + + ); +}; + +export default BottomNav; diff --git a/src/components/ChistmasAnimation/Snowfalling.js b/src/components/ChistmasAnimation/Snowfalling.js index a83c8df40..0c8ec8691 100644 --- a/src/components/ChistmasAnimation/Snowfalling.js +++ b/src/components/ChistmasAnimation/Snowfalling.js @@ -2,27 +2,27 @@ import React, { useEffect, useState } from 'react'; import Snowfall from 'react-snowfall'; const Snowfalling = () => { - const [isChristmasSeason, setIsChristmasSeason] = useState(false); + const [isChristmasSeason, setIsChristmasSeason] = useState(false); - useEffect(() => { - const checkSeason = () => { - const today = new Date(); - const currentYear = today.getFullYear(); - - const start = new Date(currentYear, 11, 20); - const end = new Date(currentYear + 1, 0, 6); - - return today >= start && today <= end; - }; + useEffect(() => { + const checkSeason = () => { + const today = new Date(); + const currentYear = today.getFullYear(); - setIsChristmasSeason(checkSeason()); - }, []); + const start = new Date(currentYear, 11, 20); + const end = new Date(currentYear + 1, 0, 6); - return ( - <> - {isChristmasSeason && } - - ); + return today >= start && today <= end; + }; + + setIsChristmasSeason(checkSeason()); + }, []); + + return ( + <> + {isChristmasSeason && } + + ); } export default Snowfalling; diff --git a/src/components/Credentials/ApiFetchCredential.ts b/src/components/Credentials/ApiFetchCredential.ts deleted file mode 100644 index 272ba1400..000000000 --- a/src/components/Credentials/ApiFetchCredential.ts +++ /dev/null @@ -1,45 +0,0 @@ -// apiUtils.js - -import { BackendApi } from '../../api'; -import parseJwt from '../../functions/ParseJwt'; - -export async function fetchCredentialData(api: BackendApi, id = null) { - try { - const response = await api.get('/storage/vc'); - - if (id) { - const targetImage = response.data.vc_list.find((img) => img.id.toString() === id); - const newImages = targetImage - ? [targetImage].map((item) => ({ - id: item.id, - credentialIdentifier:item.credentialIdentifier, - src: item.logoURL, - alt: item.issuerFriendlyName, - data: parseJwt(item.credential)["vc"]['credentialSubject'], - type: parseJwt(item.credential)['vc']["type"]["2"], - expdate: parseJwt(item.credential)['vc']["expirationDate"], - json:JSON.stringify(parseJwt(item.credential)["vc"], null, 2) - - })) - : []; - - return newImages[0]; - } else { - const newImages = response.data.vc_list.map((item) => ({ - id: item.id, - credentialIdentifier:item.credentialIdentifier, - src: item.logoURL, - alt: item.issuerFriendlyName, - data: parseJwt(item.credential)["vc"]['credentialSubject'], - type: parseJwt(item.credential)['vc']["type"]["2"], - expdate: parseJwt(item.credential)['vc']["expirationDate"], - json:JSON.stringify(parseJwt(item.credential)["vc"], null, 2) - })); - - return newImages; - } - } catch (error) { - console.error('Failed to fetch data', error); - return null; - } -} diff --git a/src/components/Credentials/CredentialDeleteButton.js b/src/components/Credentials/CredentialDeleteButton.js index a02931543..ae1525c7c 100644 --- a/src/components/Credentials/CredentialDeleteButton.js +++ b/src/components/Credentials/CredentialDeleteButton.js @@ -5,11 +5,11 @@ import { useTranslation } from 'react-i18next'; const CredentialDeleteButton = ({ onDelete }) => { const { t } = useTranslation(); - const handleClick = () => { - onDelete(); - }; + const handleClick = () => { + onDelete(); + }; - return ( + return (
- ); + ); }; export default CredentialDeleteButton; diff --git a/src/components/Credentials/CredentialDeletePopup.js b/src/components/Credentials/CredentialDeletePopup.js deleted file mode 100644 index de4ff0221..000000000 --- a/src/components/Credentials/CredentialDeletePopup.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { MdDelete } from 'react-icons/md'; -import Spinner from '../../components/Spinner'; -import { useTranslation } from 'react-i18next'; - -const CredentialDeletePopup = ({ credential, onCancel, onConfirm, loading }) => { - const { t } = useTranslation(); - - return ( -
-
-
- {loading ? ( -
- -
- ) : ( - <> -

- - {t('common.delete')}: {credential.type.replace(/([A-Z])/g, ' $1')} -

-
-

- {t('pageCredentials.deletePopup.messagePart1')}{' '} - {credential.type.replace(/([A-Z])/g, ' $1')} {t('pageCredentials.deletePopup.messagePart2')} -
- {t('pageCredentials.deletePopup.messagePart3')}If you delete it,{' '} - {t('pageCredentials.deletePopup.messagePart4')} -

-
- - -
- - )} -
-
- ); -}; - -export default CredentialDeletePopup; diff --git a/src/components/Credentials/CredentialImage.js b/src/components/Credentials/CredentialImage.js new file mode 100644 index 000000000..2f66a69a2 --- /dev/null +++ b/src/components/Credentials/CredentialImage.js @@ -0,0 +1,27 @@ +import { useState, useEffect } from "react" +import { parseCredential } from "../../functions/parseCredential"; +import StatusRibbon from '../../components/Credentials/StatusRibbon'; + + +export const CredentialImage = ({ credential, className, onClick, showRibbon = true }) => { + const [parsedCredential, setParsedCredential] = useState(null); + + useEffect(() => { + parseCredential(credential).then((c) => { + setParsedCredential(c); + }); + }, []); + + return ( + <> + {parsedCredential && ( + <> + {"Credential"} + {showRibbon && + + } + + )} + + ) +} diff --git a/src/components/Credentials/CredentialInfo.js b/src/components/Credentials/CredentialInfo.js index 25b8ba3be..c74fbc346 100644 --- a/src/components/Credentials/CredentialInfo.js +++ b/src/components/Credentials/CredentialInfo.js @@ -1,68 +1,80 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { BiSolidCategoryAlt, BiSolidUserCircle } from 'react-icons/bi'; import { AiFillCalendar } from 'react-icons/ai'; import { RiPassExpiredFill } from 'react-icons/ri'; -import { MdTitle, MdGrade } from 'react-icons/md'; +import { MdTitle, MdGrade, MdOutlineNumbers } from 'react-icons/md'; import { GiLevelEndFlag } from 'react-icons/gi'; import { formatDate } from '../../functions/DateFormat'; +import { parseCredential } from '../../functions/parseCredential'; const getFieldIcon = (fieldName) => { - switch (fieldName) { - case 'type': - return ; - case 'expdate': - return ; - case 'dateOfBirth': - return ; - case 'familyName': - case 'firstName': - return ; - case 'diplomaTitle': - return ; - case 'eqfLevel': - return ; - case 'grade': - return ; - default: - return null; - } + switch (fieldName) { + case 'type': + return ; + case 'expdate': + return ; + case 'dateOfBirth': + return ; + case 'personalIdentifier': + return + case 'familyName': + case 'firstName': + return ; + case 'diplomaTitle': + return ; + case 'eqfLevel': + return ; + case 'grade': + return ; + default: + return null; + } }; const renderRow = (fieldName, fieldValue) => { - if (fieldValue) { - return ( - - - {getFieldIcon(fieldName)} - - {fieldValue} - - ); - } - return null; + if (fieldValue) { + return ( + + + {getFieldIcon(fieldName)} + + {fieldValue} + + ); + } + return null; }; -const CredentialInfo = ({ credential }) => { - return ( -
- - - {credential && ( - <> - {renderRow('type', credential.type)} - {renderRow('expdate', formatDate(credential.expdate))} - {renderRow('familyName', credential.data.familyName)} - {renderRow('firstName', credential.data.firstName)} - {renderRow('dateOfBirth', credential.data.dateOfBirth)} - {renderRow('diplomaTitle', credential.data.diplomaTitle)} - {renderRow('eqfLevel', credential.data.eqfLevel)} - {renderRow('grade', credential.data.grade)} - - )} - -
-
- ); +const CredentialInfo = ({ credential, mainClassName="pt-5 pr-2 w-full" }) => { + + const [parsedCredential, setParsedCredential] = useState(null); + + useEffect(() => { + parseCredential(credential).then((c) => { + setParsedCredential(c); + }); + }, []); + + return ( +
+ + + {parsedCredential && ( + <> + {renderRow('expdate', formatDate(parsedCredential.expirationDate))} + {renderRow('familyName', parsedCredential.credentialSubject.familyName)} + {renderRow('firstName', parsedCredential.credentialSubject.firstName)} + {renderRow('personalIdentifier', parsedCredential.credentialSubject.personalIdentifier)} + {renderRow('dateOfBirth', parsedCredential.credentialSubject.dateOfBirth)} + {renderRow('diplomaTitle', parsedCredential.credentialSubject.diplomaTitle)} + {renderRow('eqfLevel', parsedCredential.credentialSubject.eqfLevel)} + {renderRow('grade', parsedCredential.credentialSubject.grade)} + + )} + +
+
+ ); }; export default CredentialInfo; diff --git a/src/components/Credentials/CredentialJson.js b/src/components/Credentials/CredentialJson.js index e91e2fc8a..df33b83da 100644 --- a/src/components/Credentials/CredentialJson.js +++ b/src/components/Credentials/CredentialJson.js @@ -1,37 +1,46 @@ // CredentialJson.js -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AiOutlineDown, AiOutlineUp } from 'react-icons/ai'; +import { parseCredential } from '../../functions/parseCredential'; const CredentialJson = ({ credential }) => { const [showJsonCredentials, setShowJsonCredentials] = useState(false); - return ( + const [parsedCredential, setParsedCredential] = useState(null); + + useEffect(() => { + parseCredential(credential).then((c) => { + setParsedCredential(c); + }); + }, []); + + return (
- +
-
+
- {showJsonCredentials && credential ? ( + {showJsonCredentials && parsedCredential ? (
+ +
+
+ )} + +
+ {vcEntities.map(vcEntity => ( + <> +
+
+ handleClick(vcEntity.credentialIdentifier)} className={"w-full object-cover rounded-xl"} /> +
+
+ +
+ +
+
+
+ + ))} +
+ + + + ); +} + +export default SelectCredentials; diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 06e6db1ad..07943adfb 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -5,65 +5,60 @@ import { useLocalStorageKeystore } from '../services/LocalStorageKeystore'; import { fetchToken } from '../firebase'; import Layout from './Layout'; import Spinner from './Spinner'; // Import your spinner component +import { useSessionStorage } from '../components/useStorage'; const PrivateRoute = ({ children }) => { - const api = useApi(); - const [isPermissionGranted, setIsPermissionGranted] = useState(false); - const [isPermissionValue, setispermissionValue] = useState(''); - const [loading, setLoading] = useState(false); - const keystore = useLocalStorageKeystore(); - const isLoggedIn = api.isLoggedIn() && keystore.isOpen(); - - const location = useLocation(); - const navigate = useNavigate(); + const api = useApi(); + const [isPermissionGranted, setIsPermissionGranted] = useState(false); + const [loading, setLoading] = useState(false); + const keystore = useLocalStorageKeystore(); + const isLoggedIn = api.isLoggedIn() && keystore.isOpen(); + const [tokenSentInSession, setTokenSentInSession,] = api.useClearOnClearSession(useSessionStorage('tokenSentInSession', null)); - useEffect(() => { - const requestNotificationPermission = async () => { - console.log(Notification.permission); - try { - if (Notification.permission !== 'granted') { - sessionStorage.setItem('tokenSentInSession', 'false'); - const permissionResult = await Notification.requestPermission(); - if (permissionResult === 'granted') { - setIsPermissionGranted(true); - } - setispermissionValue(permissionResult); - } else { - setIsPermissionGranted(true); - sessionStorage.setItem('tokenSentInSession', 'false'); + const location = useLocation(); + const navigate = useNavigate(); - } - } catch (error) { - console.error('Error requesting notification permission:', error); - } - }; + useEffect(() => { + const requestNotificationPermission = async () => { + console.log(Notification.permission); + + try { + if (Notification.permission !== 'granted') { + setTokenSentInSession(false) + const permissionResult = await Notification.requestPermission(); + if (permissionResult === 'granted') { + setIsPermissionGranted(true); + } + } else { + setIsPermissionGranted(true); + } + } catch (error) { + console.error('Error requesting notification permission:', error); + } + }; - if (isLoggedIn) { - requestNotificationPermission(); - } - }, [isLoggedIn,location]); + if (isLoggedIn) { + requestNotificationPermission(); + } + }, [isLoggedIn, location]); useEffect(() => { const sendFcmTokenToBackend = async () => { - - console.log('isPermissionGranted:',isPermissionGranted); + console.log('isPermissionGranted:', isPermissionGranted); if (isPermissionGranted) { + console.log('tokenSentInSession:', tokenSentInSession); - // Check if the token has already been sent in the current session - const tokenSentInSession = sessionStorage.getItem('tokenSentInSession'); - console.log('tokenSentInSession:',tokenSentInSession); - - if (tokenSentInSession==='false') { + if (!tokenSentInSession) { setLoading(true); try { const fcmToken = await fetchToken(); - + if (fcmToken !== null) { await api.post('/user/session/fcm_token/add', { fcm_token: fcmToken }); - // Set a flag in sessionStorage to indicate that the token has been sent - sessionStorage.setItem('tokenSentInSession', 'true'); - console.log('send FCM Token:', fcmToken); - - console.log('FCM Token:', fcmToken); + setTokenSentInSession(true) + console.log('FCM Token success:', fcmToken); + } else { + console.log('FCM Token failed to get fcmtoken in private route', fcmToken); + } } catch (error) { console.error('Error sending FCM token to the backend:', error); } finally { @@ -72,35 +67,34 @@ const PrivateRoute = ({ children }) => { } } } - + sendFcmTokenToBackend(); }, [isPermissionGranted]); - useEffect(() => { - if (!isLoggedIn) { - const destination = location.pathname + location.search; - navigate('/login', { state: { from: destination } }); - } - }, [isLoggedIn, location, navigate]); + + useEffect(() => { + if (!isLoggedIn) { + const destination = location.pathname + location.search; + navigate('/login', { state: { from: destination } }); + } + }, [isLoggedIn, location, navigate]); - if (!isLoggedIn) { - return ; - } + if (!isLoggedIn) { + return ; + } return ( - <> - {loading && } - {!loading && ( - - {children} - - )} - - ); - - return children; + <> + {loading && } + {!loading && ( + + {children} + + )} + + ); }; -export default PrivateRoute; \ No newline at end of file +export default PrivateRoute; diff --git a/src/components/QRCodeScanner/CornerBox.js b/src/components/QRCodeScanner/CornerBox.js index eaa4dccca..fba38c88d 100644 --- a/src/components/QRCodeScanner/CornerBox.js +++ b/src/components/QRCodeScanner/CornerBox.js @@ -23,4 +23,4 @@ const CornerBox = ({ qrDetected, side, position, boxSize }) => { return
; }; -export default CornerBox; \ No newline at end of file +export default CornerBox; diff --git a/src/components/QRCodeScanner/QRCodeScanner.js b/src/components/QRCodeScanner/QRCodeScanner.js index 7ad06c2ff..6711efa3d 100644 --- a/src/components/QRCodeScanner/QRCodeScanner.js +++ b/src/components/QRCodeScanner/QRCodeScanner.js @@ -13,6 +13,7 @@ import { RiZoomInFill, RiZoomOutFill } from "react-icons/ri"; const QRScanner = ({ onClose }) => { const [devices, setDevices] = useState([]); + const [bestCameraResolutions, setBestCameraResolutions] = useState({ front: null, back: null }); const webcamRef = useRef(null); const [cameraReady, setCameraReady] = useState(false); const [loading, setLoading] = useState(false); @@ -20,6 +21,7 @@ const QRScanner = ({ onClose }) => { const [qrDetected, setQrDetected] = useState(false); const [boxSize, setBoxSize] = useState(null); const [zoomLevel, setZoomLevel] = useState(1); + const [hasCameraPermission, setHasCameraPermission] = useState(null); const { t } = useTranslation(); const handleZoomChange = (event) => { @@ -40,25 +42,77 @@ const QRScanner = ({ onClose }) => { }; useEffect(() => { - navigator.mediaDevices.enumerateDevices() - .then(mediaDevices => { - const videoDevices = mediaDevices.filter(({ kind }) => kind === "videoinput"); - setDevices(videoDevices); - - // Find and prioritize the back camera if it exists - const backCameraIndex = videoDevices.findIndex(device => device.label.toLowerCase().includes('back')); - if (backCameraIndex !== -1) { - setCurrentDeviceIndex(backCameraIndex); - } - - setCameraReady(true); + navigator.mediaDevices.getUserMedia({ video: true }) + .then(stream => { + setHasCameraPermission(true); + stream.getTracks().forEach(track => track.stop()); }) .catch(error => { - console.error("Error accessing camera:", error); - setCameraReady(false); + console.error("Camera access denied:", error); + setHasCameraPermission(false); }); }, []); + + useEffect(() => { + if (hasCameraPermission) { + navigator.mediaDevices.enumerateDevices() + .then(async mediaDevices => { + const videoDevices = mediaDevices.filter(({ kind }) => kind === "videoinput"); + + let bestFrontCamera = null; + let bestBackCamera = null; + + for (const device of videoDevices) { + const stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: device.deviceId } }); + const track = stream.getVideoTracks()[0]; + const capabilities = track.getCapabilities(); + const isBackCamera = device.label.toLowerCase().includes('back'); + const resolution = { + width: capabilities.width?.max || 0, + height: capabilities.height?.max || 0 + }; + + if (isBackCamera && (!bestBackCamera || bestBackCamera.resolution.width * bestBackCamera.resolution.height < resolution.width * resolution.height)) { + bestBackCamera = { device, resolution }; + } else if (!isBackCamera && (!bestFrontCamera || bestFrontCamera.resolution.width * bestFrontCamera.resolution.height < resolution.width * resolution.height)) { + bestFrontCamera = { device, resolution }; + } + + track.stop(); + } + + const filteredDevices = []; + if (bestFrontCamera) { + filteredDevices.push(bestFrontCamera.device); + } + if (bestBackCamera) { + filteredDevices.push(bestBackCamera.device); + } + + setBestCameraResolutions({ + front: bestFrontCamera ? bestFrontCamera.resolution : null, + back: bestBackCamera ? bestBackCamera.resolution : null, + }); + + setDevices(filteredDevices); + + const backCameraIndex = filteredDevices.findIndex(device => + device.label.toLowerCase().includes('back')); + + if (backCameraIndex !== -1) { + setCurrentDeviceIndex(backCameraIndex); + } else { + setCurrentDeviceIndex(0); + } + setCameraReady(true); + }) + .catch(error => { + console.error("Error enumerating devices:", error); + }); + } + }, [hasCameraPermission]); + const switchCamera = () => { if (devices.length > 1) { const newIndex = (currentDeviceIndex + 1) % devices.length; @@ -84,13 +138,15 @@ const QRScanner = ({ onClose }) => { setQrDetected(true); // Redirect to the URL found in the QR code const scannedUrl = code.data; - setLoading(true); + setTimeout(() => { + setLoading(true); + }, 1000); setTimeout(() => { const baseUrl = window.location.origin; const params = scannedUrl.split('?'); const cvUrl = `${baseUrl}/cb?${params[1]}&wwwallet_camera_was_used=true`; window.location.href = cvUrl; - }, 1500); + }, 1000); } }; @@ -109,7 +165,7 @@ const QRScanner = ({ onClose }) => { scanningMargin = (height - size) / 2; } document.documentElement.style.setProperty('--scanning-margin', scanningMargin + 'px'); - + document.documentElement.style.setProperty('--scanning', size + 'px'); setBoxSize(size); } }; @@ -143,13 +199,58 @@ const QRScanner = ({ onClose }) => { waitForVideoDimensions(); }; + + const currentCameraType = devices[currentDeviceIndex]?.label.toLowerCase().includes('back') ? 'back' : 'front'; + const maxResolution = bestCameraResolutions[currentCameraType]; + + let idealWidth, idealHeight; + if (maxResolution) { + if ((maxResolution.width < maxResolution.height)) { + idealHeight = maxResolution.width; + idealWidth = maxResolution.width; + + } else { + idealHeight = maxResolution.height; + idealWidth = maxResolution.height; + } + } else { + idealWidth = 1080; + idealHeight = 1080; + } + return ( -
-
- {loading && } -
- {cameraReady && ( -
+
+
+ + {hasCameraPermission === false ? ( +
+
+

+ + {t('qrCodeScanner.title')} +

+ + +
+
+

+ {t('qrCodeScanner.cameraPermissionAllow')} +

+
+ ) : (!cameraReady || loading) ? ( +
+ +
+ ) : ( +

@@ -175,8 +276,12 @@ const QRScanner = ({ onClose }) => { audio={false} ref={webcamRef} screenshotFormat="image/jpeg" - videoConstraints={{ deviceId: devices[currentDeviceIndex].deviceId }} - style={{ width: '100%', transform: `scale(${zoomLevel})`, transformOrigin: 'center' }} + videoConstraints={{ + deviceId: devices[currentDeviceIndex]?.deviceId, + height: { ideal: idealHeight }, + width: { ideal: idealWidth } + }} + style={{ height: 'auto', transform: `scale(${zoomLevel})`, transformOrigin: 'center', width: '100%', }} onUserMedia={onUserMedia} /> {boxSize && ( @@ -211,7 +316,7 @@ const QRScanner = ({ onClose }) => { {devices.length > 1 && ( -

- - {/* Logo */} + {/* Header and Nav */}
+
+ Logo handleNavigate('/')} /> +

handleNavigate('/')} + > + {t('common.walletName')} +

+ +
{
{/* Nav Menu */} -
+
{t("common.navItemCredentials")}
-
+
{t("common.navItemHistory")}
-
+
{t("common.navItemAddCredentials")}
-
+
{t("common.navItemSendCredentials")}
-
- - - {t("common.navItemSettings")} - -
+ + + {t("common.navItemSettings")} + +
  • { {t("sidebar.navItemLogout")}
  • - {/* Footer */} -
    -
    + + {/* Powered By */} +
    >, + showSelectCredentialsPopup: boolean, + setShowSelectCredentialsPopup: Dispatch>, setSelectionMap: Dispatch>, conformantCredentialsMap: any, - showPinPopup: boolean, - setShowPinPopup: Dispatch>, + showPinInputPopup: boolean, + setShowPinInputPopup: Dispatch>, + verifierDomainName: string, + showMessagePopup: boolean; + setMessagePopup: Dispatch>; + textMessagePopup: { title: string, description: string }; + typeMessagePopup: string; } { const api = useApi(); const isLoggedIn: boolean = api.isLoggedIn(); - const [isValidURL, setIsValidURL] = useState(null); - const [showPopup, setShowPopup] = useState(false); - const [showPinPopup, setShowPinPopup] = useState(false); + const [showSelectCredentialsPopup, setShowSelectCredentialsPopup] = useState(false); + const [showPinInputPopup, setShowPinInputPopup] = useState(false); const [selectionMap, setSelectionMap] = useState(null); const [conformantCredentialsMap, setConformantCredentialsMap] = useState(null); + const [verifierDomainName, setVerifierDomainName] = useState(""); + const [showMessagePopup, setMessagePopup] = useState(false); + const [textMessagePopup, setTextMessagePopup] = useState<{ title: string, description: string }>({ title: "", description: "" }); + const [typeMessagePopup, setTypeMessagePopup] = useState(""); const keystore = useLocalStorageKeystore(); + const { t } = useTranslation(); useEffect(() => { @@ -28,11 +45,18 @@ function useCheckURL(urlToCheck: string): { const wwwallet_camera_was_used = new URL(url).searchParams.get('wwwallet_camera_was_used'); const res = await api.post('/communication/handle', { url, camera_was_used: (wwwallet_camera_was_used != null && wwwallet_camera_was_used === 'true') }); - const { redirect_to, conformantCredentialsMap, verifierDomainName, preauth, ask_for_pin } = res.data; - + const { redirect_to, conformantCredentialsMap, verifierDomainName, preauth, ask_for_pin, error } = res.data; + if (error && error == HandleOutboundRequestError.INSUFFICIENT_CREDENTIALS) { + console.error(`${HandleOutboundRequestError.INSUFFICIENT_CREDENTIALS}`); + setTextMessagePopup({ title: `${t('messagePopup.insufficientCredentials.title')}`, description: `${t('messagePopup.insufficientCredentials.description')}` }); + setTypeMessagePopup('error'); + setMessagePopup(true); + return false; + } + if (preauth && preauth == true) { if (ask_for_pin) { - setShowPinPopup(true); + setShowPinInputPopup(true); return true; } else { @@ -46,9 +70,10 @@ function useCheckURL(urlToCheck: string): { return true; } else if (conformantCredentialsMap) { console.log('need action'); + setVerifierDomainName(verifierDomainName); setConformantCredentialsMap(conformantCredentialsMap); - setShowPopup(true); - console.log("called setShowPopup") + setShowSelectCredentialsPopup(true); + console.log("called setShowSelectCredentialsPopup") return true; } else { @@ -63,13 +88,10 @@ function useCheckURL(urlToCheck: string): { if (urlToCheck && isLoggedIn && window.location.pathname === "/cb") { (async () => { - if (await communicationHandler(urlToCheck)) { - setIsValidURL(true); - } else { - setIsValidURL(false); - } + await communicationHandler(urlToCheck); })(); } + }, [api, keystore, urlToCheck, isLoggedIn]); useEffect(() => { @@ -80,16 +102,31 @@ function useCheckURL(urlToCheck: string): { { verifiable_credentials_map: selectionMap }, ).then(success => { console.log(success); - const { redirect_to } = success.data; - if (redirect_to) + const { redirect_to, error } = success.data; + + if (error && error == SendResponseError.SEND_RESPONSE_ERROR) { + setTextMessagePopup({ title: `${t('messagePopup.sendResponseError.title')}`, description: `${t('messagePopup.sendResponseError.description')}` }); + setTypeMessagePopup('error'); + setMessagePopup(true); + return; + } + if (redirect_to) { window.location.href = redirect_to; // Navigate to the redirect URL + } + else { + setTextMessagePopup({ title: `${t('messagePopup.sendResponseSuccess.title')}`, description: `${t('messagePopup.sendResponseSuccess.description')}` }); + setTypeMessagePopup('success'); + setMessagePopup(true); + return; + } }).catch(err => { - alert("Presentation failed") + console.error("Error"); + console.error(err); }); } }, [api, keystore, selectionMap]); - return { isValidURL, showPopup, setShowPopup, setSelectionMap, conformantCredentialsMap, showPinPopup, setShowPinPopup }; + return { showSelectCredentialsPopup, setShowSelectCredentialsPopup, setSelectionMap, conformantCredentialsMap, showPinInputPopup, setShowPinInputPopup, verifierDomainName, showMessagePopup, setMessagePopup, textMessagePopup, typeMessagePopup }; } export default useCheckURL; diff --git a/src/components/useStorage.ts b/src/components/useStorage.ts index b811acb49..a1e229289 100644 --- a/src/components/useStorage.ts +++ b/src/components/useStorage.ts @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'reac import { jsonParseTaggedBinary, jsonStringifyTaggedBinary } from '../util'; type ClearHandle = () => void; -type UseStorageHandle = [T, Dispatch>, ClearHandle]; +export type UseStorageHandle = [T, Dispatch>, ClearHandle]; type UseStorageEvent = { storageArea: Storage }; type ClearEvent = UseStorageEvent; type SetValueEvent = UseStorageEvent & { name: string, value: T }; diff --git a/src/firebase.js b/src/firebase.js index 613668bdb..825df0157 100644 --- a/src/firebase.js +++ b/src/firebase.js @@ -2,13 +2,13 @@ import { initializeApp } from "firebase/app"; import { getMessaging, getToken, onMessage, isSupported } from 'firebase/messaging'; const firebaseConfig = { - apiKey: process.env.REACT_APP_FIREBASE_API_KEY, - authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, - projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, - storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.REACT_APP_FIREBASE_APP_ID, - measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID + apiKey: process.env.REACT_APP_FIREBASE_API_KEY, + authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, + projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, + storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.REACT_APP_FIREBASE_APP_ID, + measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID }; let firebase = null; @@ -16,125 +16,124 @@ let messaging = null; let supported = false; const initializeFirebase = async () => { - supported = await isSupported(); - if (supported) { - firebase = initializeApp(firebaseConfig); - messaging = getMessaging(); - } - console.log("Supported", supported); + supported = await isSupported(); + if (supported) { + firebase = initializeApp(firebaseConfig); + messaging = getMessaging(); + } + console.log("Supported", supported); }; const requestForToken = async () => { if (!supported) { - return null; + return null; } if (messaging) { - try { - const currentToken = await getToken(messaging, { vapidKey: process.env.REACT_APP_FIREBASE_VAPIDKEY }); - if (currentToken) { - console.log('Current token for client:', currentToken); - return currentToken; - } else { - console.log('No registration token available. Request permission to generate one.'); - return null; - } - } catch (err) { - console.log('ERROR:',err.message,err.code); - if (err.code === 'messaging/permission-blocked') { - console.error('Notification permission was blocked or click close.'); - return null; - }else if (err.message === "Failed to execute 'subscribe' on 'PushManager': Subscription failed - no active Service Worker") { - console.error('Failed beacuse there is no token created yet, so we are going to re-register'); - - } else { - console.error('An error occurred while retrieving token:',err); - return null; - } + try { + const currentToken = await getToken(messaging, { vapidKey: process.env.REACT_APP_FIREBASE_VAPIDKEY }); + if (currentToken) { + console.log('Current token for client:', currentToken); + return currentToken; + } else { + console.log('No registration token available. Request permission to generate one.'); + return null; + } + } catch (err) { + console.log('ERROR:', err.message, err.code); + if (err.code === 'messaging/permission-blocked') { + console.error('Notification permission was blocked or click close.'); + return null; + } else if (err.message === "Failed to execute 'subscribe' on 'PushManager': Subscription failed - no active Service Worker") { + console.error('Failed beacuse there is no token created yet, so we are going to re-register'); + + } else { + console.error('An error occurred while retrieving token:', err); + return null; } + } } else { - console.log('Messaging is not initialized.'); - return null; + console.log('Messaging is not initialized.'); + return null; } }; const reRegisterServiceWorkerAndGetToken = async () => { - if ('serviceWorker' in navigator) { - try { - // Re-register the service worker - const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js'); - if (registration) { - console.log('Service Worker re-registered', registration); - const token = await requestForToken(); - if (token) { - console.log('New FCM token obtained:', token); - return token; - } else { - console.log('Failed to retrieve a new token.'); - return null; - } - } else { - console.log('Service Worker re-registration failed'); - } - } catch (error) { - console.error('Service Worker re-registration failed with', error); - } - } else { - console.log('Service Workers are not supported in this browser.'); - } + if ('serviceWorker' in navigator) { + try { + // Re-register the service worker + const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js'); + if (registration) { + console.log('Service Worker re-registered', registration); + const token = await requestForToken(); + if (token) { + console.log('New FCM token obtained:', token); + return token; + } else { + console.log('Failed to retrieve a new token.'); + return null; + } + } else { + console.log('Service Worker re-registration failed'); + } + } catch (error) { + console.error('Service Worker re-registration failed with', error); + } + } else { + console.log('Service Workers are not supported in this browser.'); + } }; export const fetchToken = async () => { if (messaging) { - const token = await requestForToken(); - console.log('token:',token); - if (token || token ===null) { - return token; + const token = await requestForToken(); + console.log('token:', token); + if (token) { + return token; + } else { + console.log('Failed to retrieve token. Trying to re-register service worker.'); + const newToken = await reRegisterServiceWorkerAndGetToken(); // Re-register service worker and fetch token + if (newToken) { + return newToken; } else { - console.log('Failed to retrieve token. Trying to re-register service worker.'); - const newToken =await reRegisterServiceWorkerAndGetToken(); // Re-register service worker and fetch token - if (newToken) { - return newToken; - } else { - console.log('Failed to retrieve a new token after re-registration.'); - } + console.log('Failed to retrieve a new token after re-registration.'); } + } } else { - console.log('Messaging is not initialized.'); - + console.log('Messaging is not initialized.'); } return null; // Return null in case of failure }; export const onMessageListener = () => - new Promise((resolve) => { - if (supported) { - onMessage(messaging, (payload) => { - resolve(payload); - }); - } -}); + new Promise((resolve) => { + if (supported) { + onMessage(messaging, (payload) => { + resolve(payload); + }); + } + }); const initializeMessaging = async () => { - // Check for service worker - if ('serviceWorker' in navigator) { - try { - const registration = await navigator.serviceWorker.getRegistration('/firebase-messaging-sw.js'); - if (registration) { - console.log('Service Worker registered', registration); - return getMessaging(); - } else { - console.log('Service Worker registration failed'); - } - } catch (error) { - console.error('Service Worker registration failed with', error); - } - } else { - console.log('Service Workers are not supported in this browser.'); - } + // Check for service worker + if ('serviceWorker' in navigator) { + try { + const registration = await navigator.serviceWorker.getRegistration('/firebase-messaging-sw.js'); + if (registration) { + console.log('Service Worker registered', registration); + return getMessaging(); + } else { + console.log('Service Worker registration failed'); + } + } catch (error) { + console.error('Service Worker registration failed with', error); + } + } else { + console.log('Service Workers are not supported in this browser.'); + } }; const initializeFirebaseAndMessaging = async () => { @@ -142,10 +141,10 @@ const initializeFirebaseAndMessaging = async () => { const messaging = await initializeMessaging(); // Now you can use the messaging object if it's available if (messaging) { - // Do something with the messaging object + // Do something with the messaging object } else { - console.log('Messaging is not initialized.'); + console.log('Messaging is not initialized.'); } }; -initializeFirebaseAndMessaging(); \ No newline at end of file +initializeFirebaseAndMessaging(); diff --git a/src/functions/DateFormat.js b/src/functions/DateFormat.js index d49e25dab..95f606e51 100644 --- a/src/functions/DateFormat.js +++ b/src/functions/DateFormat.js @@ -1,13 +1,11 @@ export function formatDate(dateString) { - const options = { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }; - return new Date(dateString).toLocaleString(undefined, options); + const options = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }; + return new Date(dateString).toLocaleString(undefined, options); }; - - diff --git a/src/functions/ParseJwt.js b/src/functions/ParseJwt.js index 9e4e3e05a..3e875e82a 100644 --- a/src/functions/ParseJwt.js +++ b/src/functions/ParseJwt.js @@ -1,18 +1,18 @@ // jwtParser.js function parseJwt(token) { - var base64Url = token.split('.')[1]; - var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - var jsonPayload = decodeURIComponent( - window.atob(base64) - .split('') - .map(function(c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }) - .join('') - ); + var base64Url = token.split('.')[1]; + var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + var jsonPayload = decodeURIComponent( + window.atob(base64) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join('') + ); - return JSON.parse(jsonPayload); + return JSON.parse(jsonPayload); } -export default parseJwt; \ No newline at end of file +export default parseJwt; diff --git a/src/functions/extractCredentialFriendlyName.ts b/src/functions/extractCredentialFriendlyName.ts new file mode 100644 index 000000000..f09835fd6 --- /dev/null +++ b/src/functions/extractCredentialFriendlyName.ts @@ -0,0 +1,7 @@ +import { parseCredential } from "./parseCredential"; + + +export const extractCredentialFriendlyName = async (credential: string | object): Promise => { + const parsedCredential = await parseCredential(credential) as any; + return parsedCredential.name ?? parsedCredential.id; +} diff --git a/src/functions/extractCredentialImage.ts b/src/functions/extractCredentialImage.ts new file mode 100644 index 000000000..9d9a3af42 --- /dev/null +++ b/src/functions/extractCredentialImage.ts @@ -0,0 +1,6 @@ +import { parseCredential } from "./parseCredential" + +export const extractCredentialImageURL = async (credential: string | object): Promise => { + const parsedCredential = await parseCredential(credential) as any; + return parsedCredential?.credentialBranding?.image?.url; +} diff --git a/src/functions/parseCredential.ts b/src/functions/parseCredential.ts new file mode 100644 index 000000000..ff16f466d --- /dev/null +++ b/src/functions/parseCredential.ts @@ -0,0 +1,37 @@ +import parseJwt from './ParseJwt'; +import { + HasherAlgorithm, + HasherAndAlgorithm, + SdJwt, +} from '@sd-jwt/core' + +export enum CredentialFormat { + VC_SD_JWT = "vc+sd-jwt", + JWT_VC_JSON = "jwt_vc_json" +} + +const encoder = new TextEncoder(); + +// Encoding the string into a Uint8Array +const hasherAndAlgorithm: HasherAndAlgorithm = { + hasher: (input: string) => { + return crypto.subtle.digest('SHA-256', encoder.encode(input)).then((v) => new Uint8Array(v)); + }, + algorithm: HasherAlgorithm.Sha256 +} + +export const parseCredential = async (credential: string | object): Promise => { + if (typeof credential == 'string') { // is JWT + if (credential.includes('~')) { // is SD-JWT + return SdJwt.fromCompact, any>(credential) + .withHasher(hasherAndAlgorithm) + .getPrettyClaims() + .then((payload) => payload.vc); + } + else { // is plain JWT + return parseJwt(credential) + .then((payload) => payload.vc); + } + } + throw new Error("Type of credential is not supported") +} diff --git a/src/i18n.js b/src/i18n.js index eb27dd7f8..72394fb0b 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -5,15 +5,15 @@ import { initReactI18next } from 'react-i18next'; import enTranslation from './locales/en.json'; i18n - .use(initReactI18next) - .init({ - resources: { - en: { translation: enTranslation }, - }, - lng: 'en', - interpolation: { - escapeValue: false, - }, - }); + .use(initReactI18next) + .init({ + resources: { + en: { translation: enTranslation }, + }, + lng: 'en', + interpolation: { + escapeValue: false, + }, + }); export default i18n; diff --git a/src/index.css b/src/index.css index 44f4b0ef2..aca0cc07e 100644 --- a/src/index.css +++ b/src/index.css @@ -4,69 +4,93 @@ /* Font family and rendered font appear smoother */ * { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* Slider margin in mobile screen */ -.slick-slide > div { - margin: 0 5px !important; +.slick-slide>div { + margin: 0 5px !important; } -/* Custom scrollbar in login */ +/* Custom scrollbar */ .custom-scrollbar { - scrollbar-width: thin; - scrollbar-color: #ccc #f0f0f0; - padding-right: 5px; + padding-right: 10px; } .custom-scrollbar::-webkit-scrollbar { - width: 5px; + width: 10px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background-color: #f0f0f0; + border-radius: 5px; } .custom-scrollbar::-webkit-scrollbar-thumb { - background-color: #ccc; - border-radius: 5px; + background-color: #ccc; + border-radius: 5px; + border: 2px solid #f0f0f0; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { - background-color: #888; + background-color: #888; + width: 15px; + border: 2px solid #888; +} + +.custom-scrollbar::-webkit-scrollbar:hover { + width: 15px; } /* Nav item animation on Hover */ .nav-item-animate-hover { - background: linear-gradient(90deg, transparent 50%, white 50%); - background-size: 200% 100%; - background-position: left; - transition: background 0.2s, color 0.2s; - color: white; + background: linear-gradient(90deg, transparent 50%, white 50%); + background-size: 200% 100%; + background-position: left; + transition: background 0.2s, color 0.2s; + color: white; } .nav-item-animate-hover:hover { - background-position: right; - color: #003476; + background-position: right; + color: #003476; } /* Fade in animation for content */ -.fade-in-content { - opacity: 0; - transition: opacity 0.3s ease-in-out; +.content-fade-in-enter { + opacity: 0; +} + +.content-fade-in-enter-active { + opacity: 1; + transition: opacity 400ms ease-in; } -.fade-in-content.visible { - opacity: 1; +.content-fade-in-exit { + opacity: 1; +} + +.content-fade-in-exit-active { + opacity: 0; + transition: opacity 400ms ease-in; } @keyframes scan-vertical { - 0%, 100% { top: calc(0% + var(--scanning-margin)); } - 50% { top: calc(100% - var(--scanning-margin)); } + + 0%, + 100% { + transform: translateY(calc(-50% + var(--scanning)/2 - var(--scanning-margin))); + } + + 50% { + transform: translateY(calc(-50% - var(--scanning)/2 + var(--scanning-margin))); + } } .text-overflow-ellipsis { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} \ No newline at end of file + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/src/index.js b/src/index.js index 3ce4401cf..66f0b1819 100644 --- a/src/index.js +++ b/src/index.js @@ -8,4 +8,3 @@ ConsoleBehavior(); const root = createRoot(document.getElementById('root')); root.render(); - diff --git a/src/locales/en.json b/src/locales/en.json index b5bc9239a..fc7e7d6a9 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -4,9 +4,12 @@ "continue": "Continue", "delete": "Delete", "navItemAddCredentials": "Add Credentials", + "navItemAddCredentialsSimple": "Add", "navItemCredentials": "Credentials", "navItemHistory": "History", + "navItemProfile": "Profile", "navItemSendCredentials": "Send Credentials", + "navItemSendCredentialsSimple": "Send", "navItemSettings": "Settings", "save": "Save", "submit": "submit", @@ -26,6 +29,10 @@ "windows": "Chrome, Edge, Opera or Brave on Windows" } }, + "layout": { + "messageAllowPermission": "To receive real-time updates of Credentials, allow notifications permission and reload this page.", + "messageResetPermission": "Something not working properly with the notifications, please reload this page." + }, "loginSignup": { "alreadyHaveAccountQuestion": "Already have an account? ", "capitalLetter": "At least one capital letter", @@ -74,6 +81,21 @@ "weakPasswordError": "Weak password", "welcomeMessage": "Welcome to wwWallet" }, + "messagePopup": { + "close": "Close", + "insufficientCredentials": { + "title": "Insufficient Credentials", + "description": "One or more of the credentials you want to present do not exist for selection." + }, + "sendResponseError": { + "title": "Send Response Error", + "description": "An error occured during the presentation of the credentials." + }, + "sendResponseSuccess": { + "title": "Presentation was successful", + "description": "Credentials were successfully sent." + } + }, "notFound": { "homeButton": "Back to Home", "message": "Sorry, the page you're looking for cannot be accessed", @@ -175,17 +197,24 @@ "title": "Enter the Pin" }, "qrCodeScanner": { + "cameraPermissionAllow": "Please allow camera permission to use the QR scanner.", "description": "Target the QR Code, and you will redirect to proceed with the process", "title": "Scan the QR Code" }, "selectCredentialPopup": { - "description": "Please select one of the above credentials to proceed with the presentation", + "description": "Please select one of the credentials below to proceed with the presentation", + "requestedFieldsHide": "Hide Requested Fields", + "requestedFieldsinfo": "The following fields were requested from the verifier", + "requestedFieldsShow": "Show Requested Fields", "title": "Select an Option:" }, "sidebar": { "navItemLogout": "Logout", "poweredBy": "Powered by wwWallet" }, + "statusRibbon": { + "expired": "Expired" + }, "tourGuide": { "tourStep1": "Here, you can view all your stored credentials. Click the 'Add New Credentials' card to get a new VC from an available Issuer", "tourStep2": "This is another way to get a New Credential", @@ -206,4 +235,4 @@ "dismissButton": "Dismiss", "startTourButton": "Start Tour" } -} \ No newline at end of file +} diff --git a/src/pages/AddCredentials/AddCredentials.js b/src/pages/AddCredentials/AddCredentials.js index ebad504b6..13ddc268c 100644 --- a/src/pages/AddCredentials/AddCredentials.js +++ b/src/pages/AddCredentials/AddCredentials.js @@ -1,12 +1,10 @@ import React, { useState, useEffect } from 'react'; -import { FaShare } from 'react-icons/fa'; import {BsQrCodeScan} from 'react-icons/bs' - import { useTranslation } from 'react-i18next'; +import QRCodeScanner from '../../components/QRCodeScanner/QRCodeScanner'; +import RedirectPopup from '../../components/Popups/RedirectPopup'; import { useApi } from '../../api'; -import Spinner from '../../components/Spinner'; -import QRCodeScanner from '../../components/QRCodeScanner/QRCodeScanner'; // Replace with the actual import path function highlightBestSequence(issuer, search) { if (typeof issuer !== 'string' || typeof search !== 'string') { @@ -24,24 +22,24 @@ const Issuers = () => { const [searchQuery, setSearchQuery] = useState(''); const [issuers, setIssuers] = useState([]); const [filteredIssuers, setFilteredIssuers] = useState([]); - const [showPopup, setShowPopup] = useState(false); + const [showRedirectPopup, setShowRedirectPopup] = useState(false); const [selectedIssuer, setSelectedIssuer] = useState(null); const [loading, setLoading] = useState(false); - const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < 768); + const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < 768); const { t } = useTranslation(); useEffect(() => { - const handleResize = () => { - setIsSmallScreen(window.innerWidth < 768); - }; + const handleResize = () => { + setIsSmallScreen(window.innerWidth < 768); + }; - window.addEventListener('resize', handleResize); + window.addEventListener('resize', handleResize); - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); useEffect(() => { const fetchIssuers = async () => { @@ -77,30 +75,30 @@ const Issuers = () => { const clickedIssuer = issuers.find((issuer) => issuer.did === did); if (clickedIssuer) { setSelectedIssuer(clickedIssuer); - setShowPopup(true); + setShowRedirectPopup(true); } }; - const handleCancel = () => { - setShowPopup(false); - setSelectedIssuer(null); - }; + const handleCancel = () => { + setShowRedirectPopup(false); + setSelectedIssuer(null); + }; const handleContinue = () => { setLoading(true); - + console.log('Continue with:', selectedIssuer); - + if (selectedIssuer && selectedIssuer.did) { const payload = { legal_person_did: selectedIssuer.did, }; - + api.post('/communication/handle', payload) .then((response) => { const { redirect_to } = response.data; console.log(redirect_to); - + // Redirect to the URL received from the backend window.location.href = redirect_to; }) @@ -109,9 +107,9 @@ const Issuers = () => { console.error('Error sending request to backend:', error); }); } - + setLoading(false); - setShowPopup(false); + setShowRedirectPopup(false); }; // QR Code part @@ -129,19 +127,19 @@ const Issuers = () => { <>
    -

    {t('common.navItemAddCredentials')}

    - { isSmallScreen && ( +

    {t('common.navItemAddCredentials')}

    + {isSmallScreen && ( + className="px-2 py-2 mb-2 text-white bg-custom-blue hover:bg-custom-blue-hover focus:ring-4 focus:outline-none focus:ring-custom-blue font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-custom-blue-hover dark:hover:bg-custom-blue-hover dark:focus:ring-custom-blue-hover" + onClick={openQRScanner} // Open the QR code scanner modal + > +
    + +
    + )} - -
    + +

    {t('pageAddCredentials.description')}

    @@ -169,54 +167,31 @@ const Issuers = () => { onClick={() => handleIssuerClick(issuer.did)} >
    - + ))} )}
    - {showPopup && ( -
    -
    -
    - {loading ? ( -
    - -
    - ) : ( - <> -

    - - {t('pageAddCredentials.popup.title')} {selectedIssuer?.friendlyName} -

    -
    -

    - {t('pageAddCredentials.popup.messagePart1')} {selectedIssuer?.friendlyName}{t('pageAddCredentials.popup.messagePart2')} -

    -
    - - -
    - - )} -
    -
    + {showRedirectPopup && ( + )} + {/* QR Code Scanner Modal */} {isQRScannerOpen && ( -
    - -
    )} ); }; -export default Issuers; \ No newline at end of file +export default Issuers; diff --git a/src/pages/History/History.js b/src/pages/History/History.js index 13fd9ee61..b1cf7fabd 100644 --- a/src/pages/History/History.js +++ b/src/pages/History/History.js @@ -3,27 +3,28 @@ import { BiLeftArrow, BiRightArrow } from 'react-icons/bi'; import { useTranslation } from 'react-i18next'; import Slider from 'react-slick'; -import "slick-carousel/slick/slick.css"; +import "slick-carousel/slick/slick.css"; import "slick-carousel/slick/slick-theme.css"; import { useApi } from '../../api'; -import { fetchCredentialData } from '../../components/Credentials/ApiFetchCredential'; import CredentialInfo from '../../components/Credentials/CredentialInfo'; -import {formatDate} from '../../functions/DateFormat'; +import { formatDate } from '../../functions/DateFormat'; +import { base64url } from 'jose'; +import { CredentialImage } from '../../components/Credentials/CredentialImage'; const History = () => { - const api = useApi(); - const [history, setHistory] = useState([]); - const [matchingCredentials, setMatchingCredentials] = useState([]); + const api = useApi(); + const [history, setHistory] = useState([]); + const [matchingCredentials, setMatchingCredentials] = useState([]); const [isImageModalOpen, setImageModalOpen] = useState(false); const [currentSlide, setCurrentSlide] = useState(1); const { t } = useTranslation(); - const sliderRef = useRef(); + const sliderRef = useRef(); const settings = { dots: false, @@ -38,77 +39,78 @@ const History = () => { style: { margin: '0 10px' }, }; - const handleHistoryItemClick = async (ivci) => { + const handleHistoryItemClick = async (item) => { + // Export all credentials from the presentation + const vpTokenPayload = JSON.parse(new TextDecoder().decode( + base64url.decode(item.presentation.split('.')[1]) + )); - // Fetch all credentials - const temp_cred = await fetchCredentialData(api); + const verifiableCredentials = vpTokenPayload.vp.verifiableCredential; // in raw format - // Filter credentials to keep only those with matching IDs in ivci - const matchingCreds = temp_cred.filter((cred) => ivci.includes(cred.credentialIdentifier)); - - // Set matching credentials and show the popup - setMatchingCredentials(matchingCreds); - setImageModalOpen(true); - }; + // Set matching credentials and show the popup + setMatchingCredentials(verifiableCredentials); + setImageModalOpen(true); + }; - useEffect(() => { - const fetchedPresentations = async () => { - try { - const fetchedPresentations = await api.getAllPresentations(); + useEffect(() => { + const fetchedPresentations = async () => { + try { + const fetchedPresentations = await api.getAllPresentations(); console.log(fetchedPresentations.vp_list); - // Extract and map the vp_list from fetchedPresentations. - const vpListFromApi = fetchedPresentations.vp_list.map((item) => ({ - id: item.id, - ivci: item.includedVerifiableCredentialIdentifiers, - audience: item.audience, - issuanceDate: item.issuanceDate, - })); - - setHistory(vpListFromApi); - } catch (error) { - console.error('Error fetching verifiers:', error); - } - }; - - fetchedPresentations(); - }, [api]); - - return ( - <> -
    -

    {t('common.navItemHistory')}

    -
    -

    + // Extract and map the vp_list from fetchedPresentations. + const vpListFromApi = fetchedPresentations.vp_list.map((item) => ({ + id: item.id, + presentation: item.presentation, + // ivci: item.includedVerifiableCredentialIdentifiers, + audience: item.audience, + issuanceDate: item.issuanceDate, + })); + + setHistory(vpListFromApi); + } catch (error) { + console.error('Error fetching verifiers:', error); + } + }; + + fetchedPresentations(); + }, [api]); + + return ( + <> +

    +

    {t('common.navItemHistory')}

    +
    +

    {t('pageHistory.description')} -

    +

    {history.length === 0 ? ( -

    +

    {t('pageHistory.noFound')}

    - ) : ( + ) : (
    - {history.map((item) => ( -
    handleHistoryItemClick(item.ivci)} - > -
    {item.audience}
    -
    {formatDate(new Date(item.issuanceDate * 1000).toISOString())}
    -
    - ))} -
    - )} -
    - - {isImageModalOpen && ( + {history.map((item) => ( +
    handleHistoryItemClick(item)} + > +
    {item.audience}
    +
    {formatDate(new Date(item.issuanceDate * 1000).toISOString())}
    +
    + ))} +
    + )} + + + {isImageModalOpen && (
    -
    setImageModalOpen(false)}>
    +
    setImageModalOpen(false)}>
    - + {/* Popup content */}

    @@ -116,41 +118,41 @@ const History = () => {

    -
    +

    {/* Display presented credentials */}
    - {matchingCredentials.map((credential) => ( - -
    - {credential.alt} -
    -
    - {currentSlide} of {matchingCredentials.length} - - -
    -
    - -
    -
    - ))} + {matchingCredentials.map((credential) => ( + +
    + +
    +
    + {currentSlide} of {matchingCredentials.length} + + +
    +
    + +
    +
    + ))}
    - - )} - - ); + + )} + + ); }; export default History; diff --git a/src/pages/Home/CredentialDetail.js b/src/pages/Home/CredentialDetail.js index 185694911..e5f4d1ca2 100644 --- a/src/pages/Home/CredentialDetail.js +++ b/src/pages/Home/CredentialDetail.js @@ -3,41 +3,57 @@ import React, { useState, useEffect } from 'react'; import { Link, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; - -import { AiOutlineCloseCircle } from 'react-icons/ai'; +import { extractCredentialFriendlyName } from "../../functions/extractCredentialFriendlyName"; import { BiRightArrowAlt } from 'react-icons/bi'; import { useApi } from '../../api'; import CredentialInfo from '../../components/Credentials/CredentialInfo'; import CredentialJson from '../../components/Credentials/CredentialJson'; -import { fetchCredentialData } from '../../components/Credentials/ApiFetchCredential'; import CredentialDeleteButton from '../../components/Credentials/CredentialDeleteButton'; -import CredentialDeletePopup from '../../components/Credentials/CredentialDeletePopup'; +import FullscreenPopup from '../../components/Popups/FullscreenImg'; +import DeletePopup from '../../components/Popups/DeletePopup'; +import { CredentialImage } from '../../components/Credentials/CredentialImage'; const CredentialDetail = () => { const api = useApi(); const { id } = useParams(); - const [credential, setCredentials] = useState(null); - const [isImageModalOpen, setImageModalOpen] = useState(false); + const [vcEntity, setVcEntity] = useState(null); + const [showFullscreenImgPopup, setShowFullscreenImgPopup] = useState(false); const [showDeletePopup, setShowDeletePopup] = useState(false); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); const { t } = useTranslation(); + const [credentialFiendlyName, setCredentialFriendlyName] = useState(null); + + + useEffect(() => { const getData = async () => { - - const newCredential = await fetchCredentialData(api, id); - console.log(newCredential.json); - setCredentials(newCredential); + const response = await api.get('/storage/vc'); + const vcEntity = response.data.vc_list + .filter((vcEntity) => vcEntity.credentialIdentifier == id)[0]; + if (!vcEntity) { + throw new Error("Credential not found"); + } + setVcEntity(vcEntity); }; + getData(); }, [api, id]); + useEffect(() => { + if (vcEntity && vcEntity.credential) { + extractCredentialFriendlyName(vcEntity.credential).then((name) => { + setCredentialFriendlyName(name); + }); + } + }, [vcEntity]); + const handleSureDelete = async () => { setLoading(true); try { - await api.del(`/storage/vc/${credential.credentialIdentifier}`); + await api.del(`/storage/vc/${vcEntity.credentialIdentifier}`); } catch (error) { console.error('Failed to delete data', error); } @@ -52,12 +68,12 @@ const CredentialDetail = () => {
    -

    {t('common.navItemCredentials')}

    - +

    {t('common.navItemCredentials')}

    +
    - {credential && ( -

    {credential.type.replace(/([A-Z])/g, ' $1')}

    + {vcEntity && ( +

    {credentialFiendlyName}

    )}

    @@ -66,56 +82,58 @@ const CredentialDetail = () => {
    {/* Block 1: credential */}
    - {credential && credential.src ? ( - // Open the modal when the credential is clicked -
    setImageModalOpen(true)}> - {credential.alt} - -
    + {vcEntity ? ( + // Open the modal when the credential is clicked +
    setShowFullscreenImgPopup(true)}> + +
    ) : ( <> )}
    {/* Block 2: Information List */} - {credential && } {/* Use the CredentialInfo component */} + {vcEntity && } {/* Use the CredentialInfo component */}
    - { setShowDeletePopup(true); }} /> + { setShowDeletePopup(true); }} />
    - + {vcEntity && }
    {/* Modal for Fullscreen credential */} - {isImageModalOpen && ( -
    -
    - {credential.src} -
    - -
    + {showFullscreenImgPopup && vcEntity && ( + setShowFullscreenImgPopup(false)} + content={ + + } + /> )} {/* Delete Credential Modal */} - {showDeletePopup && credential && ( - setShowDeletePopup(false)} - onConfirm={handleSureDelete} - loading={loading} - /> - )} + {showDeletePopup && vcEntity && ( + + setShowDeletePopup(false)} + message={ + + {t('pageCredentials.deletePopup.messagePart1')}{' '} {credentialFiendlyName} {t('pageCredentials.deletePopup.messagePart2')} +
    {t('pageCredentials.deletePopup.messagePart3')}{' '} {t('pageCredentials.deletePopup.messagePart4')} +
    + } + loading={loading} + /> + )} ); }; -export default CredentialDetail; \ No newline at end of file +export default CredentialDetail; diff --git a/src/pages/Home/Home.js b/src/pages/Home/Home.js index 6a3d2248d..db36ff7dc 100644 --- a/src/pages/Home/Home.js +++ b/src/pages/Home/Home.js @@ -4,11 +4,10 @@ import { useTranslation } from 'react-i18next'; import { BsPlusCircle } from 'react-icons/bs'; import { BiLeftArrow, BiRightArrow } from 'react-icons/bi'; -import { AiOutlineCloseCircle } from 'react-icons/ai'; -import {BsQrCodeScan} from 'react-icons/bs' +import { BsQrCodeScan } from 'react-icons/bs' import Slider from 'react-slick'; -import "slick-carousel/slick/slick.css"; +import "slick-carousel/slick/slick.css"; import "slick-carousel/slick/slick-theme.css"; import addImage from '../../assets/images/cred.png'; @@ -17,22 +16,23 @@ import { useApi } from '../../api'; import CredentialInfo from '../../components/Credentials/CredentialInfo'; import CredentialJson from '../../components/Credentials/CredentialJson'; import CredentialDeleteButton from '../../components/Credentials/CredentialDeleteButton'; -import CredentialDeletePopup from '../../components/Credentials/CredentialDeletePopup'; -import { fetchCredentialData } from '../../components/Credentials/ApiFetchCredential'; -import QRCodeScanner from '../../components/QRCodeScanner/QRCodeScanner'; // Replace with the actual import path +import QRCodeScanner from '../../components/QRCodeScanner/QRCodeScanner'; +import FullscreenPopup from '../../components/Popups/FullscreenImg'; +import DeletePopup from '../../components/Popups/DeletePopup'; +import { CredentialImage } from '../../components/Credentials/CredentialImage'; const Home = () => { - const api = useApi(); - const [credentials, setCredentials] = useState([]); - const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < 768); - const [currentSlide, setCurrentSlide] = useState(1); - const [isImageModalOpen, setImageModalOpen] = useState(false); - const [selectedCredential, setSelectedCredential] = useState(null); - const [showDeletePopup, setShowDeletePopup] = useState(false); - const [loading, setLoading] = useState(false); - - const navigate = useNavigate(); - const sliderRef = useRef(); + const api = useApi(); + const [vcEntityList, setVcEntityList] = useState([]); + const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < 768); + const [currentSlide, setCurrentSlide] = useState(1); + const [showFullscreenImgPopup, setShowFullscreenImgPopup] = useState(false); + const [selectedVcEntity, setSelectedVcEntity] = useState(null); + const [showDeletePopup, setShowDeletePopup] = useState(false); + const [loading, setLoading] = useState(false); + + const navigate = useNavigate(); + const sliderRef = useRef(); const { t } = useTranslation(); const settings = { @@ -47,35 +47,35 @@ const Home = () => { centerPadding: '10px', // Set the padding between adjacent images to 2 pixels style: { margin: '0 10px' }, }; - - useEffect(() => { - const handleResize = () => { - setIsSmallScreen(window.innerWidth < 768); - }; - window.addEventListener('resize', handleResize); + useEffect(() => { + const handleResize = () => { + setIsSmallScreen(window.innerWidth < 768); + }; - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); useEffect(() => { const getData = async () => { - const temp_cred = await fetchCredentialData(api); - console.log(temp_cred); - setCredentials(temp_cred); + const response = await api.get('/storage/vc'); + const vcEntityList = response.data.vc_list; + setVcEntityList(vcEntityList); }; getData(); }, [api]); - const handleAddCredential = () => { - navigate('/add'); - }; + const handleAddCredential = () => { + navigate('/add'); + }; - const handleImageClick = (credential) => { - navigate(`/credential/${credential.id}`); - }; + const handleImageClick = (vcEntity) => { + navigate(`/credential/${vcEntity.credentialIdentifier}`); + }; // QR Code part const [isQRScannerOpen, setQRScannerOpen] = useState(false); @@ -91,7 +91,7 @@ const Home = () => { const handleSureDelete = async () => { setLoading(true); try { - await api.del(`/storage/vc/${selectedCredential.credentialIdentifier}`); + await api.del(`/storage/vc/${selectedVcEntity.credentialIdentifier}`); } catch (error) { console.error('Failed to delete data', error); } @@ -100,43 +100,31 @@ const Home = () => { window.location.href = '/'; }; - return ( - <> -
    -
    -

    {t('common.navItemCredentials')}

    + return ( + <> +
    +
    +

    {t('common.navItemCredentials')}

    -
    - { isSmallScreen && ( + {isSmallScreen && ( + className="px-2 py-2 mb-2 text-white bg-custom-blue hover:bg-custom-blue-hover focus:ring-4 focus:outline-none focus:ring-custom-blue font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-custom-blue-hover dark:hover:bg-custom-blue-hover dark:focus:ring-custom-blue-hover" + onClick={openQRScanner} // Open the QR code scanner modal + > +
    + +
    + )} - -
    - - -
    -
    -

    {t('pageCredentials.description')}

    -
    - {isSmallScreen ? ( - <> - - {credentials.length === 0 ? ( + +
    +
    +

    {t('pageCredentials.description')}

    +
    + {isSmallScreen ? ( + <> + + {vcEntityList.length === 0 ? (
    { ) : ( <> - {credentials.map((credential) => ( + {vcEntityList && vcEntityList.map((vcEntity) => ( <> -
    {setImageModalOpen(true);setSelectedCredential(credential);}}> - {credential.alt} +
    { setShowFullscreenImgPopup(true); setSelectedVcEntity(vcEntity); }}> +
    - {currentSlide} of {credentials.length} + {currentSlide} of {vcEntityList.length} @@ -168,80 +156,80 @@ const Home = () => {
    - - { setShowDeletePopup(true); setSelectedCredential(credential); }} /> - + + { setShowDeletePopup(true); setSelectedVcEntity(vcEntity); }} /> + ))} )} - - ) : ( -
    - {credentials.map((credential) => ( -
    handleImageClick(credential)} - > - {credential.alt} -
    - ))} + + ) : ( +
    + {vcEntityList.map((vcEntity) => (
    handleImageClick(vcEntity)} > - add new credential -
    - - {t('pageCredentials.addCardTitle')} -
    + +
    + ))} +
    + add new credential +
    + + {t('pageCredentials.addCardTitle')}
    - )} -
    -
    - {/* Modal for Fullscreen credential */} - {isImageModalOpen && ( -
    -
    - {selectedCredential.src} -
    - +
    + )}
    +
    + {/* Modal for Fullscreen credential */} + {showFullscreenImgPopup && ( + setShowFullscreenImgPopup(false)} + content={ + + } + /> )} {/* QR Code Scanner Modal */} {isQRScannerOpen && ( -
    - -
    + )} {/* Delete Credential Modal */} - {showDeletePopup && selectedCredential && ( - setShowDeletePopup(false)} - onConfirm={handleSureDelete} - loading={loading} - /> - )} - - ); + {showDeletePopup && selectedVcEntity && ( + setShowDeletePopup(false)} + message={ + + {t('pageCredentials.deletePopup.messagePart1')}{' '} {selectedVcEntity.credentialIdentifier} {t('pageCredentials.deletePopup.messagePart2')} +
    {t('pageCredentials.deletePopup.messagePart3')}{' '} {t('pageCredentials.deletePopup.messagePart4')} +
    + } + loading={loading} + /> + )} + + ); }; export default Home; diff --git a/src/pages/Login/Login.js b/src/pages/Login/Login.js index 9f0bed75d..71ecfb2c4 100644 --- a/src/pages/Login/Login.js +++ b/src/pages/Login/Login.js @@ -4,6 +4,7 @@ import { FaEye, FaExclamationTriangle, FaEyeSlash, FaInfoCircle, FaLock, FaUser import { GoPasskeyFill, GoTrash } from 'react-icons/go'; import { AiOutlineUnlock } from 'react-icons/ai'; import { Trans, useTranslation } from 'react-i18next'; +import { CSSTransition } from 'react-transition-group'; import { useApi } from '../../api'; import { useLocalStorageKeystore } from '../../services/LocalStorageKeystore'; @@ -87,10 +88,10 @@ const PasswordStrength = ({ label, value }) => (
    = 50 && value < 100 - ? 'bg-yellow-500' - : 'bg-green-500' + ? 'bg-red-500' + : value >= 50 && value < 100 + ? 'bg-yellow-500' + : 'bg-green-500' }`} style={{ width: `${value}%` }} >
    @@ -206,7 +207,7 @@ const WebauthnSignupLogin = ({ case 'passkeySignupPrfNotSupported': setError( resolvePrfRetryPrompt(false)} > Cancel @@ -375,7 +376,7 @@ const WebauthnSignupLogin = ({ value={name} required /> -
    +
    )} - {isLogin && cachedUsers?.length > 0 && } + {isLogin && cachedUsers?.length > 0 && }
    -
    +
    {process.env.REACT_APP_VERSION}
    ); diff --git a/src/pages/NotFound/NotFound.js b/src/pages/NotFound/NotFound.js index 04501fc08..253e6e5bf 100644 --- a/src/pages/NotFound/NotFound.js +++ b/src/pages/NotFound/NotFound.js @@ -2,10 +2,11 @@ import React, { useState, useEffect } from 'react'; import logo from '../../assets/images/logo.png'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { CSSTransition } from 'react-transition-group'; const NotFound = () => { const navigate = useNavigate(); - const [isContentVisible, setIsContentVisible] = useState(false); + const [isContentVisible, setIsContentVisible] = useState(false); const { t } = useTranslation(); @@ -14,41 +15,41 @@ const NotFound = () => { }; useEffect(() => { - setTimeout(() => { - setIsContentVisible(true); - }, 0); - }, []); + setTimeout(() => { + setIsContentVisible(true); + }, 0); + }, []); - return( -
    -
    -
    - - logo - -

    - {t('common.walletName')} -

    -
    -
    -

    - {t('notFound.title')} -

    + return ( +
    + +
    + + logo + +

    + {t('common.walletName')} +

    +
    +
    +

    + {t('notFound.title')} +

    -

    - {t('notFound.message')} -

    - +

    + {t('notFound.message')} +

    + +
    -
    -
    -
    + + ); }; diff --git a/src/pages/SendCredentials/SendCredentials.js b/src/pages/SendCredentials/SendCredentials.js index 342123c00..ff2d25a42 100644 --- a/src/pages/SendCredentials/SendCredentials.js +++ b/src/pages/SendCredentials/SendCredentials.js @@ -1,102 +1,100 @@ import React, { useState, useEffect } from 'react'; -import { FaShare } from 'react-icons/fa'; -import {BsQrCodeScan} from 'react-icons/bs' +import { BsQrCodeScan } from 'react-icons/bs' import { useTranslation } from 'react-i18next'; -import { useApi } from '../../api'; - -import Spinner from '../../components/Spinner'; import QRCodeScanner from '../../components/QRCodeScanner/QRCodeScanner'; // Replace with the actual import path +import RedirectPopup from '../../components/Popups/RedirectPopup'; +import { useApi } from '../../api'; function highlightBestSequence(verifier, search) { - if (typeof verifier !== 'string' || typeof search !== 'string') { - return verifier; - } + if (typeof verifier !== 'string' || typeof search !== 'string') { + return verifier; + } - const searchRegex = new RegExp(search, 'gi'); - const highlighted = verifier.replace(searchRegex, '$&'); + const searchRegex = new RegExp(search, 'gi'); + const highlighted = verifier.replace(searchRegex, '$&'); - return highlighted; + return highlighted; } const Verifiers = () => { - const api = useApi(); - const [searchQuery, setSearchQuery] = useState(''); - const [verifiers, setVerifiers] = useState([]); - const [filteredVerifiers, setFilteredVerifiers] = useState([]); - const [showPopup, setShowPopup] = useState(false); - const [selectedVerifier, setSelectedVerifier] = useState(null); + const api = useApi(); + const [searchQuery, setSearchQuery] = useState(''); + const [verifiers, setVerifiers] = useState([]); + const [filteredVerifiers, setFilteredVerifiers] = useState([]); + const [showRedirectPopup, setShowRedirectPopup] = useState(false); + const [selectedVerifier, setSelectedVerifier] = useState(null); const [loading, setLoading] = useState(false); const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < 768); const { t } = useTranslation(); useEffect(() => { - const handleResize = () => { - setIsSmallScreen(window.innerWidth < 768); - }; + const handleResize = () => { + setIsSmallScreen(window.innerWidth < 768); + }; - window.addEventListener('resize', handleResize); + window.addEventListener('resize', handleResize); - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); - useEffect(() => { + useEffect(() => { - const fetchVerifiers = async () => { - try { + const fetchVerifiers = async () => { + try { const fetchedVerifiers = await api.getAllVerifiers(); - setVerifiers(fetchedVerifiers); - setFilteredVerifiers(fetchedVerifiers); - } catch (error) { - console.error('Error fetching verifiers:', error); - } - }; - - fetchVerifiers(); - }, [api]); - - const handleSearch = (event) => { - const query = event.target.value; - setSearchQuery(query); - }; - - useEffect(() => { - const filtered = verifiers.filter((verifier) => { - const name = verifier.name.toLowerCase(); - const query = searchQuery.toLowerCase(); - return name.includes(query); - }); + setVerifiers(fetchedVerifiers); + setFilteredVerifiers(fetchedVerifiers); + } catch (error) { + console.error('Error fetching verifiers:', error); + } + }; + + fetchVerifiers(); + }, [api]); + + const handleSearch = (event) => { + const query = event.target.value; + setSearchQuery(query); + }; + + useEffect(() => { + const filtered = verifiers.filter((verifier) => { + const name = verifier.name.toLowerCase(); + const query = searchQuery.toLowerCase(); + return name.includes(query); + }); setFilteredVerifiers(filtered); - }, [searchQuery, verifiers]); + }, [searchQuery, verifiers]); const handleVerifierClick = async (did) => { const clickedVerifier = verifiers.find((verifier) => verifier.did === did); if (clickedVerifier) { setSelectedVerifier(clickedVerifier); - setShowPopup(true); + setShowRedirectPopup(true); } }; - const handleCancel = () => { - setShowPopup(false); - setSelectedVerifier(null); - }; + const handleCancel = () => { + setShowRedirectPopup(false); + setSelectedVerifier(null); + }; const handleContinue = () => { setLoading(true); - + console.log('Continue with:', selectedVerifier); if (selectedVerifier) { window.location.href = selectedVerifier.url; } - + setLoading(false); - setShowPopup(false); + setShowRedirectPopup(false); }; // QR Code part @@ -110,100 +108,76 @@ const Verifiers = () => { setQRScannerOpen(false); }; - return ( - <> -
    + return ( + <> +
    -

    {t('common.navItemSendCredentials')}

    - - { isSmallScreen && ( +

    {t('common.navItemSendCredentials')}

    + + {isSmallScreen && ( + className="px-2 py-2 mb-2 text-white bg-custom-blue hover:bg-custom-blue-hover focus:ring-4 focus:outline-none focus:ring-custom-blue font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-custom-blue-hover dark:hover:bg-custom-blue-hover dark:focus:ring-custom-blue-hover" + onClick={openQRScanner} // Open the QR code scanner modal + > +
    + +
    + )} - -
    -
    -

    {t('pageSendCredentials.description')}

    - -
    - -
    - - {filteredVerifiers.length === 0 ? ( -

    {t('pageSendCredentials.noFound')}

    - ) : ( -
      - {filteredVerifiers.map((verifier) => ( -
    • handleVerifierClick(verifier.did)} - > -
      -
    • - ))} -
    - )} -
    - - {showPopup && ( -
    -
    -
    - {loading ? ( -
    - -
    - ) : ( - <> -

    - - {t('pageSendCredentials.popup.title')} {selectedVerifier?.name} -

    -
    -

    - {t('pageSendCredentials.popup.messagePart1')} {selectedVerifier?.name}{t('pageSendCredentials.popup.messagePart2')} -

    -
    - - -
    - - )} -
    + +
    +
    +

    {t('pageSendCredentials.description')}

    + +
    +
    + + {filteredVerifiers.length === 0 ? ( +

    {t('pageSendCredentials.noFound')}

    + ) : ( +
      + {filteredVerifiers.map((verifier) => ( +
    • handleVerifierClick(verifier.did)} + > +
      +
    • + ))} +
    + )} +
    + + {showRedirectPopup && ( + )} {/* QR Code Scanner Modal */} {isQRScannerOpen && ( -
    - -
    - )} - - ); + + )} + + ); }; export default Verifiers; diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index fdcf5f720..64c3e1bb8 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -8,7 +8,7 @@ import { UserData, WebauthnCredential } from '../../api/types'; import { compareBy, jsonStringifyTaggedBinary, toBase64Url } from '../../util'; import { formatDate } from '../../functions/DateFormat'; import { WrappedKeyInfo, useLocalStorageKeystore } from '../../services/LocalStorageKeystore'; -import ConfirmDeletePopup from '../../components/ConfirmDeletePopup/ConfirmDeletePopup'; +import DeletePopup from '../../components/Popups/DeletePopup'; import { useNavigate } from 'react-router-dom'; const Dialog = ({ @@ -503,6 +503,7 @@ const WebauthnCredentialItem = ({ className={` ${!onDelete || unlocked ? "bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700" : "bg-blue-600 hover:bg-blue-700 bg-gray-300 hover:bg-gray-300 cursor-not-allowed"} flex flex-row flex-nowrap items-center text-white font-medium rounded-lg text-sm px-4 py-2 text-center`} type="button" onClick={() => setEditing(true)} + disabled={!unlocked} title={!unlocked ? t("pageSettings.passkeyItem.renameButtonTitleLocked") : ""} aria-label={t('pageSettings.passkeyItem.renameAriaLabel', { passkeyLabel: currentLabel })} > @@ -517,6 +518,7 @@ const WebauthnCredentialItem = ({ className={` ${unlocked ? "bg-red-600 hover:bg-red-700 hover:text-white text-white" : "bg-gray-300 text-red-400 cursor-not-allowed hover:bg-gray-300"} text-sm font-medium rounded-lg text-sm px-5 py-2.5 text-center ml-2 px-4 py-2`} type="button" onClick={openDeleteConfirmation} + disabled={!unlocked} aria-label={t('pageSettings.passkeyItem.deleteAriaLabel', { passkeyLabel: currentLabel })} title={!unlocked ? t("pageSettings.passkeyItem.deleteButtonTitleLocked") : ""} @@ -524,7 +526,7 @@ const WebauthnCredentialItem = ({ )} - {
    )} - - {t('pageSettings.deleteAccount.messageDeleteAccount1')} {t('pageSettings.deleteAccount.messageDeleteAccount2')} ? + {t('pageSettings.deleteAccount.messageDeleteAccount1')} {t('pageSettings.deleteAccount.messageDeleteAccount2')} ? } loading={loading} diff --git a/src/pages/VerificationResult/VerificationResult.js b/src/pages/VerificationResult/VerificationResult.js index f0f7ff8ca..d3a774116 100644 --- a/src/pages/VerificationResult/VerificationResult.js +++ b/src/pages/VerificationResult/VerificationResult.js @@ -2,16 +2,16 @@ import React from 'react'; import { FaCheckCircle } from 'react-icons/fa'; const VerificationResult = () => { - return ( - <> -
    - -

    - Verification Successful! -

    -
    - - ); + return ( + <> +
    + +

    + Verification Successful! +

    +
    + + ); }; export default VerificationResult; diff --git a/src/services/LocalStorageKeystore.ts b/src/services/LocalStorageKeystore.ts index ea482c0cc..6f36ca609 100644 --- a/src/services/LocalStorageKeystore.ts +++ b/src/services/LocalStorageKeystore.ts @@ -10,6 +10,16 @@ import { useClearStorages, useLocalStorage, useSessionStorage } from "../compone import { jsonParseTaggedBinary, jsonStringifyTaggedBinary, toBase64Url } from "../util"; import { useIndexedDb } from "../components/useIndexedDb"; +import { base58btc } from 'multiformats/bases/base58'; +import { varint } from 'multiformats'; +import * as KeyDidResolver from 'key-did-resolver' +import {Resolver} from 'did-resolver' + + +const DID_KEY_VERSION = process.env.REACT_APP_DID_KEY_VERSION; + +const keyDidResolver = KeyDidResolver.getResolver(); +const didResolver = new Resolver(keyDidResolver); type UserData = { displayName: string; @@ -309,7 +319,7 @@ export interface LocalStorageKeystore { createIdToken(nonce: string, audience: string): Promise<{ id_token: string; }>, signJwtPresentation(nonce: string, audience: string, verifiableCredentials: any[]): Promise<{ vpjwt: string }>, - generateOpenid4vciProof(audience: string, nonce: string): Promise<{ proof_jwt: string }>, + generateOpenid4vciProof(nonce: string, audience: string): Promise<{ proof_jwt: string }>, } export function useLocalStorageKeystore(): LocalStorageKeystore { @@ -533,18 +543,90 @@ export function useLocalStorageKeystore(): LocalStorageKeystore { return result; }; + const compressPublicKey = async (uncompressedRawPublicKey: Uint8Array) => { + // Check if the uncompressed public key has the correct length + if (uncompressedRawPublicKey.length !== 65 || uncompressedRawPublicKey[0] !== 0x04) { + throw new Error('Invalid uncompressed public key format'); + } + + // Get the x-coordinate + const x = uncompressedRawPublicKey.subarray(1, 33) as any; + const y = uncompressedRawPublicKey.subarray(33, 65) as any; + // Determine the parity (odd or even) from the last byte + const parity = y % 2 === 0 ? 0x02 : 0x03; + + // Create the compressed public key by concatenating the x-coordinate and the parity byte + const compressedPublicKey = new Uint8Array([parity, ...x]); + + return compressedPublicKey; + } + + const getPublicKeyFromDID = async (did: string) => { + let decodedPublicKey = {} + const multibaseValue = did.split(':')[2]; + const decodedMultibaseValue = base58btc.decode(multibaseValue); + let multicodecValue = varint.decode(decodedMultibaseValue.slice(0, 2)); // header + const rawPublicKeyBytes = decodedMultibaseValue.slice(2,); + console.log("Raw public key bytes = ", rawPublicKeyBytes); + console.log("Raw public key bytes len = ", rawPublicKeyBytes.length); + } + + const createW3CDID = async (publicKey: CryptoKey) => { + const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", publicKey)); + const compressedPublicKeyBytes = await compressPublicKey(rawPublicKey) + console.log("Raw pub key bytes = ", compressedPublicKeyBytes) + // Concatenate keyType and publicKey Uint8Arrays + const multicodecPublicKey = new Uint8Array(2 + compressedPublicKeyBytes.length); + console.log("Compresses pub key length = ", compressedPublicKeyBytes.length) + varint.encodeTo(0x1200, multicodecPublicKey, 0); + + multicodecPublicKey.set(compressedPublicKeyBytes, 2); + + // Base58-btc encode the multicodec public key + const base58EncodedPublicKey = base58btc.encode(multicodecPublicKey); + + // Construct the did:key string + const didKeyString = `did:key:${base58EncodedPublicKey}`; + + const doc = await didResolver.resolve(didKeyString); + if (doc.didDocument == null) { + throw new Error("Failed to resolve the generated DID"); + } + console.log("Getting back") + await getPublicKeyFromDID(didKeyString); + return { didKeyString }; + } + const createWallet = async (mainKey: CryptoKey): Promise<{ publicData: PublicData, privateDataJwe: string }> => { - const alg = "ES256"; - const { publicKey, privateKey } = await jose.generateKeyPair(alg, { extractable: true }); + const jwtAlg = "ES256"; + const signatureAlgorithmFamily = "ECDSA"; + const namedCurve = "P-256"; + + const { publicKey, privateKey } = await crypto.subtle.generateKey( + { name: signatureAlgorithmFamily, namedCurve: namedCurve }, + true, + [ 'sign', 'verify' ] + ); + + const publicKeyJWK: JWK = await crypto.subtle.exportKey("jwk", publicKey) as JWK; - const wrappedPrivateKey: WrappedPrivateKey = await wrapPrivateKey(privateKey as CryptoKey, mainKey); + let did = null; + if (DID_KEY_VERSION === "p256-pub") { + const { didKeyString } = await createW3CDID(publicKey); + did = didKeyString; + } + else if (DID_KEY_VERSION === "jwk_jcs-pub") { + did = util.createDid(publicKeyJWK as JWK); + } + else { + throw new Error("Application was not configured with a correct DID_KEY_VERSION"); + } + const wrappedPrivateKey: WrappedPrivateKey = await wrapPrivateKey(privateKey, mainKey); - const publicKeyJWK = await jose.exportJWK(publicKey); - const did = util.createDid(publicKeyJWK); const publicData = { publicKey: publicKeyJWK, did: did, - alg: alg, + alg: jwtAlg, verificationMethod: did + "#" + did.split(':')[2] }; const privateData: PrivateData = { @@ -730,7 +812,7 @@ export function useLocalStorageKeystore(): LocalStorageKeystore { return { vpjwt: jws }; }, - generateOpenid4vciProof: async (audience: string, nonce: string): Promise<{ proof_jwt: string }> => { + generateOpenid4vciProof: async (nonce: string, audience: string): Promise<{ proof_jwt: string }> => { const [{ alg, did, wrappedPrivateKey }, sessionKey] = await openPrivateData(); const privateKey = await unwrapPrivateKey(wrappedPrivateKey, sessionKey); const header = { diff --git a/src/services/SigningRequestHandlers.ts b/src/services/SigningRequestHandlers.ts index 629caa6d5..6759cab56 100644 --- a/src/services/SigningRequestHandlers.ts +++ b/src/services/SigningRequestHandlers.ts @@ -38,7 +38,7 @@ export function SigningRequestHandlerService(): SigningRequestHandlers { }, handleGenerateOpenid4vciProofSigningRequest: async (socket, keystore, { message_id, audience, nonce }) => { - const { proof_jwt } = await keystore.generateOpenid4vciProof(audience, nonce) + const { proof_jwt } = await keystore.generateOpenid4vciProof(nonce, audience) console.log("proof jwt = ", proof_jwt); const outgoingMessage: ClientSocketMessage = { message_id: message_id, @@ -50,4 +50,4 @@ export function SigningRequestHandlerService(): SigningRequestHandlers { socket.send(JSON.stringify(outgoingMessage)); }, } -} \ No newline at end of file +} diff --git a/src/types/shared.types.ts b/src/types/shared.types.ts index 817a37a2a..5f7394369 100644 --- a/src/types/shared.types.ts +++ b/src/types/shared.types.ts @@ -5,7 +5,7 @@ export enum SignatureAction { } export type WalletKeystoreRequest = ( - { action: SignatureAction.generateOpenid4vciProof, audience: string, nonce: string } + { action: SignatureAction.generateOpenid4vciProof, nonce: string, audience: string } | { action: SignatureAction.createIdToken, nonce: string, audience: string } | { action: SignatureAction.signJwtPresentation, nonce: string, audience: string, verifiableCredentials: any[] } ); diff --git a/src/util.ts b/src/util.ts index deadcacaa..6a417d799 100644 --- a/src/util.ts +++ b/src/util.ts @@ -44,7 +44,7 @@ function replacerUint8ArrayToTaggedBase64Url(key: string, value: any): any { } export function jsonStringifyTaggedBinary(value: any): string { - return JSON.stringify(value, replacerUint8ArrayToTaggedBase64Url); + return JSON.stringify(value, replacerUint8ArrayToTaggedBase64Url); } function reviverTaggedBinaryToUint8Array(key: string, value: any): any { @@ -56,7 +56,7 @@ function reviverTaggedBinaryToUint8Array(key: string, value: any): any { } export function jsonParseTaggedBinary(json: string): any { - return JSON.parse(json, reviverTaggedBinaryToUint8Array); + return JSON.parse(json, reviverTaggedBinaryToUint8Array); } export function compareBy(f: (v: T) => U): (a: T, b: T) => number { diff --git a/tailwind.config.js b/tailwind.config.js index 7192bc1e5..38169285c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,14 +6,17 @@ module.exports = { borderRadius: { 'xl': '1rem', }, - width:{ - '55':55, + width: { + '55': 55, }, colors: { 'custom-blue': '#003476', 'custom-blue-hover': '#002b62', 'custom-light-blue': '#68caf1', 'light-red': '#ffcccc', + }, + screens: { + 'max480': { 'max': '480px' }, } }, }, diff --git a/yarn.lock b/yarn.lock index 63af10e58..85516deb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1133,7 +1133,7 @@ resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== @@ -2324,6 +2324,45 @@ resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz" integrity sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw== +"@sd-jwt/core@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.2.1.tgz#75b0b273758e6be050e042a75bd6a0c4a2a7258e" + integrity sha512-8auyt3mfzgAK+IP9mNc3kSONdo5x2Y8ypNj5gHKP7N81nVeyI+DHethoPQv84JVcqYYcNwHwyrc2Z5k7rg2lFQ== + dependencies: + "@sd-jwt/decode" "0.2.1" + "@sd-jwt/present" "0.2.1" + "@sd-jwt/types" "0.2.1" + "@sd-jwt/utils" "0.2.1" + +"@sd-jwt/decode@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.2.1.tgz#e0fb32dd2a95440ad69237e66ea2cd4770ec7e09" + integrity sha512-rs55WB3llrMObxN8jeMl06km/h0WivO9jSWNubO9JUIdlfrVhssU38xoXakvQeSDjAJkUUhfZcvmC2vNo1X6Wg== + dependencies: + "@sd-jwt/types" "0.2.1" + "@sd-jwt/utils" "0.2.1" + +"@sd-jwt/present@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.2.1.tgz#ff9958626b271a60d539dd1e601763ff33c024e8" + integrity sha512-yWIAR2C/q1jNUwzAeUlUcf3WCTEcSSGo9pltHW5AXptELjyaWGSmC5p6o9ucDXHvBnicfPONhe5OdUCSpiCntw== + dependencies: + "@sd-jwt/types" "0.2.1" + "@sd-jwt/utils" "0.2.1" + +"@sd-jwt/types@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.2.1.tgz#e1e6b47728dffa90ed244e15e2253bd01793cb96" + integrity sha512-nbNik/cq6UIMsN144FcgPZQzaqIsjEEj307j3ZSFORkQBR4Tsmcj54aswTuNh0Z0z/4aSbfw14vOKBZvRWyVLQ== + +"@sd-jwt/utils@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.2.1.tgz#35ad83232eab2de911e765d93222acd871982a5e" + integrity sha512-9eRrge44dhE3fenawR/RZGxP5iuW9DtgdOVANu/JK5PEl80r0fDsMwm/gDjuv8OgLDCmQ6uSaVte1lYaTG71bQ== + dependencies: + "@sd-jwt/types" "0.2.1" + buffer "*" + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" @@ -3768,6 +3807,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + base64url@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" @@ -3890,6 +3934,14 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@*: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtin-modules@^3.1.0: version "3.3.0" resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" @@ -4649,7 +4701,7 @@ detect-port-alt@^1.1.6: did-resolver@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/did-resolver/-/did-resolver-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/did-resolver/-/did-resolver-4.1.0.tgz#740852083c4fd5bf9729d528eca5d105aff45eb6" integrity sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA== didyoumean@^1.2.2: @@ -4717,6 +4769,14 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz" @@ -6082,6 +6142,11 @@ identity-obj-proxy@^3.0.0: dependencies: harmony-reflect "^1.4.6" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.2.0: version "5.2.4" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" @@ -7554,7 +7619,7 @@ multiformats@^11.0.1: resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-11.0.2.tgz#b14735efc42cd8581e73895e66bebb9752151b60" integrity sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg== -multiformats@^12.0.1: +multiformats@^12.0.1, multiformats@^12.1.3: version "12.1.3" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-12.1.3.tgz#cbf7a9861e11e74f8228b21376088cb43ba8754e" integrity sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw== @@ -8985,6 +9050,16 @@ react-snowfall@^1.2.1: dependencies: react-fast-compare "^3.2.0" +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-webcam@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/react-webcam/-/react-webcam-7.2.0.tgz#64141c4c7bdd3e956620500187fa3fcc77e1fd49"