From 36dc223276e0a55fcf5c278075da6a5e8d8d9051 Mon Sep 17 00:00:00 2001 From: Martin Domajnko <35891136+martines3000@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:20:14 +0200 Subject: [PATCH 1/3] feat: QR code scanning support (#290) * feat: qr generation, scanning, prisma, api routes * feat: working demo * chore: update OPTIONS request * feat: update toasts * feat: add postinstall script for prisma * feat: reduces serverless function sizes * chore: should fix scrolling issues * fix: fixes crashing on no camera permissions --- .gitignore | 1 + .prettierignore | 1 + packages/dapp/.env.example | 3 + packages/dapp/next.config.js | 9 + packages/dapp/package.json | 6 + packages/dapp/prisma/schema.prisma | 15 ++ .../authorization-request/page.tsx | 0 .../create-verifiable-presentation/page.tsx | 0 .../{ => (protected)}/get-credential/page.tsx | 0 .../app/[locale]/app/(protected)/layout.tsx | 14 ++ .../[locale]/app/{ => (protected)}/page.tsx | 0 .../app/{ => (protected)}/settings/page.tsx | 0 .../verifiable-credential/[slug]/page.tsx | 0 .../verifiable-credential/not-found.tsx | 0 .../app/(public)/qr-code-session/page.tsx | 22 ++ packages/dapp/src/app/[locale]/app/layout.tsx | 10 +- .../app/api/qr-code-session/[id]/route.tsx | 118 ++++++++++ .../src/components/AppBottomBar/index.tsx | 14 +- .../dapp/src/components/AppNavbar/index.tsx | 8 +- .../components/AuthorizationRequest/index.tsx | 79 +++---- .../CheckMetaMaskCompatibility/index.tsx | 214 ++++++++++++++++++ .../components/ConnectedProvider/index.tsx | 49 +--- .../src/components/Controlbar/Controlbar.tsx | 46 ++-- .../CreateConnectionModal.tsx | 122 ++++++++++ .../components/CreateConnectionCard/index.tsx | 46 ++++ .../components/CredentialOfferModal/index.tsx | 73 ++++++ .../dapp/src/components/DeleteModal/index.tsx | 65 +++--- .../src/components/GetCredential/index.tsx | 95 ++++---- .../src/components/MetaMaskProvider/index.tsx | 167 +------------- .../components/MethodDropdownMenu/index.tsx | 55 +++-- .../src/components/ModifyDSModal/index.tsx | 50 ++-- .../src/components/QRCodeScanner/index.tsx | 84 +++++++ .../QRCodeSessionProvider/index.tsx | 121 ++++++++++ .../ScanConnectionCard/ScanQRCodeModal.tsx | 83 +++++++ .../components/ScanConnectionCard/index.tsx | 180 +++++++++++++++ .../src/components/SettingsCard/index.tsx | 33 +-- .../src/components/ToastWrapper/index.tsx | 5 +- .../dapp/src/components/VCTable/index.tsx | 48 ++-- packages/dapp/src/messages/en.json | 4 +- packages/dapp/src/stores/index.ts | 1 + packages/dapp/src/stores/sessionStore.ts | 25 ++ packages/dapp/src/stores/toastStore.ts | 20 +- packages/dapp/src/styles/globals.css | 4 +- packages/dapp/src/utils/prisma.ts | 9 + pnpm-lock.yaml | 66 ++++++ 45 files changed, 1492 insertions(+), 473 deletions(-) create mode 100644 packages/dapp/prisma/schema.prisma rename packages/dapp/src/app/[locale]/app/{ => (protected)}/authorization-request/page.tsx (100%) rename packages/dapp/src/app/[locale]/app/{ => (protected)}/create-verifiable-presentation/page.tsx (100%) rename packages/dapp/src/app/[locale]/app/{ => (protected)}/get-credential/page.tsx (100%) create mode 100644 packages/dapp/src/app/[locale]/app/(protected)/layout.tsx rename packages/dapp/src/app/[locale]/app/{ => (protected)}/page.tsx (100%) rename packages/dapp/src/app/[locale]/app/{ => (protected)}/settings/page.tsx (100%) rename packages/dapp/src/app/[locale]/app/{ => (protected)}/verifiable-credential/[slug]/page.tsx (100%) rename packages/dapp/src/app/[locale]/app/{ => (protected)}/verifiable-credential/not-found.tsx (100%) create mode 100644 packages/dapp/src/app/[locale]/app/(public)/qr-code-session/page.tsx create mode 100644 packages/dapp/src/app/api/qr-code-session/[id]/route.tsx create mode 100644 packages/dapp/src/components/CheckMetaMaskCompatibility/index.tsx create mode 100644 packages/dapp/src/components/CreateConnectionCard/CreateConnectionModal.tsx create mode 100644 packages/dapp/src/components/CreateConnectionCard/index.tsx create mode 100644 packages/dapp/src/components/CredentialOfferModal/index.tsx create mode 100644 packages/dapp/src/components/QRCodeScanner/index.tsx create mode 100644 packages/dapp/src/components/QRCodeSessionProvider/index.tsx create mode 100644 packages/dapp/src/components/ScanConnectionCard/ScanQRCodeModal.tsx create mode 100644 packages/dapp/src/components/ScanConnectionCard/index.tsx create mode 100644 packages/dapp/src/stores/sessionStore.ts create mode 100644 packages/dapp/src/utils/prisma.ts diff --git a/.gitignore b/.gitignore index 779f9d219..b774302d0 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ nx-cloud.env .yarn/install-state.gz .pnp.* .vscode +.vercel diff --git a/.prettierignore b/.prettierignore index 71c46681f..489ea7105 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ apps libs packages pnpm-lock.yaml +.vercel diff --git a/packages/dapp/.env.example b/packages/dapp/.env.example index 1a1f22bcd..8e8fc7e1a 100644 --- a/packages/dapp/.env.example +++ b/packages/dapp/.env.example @@ -1,2 +1,5 @@ NEXT_PUBLIC_DEMO_ISSUER=http://localhost:3003 NEXT_PUBLIC_DEMO_VERIFIER=http://localhost:3004 + +# Prisma +DATABASE_URL= diff --git a/packages/dapp/next.config.js b/packages/dapp/next.config.js index a83dc1aaf..272f27d0f 100644 --- a/packages/dapp/next.config.js +++ b/packages/dapp/next.config.js @@ -1,4 +1,5 @@ const StylelintPlugin = require('stylelint-webpack-plugin'); +const path = require('path'); // Content-Security-Policy // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy @@ -31,6 +32,14 @@ const nextConfig = { optimizeFonts: true, experimental: { appDir: true, + outputFileTracingRoot: path.join(__dirname, '../../'), + outputFileTracingExcludes: { + '*': [ + 'node_modules/@swc/core-linux-x64-gnu', + 'node_modules/@swc/core-linux-x64-musl', + 'node_modules/@esbuild/linux-x64', + ], + }, }, env: { USE_LOCAL: process.env.USE_LOCAL || 'false', diff --git a/packages/dapp/package.json b/packages/dapp/package.json index 380866e47..4206d7d3b 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -9,6 +9,7 @@ "build:docker": "pnpm build", "dev": "cross-env USE_LOCAL='true' next dev", "docker:build": "docker build . -t blockchain-lab-um/dapp:latest", + "postinstall": "pnpm prisma generate --schema=./prisma/schema.prisma", "lint": "pnpm lint:next && pnpm lint:tsc && pnpm lint:prettier && pnpm lint:stylelint", "lint:fix": "next lint . --fix && prettier . --write", "lint:next": "next lint", @@ -26,6 +27,7 @@ "@heroicons/react": "^2.0.18", "@metamask/detect-provider": "^2.0.0", "@metamask/providers": "^10.2.0", + "@prisma/client": "^4.16.1", "@radix-ui/react-toast": "^1.1.4", "@tanstack/react-table": "^8.9.2", "@veramo/core": "5.2.0", @@ -34,6 +36,7 @@ "clsx": "^1.2.1", "did-jwt-vc": "^3.2.4", "ethers": "^6.5.1", + "html5-qrcode": "^2.3.8", "lint-staged": "^13.2.2", "luxon": "^3.3.0", "next": "13.4.6", @@ -42,9 +45,11 @@ "next-themes": "^0.2.1", "prettier-plugin-tailwindcss": "^0.3.0", "prop-types": "^15.8.1", + "qrcode.react": "^3.1.0", "qs": "^6.11.2", "react": "18.2.0", "react-dom": "18.2.0", + "swr": "^2.2.0", "tailwind-scrollbar": "^3.0.4", "zustand": "^4.3.8" }, @@ -67,6 +72,7 @@ "postcss": "^8.4.24", "prettier": "^2.8.4", "prettier-plugin-packagejson": "^2.4.3", + "prisma": "^4.16.1", "rimraf": "^5.0.1", "sass": "^1.63.4", "stylelint": "^15.8.0", diff --git a/packages/dapp/prisma/schema.prisma b/packages/dapp/prisma/schema.prisma new file mode 100644 index 000000000..e2ad86a3e --- /dev/null +++ b/packages/dapp/prisma/schema.prisma @@ -0,0 +1,15 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + relationMode = "prisma" +} + +model sessions { + id String @id @default(uuid()) + data String? @db.VarChar(512) + iv String? @db.VarChar(128) +} diff --git a/packages/dapp/src/app/[locale]/app/authorization-request/page.tsx b/packages/dapp/src/app/[locale]/app/(protected)/authorization-request/page.tsx similarity index 100% rename from packages/dapp/src/app/[locale]/app/authorization-request/page.tsx rename to packages/dapp/src/app/[locale]/app/(protected)/authorization-request/page.tsx diff --git a/packages/dapp/src/app/[locale]/app/create-verifiable-presentation/page.tsx b/packages/dapp/src/app/[locale]/app/(protected)/create-verifiable-presentation/page.tsx similarity index 100% rename from packages/dapp/src/app/[locale]/app/create-verifiable-presentation/page.tsx rename to packages/dapp/src/app/[locale]/app/(protected)/create-verifiable-presentation/page.tsx diff --git a/packages/dapp/src/app/[locale]/app/get-credential/page.tsx b/packages/dapp/src/app/[locale]/app/(protected)/get-credential/page.tsx similarity index 100% rename from packages/dapp/src/app/[locale]/app/get-credential/page.tsx rename to packages/dapp/src/app/[locale]/app/(protected)/get-credential/page.tsx diff --git a/packages/dapp/src/app/[locale]/app/(protected)/layout.tsx b/packages/dapp/src/app/[locale]/app/(protected)/layout.tsx new file mode 100644 index 000000000..0bfffc95b --- /dev/null +++ b/packages/dapp/src/app/[locale]/app/(protected)/layout.tsx @@ -0,0 +1,14 @@ +import ConnectedProvider from '@/components/ConnectedProvider'; +import MetaMaskProvider from '@/components/MetaMaskProvider'; + +export default async function ProtectedLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/packages/dapp/src/app/[locale]/app/page.tsx b/packages/dapp/src/app/[locale]/app/(protected)/page.tsx similarity index 100% rename from packages/dapp/src/app/[locale]/app/page.tsx rename to packages/dapp/src/app/[locale]/app/(protected)/page.tsx diff --git a/packages/dapp/src/app/[locale]/app/settings/page.tsx b/packages/dapp/src/app/[locale]/app/(protected)/settings/page.tsx similarity index 100% rename from packages/dapp/src/app/[locale]/app/settings/page.tsx rename to packages/dapp/src/app/[locale]/app/(protected)/settings/page.tsx diff --git a/packages/dapp/src/app/[locale]/app/verifiable-credential/[slug]/page.tsx b/packages/dapp/src/app/[locale]/app/(protected)/verifiable-credential/[slug]/page.tsx similarity index 100% rename from packages/dapp/src/app/[locale]/app/verifiable-credential/[slug]/page.tsx rename to packages/dapp/src/app/[locale]/app/(protected)/verifiable-credential/[slug]/page.tsx diff --git a/packages/dapp/src/app/[locale]/app/verifiable-credential/not-found.tsx b/packages/dapp/src/app/[locale]/app/(protected)/verifiable-credential/not-found.tsx similarity index 100% rename from packages/dapp/src/app/[locale]/app/verifiable-credential/not-found.tsx rename to packages/dapp/src/app/[locale]/app/(protected)/verifiable-credential/not-found.tsx diff --git a/packages/dapp/src/app/[locale]/app/(public)/qr-code-session/page.tsx b/packages/dapp/src/app/[locale]/app/(public)/qr-code-session/page.tsx new file mode 100644 index 000000000..a197b604e --- /dev/null +++ b/packages/dapp/src/app/[locale]/app/(public)/qr-code-session/page.tsx @@ -0,0 +1,22 @@ +import { Metadata } from 'next'; + +import CreateConnectionCard from '@/components/CreateConnectionCard'; +import ScanConnectionCard from '@/components/ScanConnectionCard'; + +export const metadata: Metadata = { + title: 'Dashboard', + description: 'Dashboard for Masca Dapp.', +}; + +export default function Page() { + return ( +
+
+ +
+
+ +
+
+ ); +} diff --git a/packages/dapp/src/app/[locale]/app/layout.tsx b/packages/dapp/src/app/[locale]/app/layout.tsx index cc308ce2a..8e85486f7 100644 --- a/packages/dapp/src/app/[locale]/app/layout.tsx +++ b/packages/dapp/src/app/[locale]/app/layout.tsx @@ -1,7 +1,7 @@ import AppBottomBar from '@/components/AppBottomBar'; import AppNavbar from '@/components/AppNavbar'; -import ConnectedProvider from '@/components/ConnectedProvider'; -import MetaMaskProvider from '@/components/MetaMaskProvider'; +import CheckMetaMaskCompatibility from '@/components/CheckMetaMaskCompatibility'; +import QRCodeSessionProvider from '@/components/QRCodeSessionProvider'; import ToastWrapper from '@/components/ToastWrapper'; export default async function AppLayout({ @@ -14,9 +14,9 @@ export default async function AppLayout({
- - {children} - + + + {children}
diff --git a/packages/dapp/src/app/api/qr-code-session/[id]/route.tsx b/packages/dapp/src/app/api/qr-code-session/[id]/route.tsx new file mode 100644 index 000000000..d0a63ccf3 --- /dev/null +++ b/packages/dapp/src/app/api/qr-code-session/[id]/route.tsx @@ -0,0 +1,118 @@ +import { NextResponse } from 'next/server'; + +import { prisma } from '@/utils/prisma'; + +export async function GET( + _: Request, + { params: { id } }: { params: { id: string } } +) { + if (!id) { + return NextResponse.json( + { error_description: 'Missing sessionId parameter' }, + { status: 400 } + ); + } + + // Get session from database + const session = await prisma.sessions.findUnique({ + where: { + id, + }, + }); + + if (!session) { + return NextResponse.json( + { error_description: 'Session not found' }, + { status: 404 } + ); + } + + await prisma.sessions.delete({ + where: { + id, + }, + }); + + // Get session data + return NextResponse.json( + { data: session.data, iv: session.iv }, + { status: 200 } + ); +} + +export async function POST( + request: Request, + { params: { id } }: { params: { id: string } } +) { + try { + const jsonData = await request.json(); + + const { data, iv } = jsonData; + + if (!id) { + return NextResponse.json( + { error_description: 'Missing sessionId' }, + { + status: 400, + } + ); + } + + if (!data) { + return NextResponse.json( + { error_description: "Missing 'data' parameter" }, + { + status: 400, + } + ); + } + + if (!iv) { + return NextResponse.json( + { error_description: "Missing 'iv' parameter" }, + { + status: 400, + } + ); + } + + // Put session data in database + await prisma.sessions.upsert({ + where: { + id, + }, + update: { + data, + iv, + }, + create: { + id, + data, + iv, + }, + }); + + return new NextResponse(null, { + status: 200, + }); + } catch (e) { + console.log(e); + return NextResponse.json( + { error_description: 'Bad request' }, + { + status: 400, + } + ); + } +} + +export async function OPTIONS(_: Request) { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} diff --git a/packages/dapp/src/components/AppBottomBar/index.tsx b/packages/dapp/src/components/AppBottomBar/index.tsx index 1b24ba303..9615826f2 100644 --- a/packages/dapp/src/components/AppBottomBar/index.tsx +++ b/packages/dapp/src/components/AppBottomBar/index.tsx @@ -2,6 +2,8 @@ import { useState } from 'react'; import Link from 'next/link'; +import { EllipsisHorizontalIcon } from '@heroicons/react/24/solid'; +import clsx from 'clsx'; import { useTranslations } from 'next-intl'; const MAIN_LINKS = [ @@ -24,6 +26,10 @@ const OTHER_LINKS = [ name: 'authorization-request', href: '/app/authorization-request', }, + { + name: 'qr-code-session', + href: '/app/qr-code-session', + }, ]; const AppBottomBar = () => { @@ -51,15 +57,15 @@ const AppBottomBar = () => { ))} - {/* */} + - {/*
{ ))}
- */} + ); }; diff --git a/packages/dapp/src/components/AppNavbar/index.tsx b/packages/dapp/src/components/AppNavbar/index.tsx index dc95bbc82..624b45355 100644 --- a/packages/dapp/src/components/AppNavbar/index.tsx +++ b/packages/dapp/src/components/AppNavbar/index.tsx @@ -30,6 +30,10 @@ const EXTRA_LINKS = [ name: 'authorization-request', href: '/app/authorization-request', }, + { + name: 'qr-code-session', + href: '/app/qr-code-session', + }, ]; export default function AppNavbar() { @@ -62,7 +66,7 @@ export default function AppNavbar() { {t(`menu.${name}`)} ))} - {/* {EXTRA_LINKS.map(({ name, href }) => ( + {EXTRA_LINKS.map(({ name, href }) => ( {t(`menu.${name}`)} - ))} */} + ))}
diff --git a/packages/dapp/src/components/AuthorizationRequest/index.tsx b/packages/dapp/src/components/AuthorizationRequest/index.tsx index d098bd550..97c8a5a06 100644 --- a/packages/dapp/src/components/AuthorizationRequest/index.tsx +++ b/packages/dapp/src/components/AuthorizationRequest/index.tsx @@ -5,7 +5,6 @@ import type { AuthorizationRequest } from '@blockchain-lab-um/oidc-types'; import { isError } from '@blockchain-lab-um/utils'; import { VerifiableCredential } from '@veramo/core'; import qs from 'qs'; -import { shallow } from 'zustand/shallow'; import Button from '@/components/Button'; import ConnectedProvider from '@/components/ConnectedProvider'; @@ -34,18 +33,6 @@ const AuthorizationRequestFlow = () => { const [isAuthorizationResponseValid, setIsAuthorizationResponseValid] = useState(null); - const { setTitle, setText, setLoading, setToastOpen, setType } = - useToastStore( - (state) => ({ - setTitle: state.setTitle, - setText: state.setText, - setLoading: state.setLoading, - setToastOpen: state.setOpen, - setType: state.setType, - }), - shallow - ); - const parseAuthorizationRequest = () => { if (!authorizationRequestURI) return; @@ -58,13 +45,14 @@ const AuthorizationRequestFlow = () => { setParsedAuthorizationRequestURI(parsedRequest); } catch (e) { console.log(e); - setToastOpen(false); - setType('error'); setTimeout(() => { - setTitle('Error while parsing authorization request'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Error while parsing authorization request', + type: 'error', + loading: false, + }); + }, 200); } }; @@ -85,13 +73,14 @@ const AuthorizationRequestFlow = () => { ); setAuthorizationRequestURI(await authorizationRequestResponse.text()); } catch (e) { - setToastOpen(false); - setType('error'); setTimeout(() => { - setTitle('Error while getting Demo Authorization Request'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Error while getting DEMO authorization request', + type: 'error', + loading: false, + }); + }, 200); console.log(e); } @@ -108,14 +97,14 @@ const AuthorizationRequestFlow = () => { }); if (isError(handleAuthorizationRequestResponse)) { - setToastOpen(false); - setType('error'); setTimeout(() => { - setTitle('Error while handling authorization request'); - setText(handleAuthorizationRequestResponse.error); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Error while handling authorization request', + type: 'error', + loading: false, + }); + }, 200); return; } @@ -135,13 +124,14 @@ const AuthorizationRequestFlow = () => { }); if (isError(sendOIDCAuthorizationResponseResponse)) { - setToastOpen(false); - setType('error'); setTimeout(() => { - setTitle('Error while sending authorization response'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Error while sending authorization response', + type: 'error', + loading: false, + }); + }, 200); setAuthorizationResponseError( sendOIDCAuthorizationResponseResponse.error @@ -157,13 +147,14 @@ const AuthorizationRequestFlow = () => { setIsAuthorizationResponseValid(result); // Show result in toast - setToastOpen(false); - setType(result ? 'success' : 'error'); setTimeout(() => { - setTitle(`Authorization response is: ${result ? 'valid' : 'invalid'}`); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: `Authorization response is: ${result ? 'valid' : 'invalid'}`, + type: result ? 'success' : 'error', + loading: false, + }); + }, 200); }; useEffect(() => { diff --git a/packages/dapp/src/components/CheckMetaMaskCompatibility/index.tsx b/packages/dapp/src/components/CheckMetaMaskCompatibility/index.tsx new file mode 100644 index 000000000..9f9c09aad --- /dev/null +++ b/packages/dapp/src/components/CheckMetaMaskCompatibility/index.tsx @@ -0,0 +1,214 @@ +'use client'; + +import { useEffect } from 'react'; +import { enableMasca } from '@blockchain-lab-um/masca-connector'; +import { isError } from '@blockchain-lab-um/utils'; +import detectEthereumProvider from '@metamask/detect-provider'; +import { shallow } from 'zustand/shallow'; + +import { useGeneralStore, useMascaStore } from '@/stores'; + +const snapId = + process.env.USE_LOCAL === 'true' + ? 'local:http://localhost:8081' + : 'npm:@blockchain-lab-um/masca'; + +const CheckMetaMaskCompatibility = () => { + const { changeHasMetaMask, changeIsFlask } = useGeneralStore( + (state) => ({ + changeHasMetaMask: state.changeHasMetaMask, + changeIsFlask: state.changeIsFlask, + }), + shallow + ); + + const { + hasMM, + hasFlask, + address, + isConnected, + isConnecting, + changeAddress, + changeIsConnected, + changeIsConnecting, + changeChainId, + } = useGeneralStore( + (state) => ({ + hasMM: state.hasMetaMask, + hasFlask: state.isFlask, + address: state.address, + isConnected: state.isConnected, + isConnecting: state.isConnecting, + changeAddress: state.changeAddress, + changeIsConnected: state.changeIsConnected, + changeIsConnecting: state.changeIsConnecting, + changeChainId: state.changeChainId, + }), + shallow + ); + + const { + changeMascaApi, + changeDID, + changeAvailableMethods, + changeCurrMethod, + changeAvailableVCStores, + } = useMascaStore( + (state) => ({ + changeMascaApi: state.changeMascaApi, + changeDID: state.changeCurrDID, + changeAvailableMethods: state.changeAvailableMethods, + changeCurrMethod: state.changeCurrDIDMethod, + changeAvailableVCStores: state.changeAvailableVCStores, + }), + shallow + ); + + const connectHandler = async () => { + if (window.ethereum) { + const result: unknown = await window.ethereum.request({ + method: 'eth_requestAccounts', + }); + + const chain = (await window.ethereum.request({ + method: 'eth_chainId', + })) as string; + + // Set the chainId + changeChainId(chain); + + // Set the address + changeAddress((result as string[])[0]); + } + }; + + const checkMetaMaskCompatibility = async () => { + try { + const provider = await detectEthereumProvider({ mustBeMetaMask: true }); + + if (!provider) { + changeHasMetaMask(false); + changeIsFlask(false); + return; + } + } catch (error) { + changeHasMetaMask(false); + changeIsFlask(false); + } + + changeHasMetaMask(true); + + const mmVersion = (await window.ethereum.request({ + method: 'web3_clientVersion', + })) as string; + + if (!mmVersion.includes('flask')) { + changeIsFlask(false); + return; + } + + changeIsFlask(true); + }; + + const enableMascaHandler = async () => { + const enableResult = await enableMasca(address, { + snapId, + version: '^0.2.1', + }); + if (isError(enableResult)) { + // FIXME: This error is shown as [Object object] + throw new Error(enableResult.error); + } + const api = enableResult.data.getMascaApi(); + + changeMascaApi(api); + + // Set currently connected address + const setAccountRes = await api.setCurrentAccount({ + currentAccount: address, + }); + + if (isError(setAccountRes)) { + console.log("Couldn't set current account"); + throw new Error(setAccountRes.error); + } + + const did = await api.getDID(); + if (isError(did)) { + console.log("Couldn't get DID"); + throw new Error(did.error); + } + + const availableMethods = await api.getAvailableMethods(); + if (isError(availableMethods)) { + console.log("Couldn't get available methods"); + throw new Error(availableMethods.error); + } + + const method = await api.getSelectedMethod(); + if (isError(method)) { + console.log("Couldn't get selected method"); + throw new Error(method.error); + } + + const accountSettings = await api.getAccountSettings(); + if (isError(accountSettings)) { + console.log("Couldn't get account settings"); + throw new Error(accountSettings.error); + } + + changeDID(did.data); + changeAvailableMethods(availableMethods.data); + changeCurrMethod(method.data); + changeAvailableVCStores(accountSettings.data.ssi.vcStore); + changeIsConnected(true); + changeIsConnecting(false); + }; + + useEffect(() => { + if (hasMM && hasFlask && window.ethereum) { + window.ethereum.on('accountsChanged', (...accounts) => { + changeAddress((accounts[0] as string[])[0]); + }); + window.ethereum.on('chainChanged', (...chain) => { + changeChainId(chain[0] as string); + }); + } + + return () => { + if (window.ethereum) { + window.ethereum.removeAllListeners('accountsChanged'); + window.ethereum.removeAllListeners('chainChanged'); + } + }; + }, [hasMM, hasFlask]); + + useEffect(() => { + if (!hasMM || !hasFlask || !address) return; + console.log('Address changed to', address); + enableMascaHandler().catch((err) => { + console.error(err); + changeIsConnecting(false); + changeAddress(''); + }); + }, [hasMM, hasFlask, address]); + + useEffect(() => { + checkMetaMaskCompatibility().catch((error) => { + console.error(error); + }); + }, []); + + useEffect(() => { + if (isConnected || !isConnecting) return; + console.log('Connecting to MetaMask...'); + connectHandler().catch((err) => { + console.error(err); + changeIsConnecting(false); + }); + }, [isConnected, isConnecting]); + + return null; +}; + +export default CheckMetaMaskCompatibility; diff --git a/packages/dapp/src/components/ConnectedProvider/index.tsx b/packages/dapp/src/components/ConnectedProvider/index.tsx index b8b15e41a..3fa93e092 100644 --- a/packages/dapp/src/components/ConnectedProvider/index.tsx +++ b/packages/dapp/src/components/ConnectedProvider/index.tsx @@ -1,8 +1,7 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { useTranslations } from 'next-intl'; -import { shallow } from 'zustand/shallow'; import { useGeneralStore } from '@/stores'; @@ -12,51 +11,7 @@ type ConnectedProviderProps = { const ConnectedProvider = ({ children }: ConnectedProviderProps) => { const t = useTranslations('Gateway'); - const { - isConnected, - isConnecting, - changeAddress, - changeIsConnecting, - changeChainId, - } = useGeneralStore( - (state) => ({ - isConnected: state.isConnected, - isConnecting: state.isConnecting, - address: state.address, - changeAddress: state.changeAddress, - changeIsConnected: state.changeIsConnected, - changeIsConnecting: state.changeIsConnecting, - changeChainId: state.changeChainId, - }), - shallow - ); - - const connectHandler = async () => { - if (window.ethereum) { - const result: unknown = await window.ethereum.request({ - method: 'eth_requestAccounts', - }); - - const chain = (await window.ethereum.request({ - method: 'eth_chainId', - })) as string; - - // Set the chainId - changeChainId(chain); - - // Set the address - changeAddress((result as string[])[0]); - } - }; - - useEffect(() => { - if (isConnected || !isConnecting) return; - console.log('Connecting to MetaMask...'); - connectHandler().catch((err) => { - console.error(err); - changeIsConnecting(false); - }); - }, [isConnected, isConnecting]); + const isConnected = useGeneralStore((state) => state.isConnected); if (isConnected) { return <>{children}; diff --git a/packages/dapp/src/components/Controlbar/Controlbar.tsx b/packages/dapp/src/components/Controlbar/Controlbar.tsx index 46f52590f..d7de1ce07 100644 --- a/packages/dapp/src/components/Controlbar/Controlbar.tsx +++ b/packages/dapp/src/components/Controlbar/Controlbar.tsx @@ -32,16 +32,7 @@ const Controlbar = () => { }), shallow ); - const { setTitle, setLoading, setToastOpen, setType } = useToastStore( - (state) => ({ - setTitle: state.setTitle, - setText: state.setText, - setLoading: state.setLoading, - setToastOpen: state.setOpen, - setType: state.setType, - }), - shallow - ); + const { api, changeVcs } = useMascaStore( (state) => ({ api: state.mascaApi, @@ -54,22 +45,43 @@ const Controlbar = () => { if (!api) return; setSpinner(true); + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Querying credentials', + type: 'normal', + loading: true, + }); + }, 200); + const res = await api.queryVCs(); + useToastStore.setState({ + open: false, + }); if (isError(res)) { console.log(res.error); - setSpinner(false); - setToastOpen(false); setTimeout(() => { - setTitle('Failed to query credentials'); - setType('error'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Failed to query credentials', + type: 'error', + loading: false, + }); + }, 200); return; } + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Successfully queried credentials', + type: 'success', + loading: false, + }); + }, 200); + changeLastFetch(Date.now()); changeVcs(res.data); setSpinner(false); diff --git a/packages/dapp/src/components/CreateConnectionCard/CreateConnectionModal.tsx b/packages/dapp/src/components/CreateConnectionCard/CreateConnectionModal.tsx new file mode 100644 index 000000000..0f008307a --- /dev/null +++ b/packages/dapp/src/components/CreateConnectionCard/CreateConnectionModal.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { Fragment, useEffect, useState } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; +import { QRCodeSVG } from 'qrcode.react'; +import { shallow } from 'zustand/shallow'; + +import { useSessionStore } from '@/stores'; + +type CreateConnectionModalProps = { + open: boolean; + setOpen: (open: boolean) => void; +}; + +const CreateConnectionModal = ({ + open, + setOpen, +}: CreateConnectionModalProps) => { + const [connectionData, setConnectionData] = useState(null); + const { changeSessionId, changeKey, changeExp } = useSessionStore( + (state) => ({ + changeSessionId: state.changeSessionId, + changeKey: state.changeKey, + changeExp: state.changeExp, + }), + shallow + ); + + const createSession = async (): Promise => { + // Create session ID + const sessionId = crypto.randomUUID(); + + const key = await crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + // Export key data + const keyData = await crypto.subtle.exportKey('jwk', key); + + // Set expiration date (1 hour from now) + const exp = Date.now() + 1000 * 60 * 60; + + // Set global session data + changeSessionId(sessionId); + changeKey(key); + changeExp(exp); + + // Create session + return JSON.stringify({ + sessionId, + keyData, + exp, + }); + }; + + useEffect(() => { + if (open) { + createSession() + .then((data) => setConnectionData(data)) + .catch(console.error); + } + }, [open]); + + return ( + + setOpen(false)}> + +
+ + +
+
+ + + + Connection QR Code + +
+
+ {connectionData && ( + + )} +
+
+
+
+
+
+
+
+ ); +}; + +export default CreateConnectionModal; diff --git a/packages/dapp/src/components/CreateConnectionCard/index.tsx b/packages/dapp/src/components/CreateConnectionCard/index.tsx new file mode 100644 index 000000000..3902cf875 --- /dev/null +++ b/packages/dapp/src/components/CreateConnectionCard/index.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useState } from 'react'; + +import Button from '@/components/Button'; +import { useGeneralStore } from '@/stores'; +import CreateConnectionModal from './CreateConnectionModal'; + +const CreateConnectionCard = () => { + const isConnected = useGeneralStore((state) => state.isConnected); + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( + <> +
+
+

+ You can use this page to create a connection with your mobile device + and use it as a QR code scanner. +

+

+ The Create Connection button will show a QR code that contains your + session ID and a secret encryption key. This allows for end to end + encryption of your data between your mobile device and your browser. +

+

+ To create a connection on your browser you need to be connected to + MetaMask and Masca, but your mobile device does not need to be. +

+
+
+ +
+
+ + + ); +}; + +export default CreateConnectionCard; diff --git a/packages/dapp/src/components/CredentialOfferModal/index.tsx b/packages/dapp/src/components/CredentialOfferModal/index.tsx new file mode 100644 index 000000000..c649a0607 --- /dev/null +++ b/packages/dapp/src/components/CredentialOfferModal/index.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { Fragment, useEffect } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; + +import { useMascaStore } from '@/stores'; + +type CredentialOfferModalProps = { + open: boolean; + setOpen: (open: boolean) => void; + credentialOffer: string; +}; + +const CredentialOfferModal = ({ + open, + setOpen, + credentialOffer, +}: CredentialOfferModalProps) => { + const api = useMascaStore((state) => state.mascaApi); + + useEffect(() => { + if (!api) return; + + api + .handleOIDCCredentialOffer({ + credentialOfferURI: credentialOffer, + }) + .catch((error) => console.error); + }, [api, credentialOffer]); + + return ( + + setOpen(false)}> + +
+ + +
+
+ + + + {credentialOffer} + + + +
+
+
+
+ ); +}; + +export default CredentialOfferModal; diff --git a/packages/dapp/src/components/DeleteModal/index.tsx b/packages/dapp/src/components/DeleteModal/index.tsx index 1186e8165..5462901a4 100644 --- a/packages/dapp/src/components/DeleteModal/index.tsx +++ b/packages/dapp/src/components/DeleteModal/index.tsx @@ -28,26 +28,19 @@ function DeleteModal({ open, setOpen, vc, store }: DeleteModalProps) { }), shallow ); - const { setTitle, setLoading, setToastOpen, setType } = useToastStore( - (state) => ({ - setTitle: state.setTitle, - setText: state.setText, - setLoading: state.setLoading, - setToastOpen: state.setOpen, - setType: state.setType, - }), - shallow - ); const deleteVC = async () => { if (!api) return; setOpen(false); if (vc) { - setLoading(true); - setType('normal'); - setTitle('Deleting Credential'); - setToastOpen(true); - setOpen(false); + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Deleting Credential', + type: 'normal', + loading: true, + }); + }, 200); let deleteReqOptions; @@ -62,42 +55,42 @@ function DeleteModal({ open, setOpen, vc, store }: DeleteModalProps) { } const res = await api.deleteVC(vc.metadata.id, deleteReqOptions); + useToastStore.setState({ + open: false, + }); if (isError(res)) { - setToastOpen(false); setTimeout(() => { - setTitle('Failed to delete credential'); - setType('error'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Failed to delete credential', + type: 'error', + loading: false, + }); + }, 200); console.log(res.error); return; } - // TODO - Delete VC from local state instead of calling queryVCs. + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Credential deleted', + type: 'success', + loading: false, + }); + }, 200); + + // TODO - Delete VC from local state instead of calling queryVCs. const vcs = await api.queryVCs(); if (isError(vcs)) { - setToastOpen(false); - setTimeout(() => { - setType('error'); - setTitle('Failed to load credentials'); - setLoading(false); - setToastOpen(true); - }, 100); + console.log(vcs.error); return; } changeLastFetch(Date.now()); if (vcs.data) { - setToastOpen(false); - setTimeout(() => { - setType('success'); - setTitle('Credential deleted'); - setLoading(false); - setToastOpen(true); - }, 100); changeVcs(vcs.data); } } diff --git a/packages/dapp/src/components/GetCredential/index.tsx b/packages/dapp/src/components/GetCredential/index.tsx index 48595adc9..b0f49a1b3 100644 --- a/packages/dapp/src/components/GetCredential/index.tsx +++ b/packages/dapp/src/components/GetCredential/index.tsx @@ -4,7 +4,6 @@ import { useState } from 'react'; import { CredentialOffer } from '@blockchain-lab-um/oidc-types'; import { isError } from '@blockchain-lab-um/utils'; import qs from 'qs'; -import { shallow } from 'zustand/shallow'; import Button from '@/components/Button'; import InputField from '@/components/InputField'; @@ -12,17 +11,6 @@ import { useMascaStore, useToastStore } from '@/stores'; const GetCredential = () => { const api = useMascaStore((state) => state.mascaApi); - const { setTitle, setText, setLoading, setToastOpen, setType } = - useToastStore( - (state) => ({ - setTitle: state.setTitle, - setText: state.setText, - setLoading: state.setLoading, - setToastOpen: state.setOpen, - setType: state.setType, - }), - shallow - ); const [credentialOfferURI, setCredentialOfferURI] = useState( null @@ -59,14 +47,16 @@ const GetCredential = () => { setParsedCredentialOfferURI(parsedOffer); } catch (e) { - setToastOpen(false); - setType('error'); - setTimeout(() => { - setTitle('Failed to parse credential offer URI'); - setLoading(false); - setToastOpen(true); - }, 100); console.log(e); + + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Failed to parse credential offer URI', + type: 'error', + loading: false, + }); + }, 200); } }; @@ -89,13 +79,14 @@ const GetCredential = () => { setCredentialOfferURI(await credentialOfferRequestResponse.text()); } catch (e) { - setToastOpen(false); - setType('error'); setTimeout(() => { - setTitle('Failed to get DEMO credential offer URI'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Failed to get DEMO credential offer URI', + type: 'error', + loading: false, + }); + }, 200); console.log(e); } }; @@ -103,35 +94,41 @@ const GetCredential = () => { const handleCredentialOfferRequest = async () => { if (!api || !credentialOfferURI || !parsedCredentialOfferURI) return; - setLoading(true); - setType('normal'); - setTitle('Handling credential offer'); - setToastOpen(true); - + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Handling credential offer', + type: 'normal', + loading: true, + }); + }, 200); const handleCredentialOfferResponse = await api.handleOIDCCredentialOffer({ credentialOfferURI, }); if (isError(handleCredentialOfferResponse)) { - setToastOpen(false); - setType('error'); setTimeout(() => { - setTitle('Error while handling credential offer'); - setText(handleCredentialOfferResponse.error); - - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Error while handling credential offer', + type: 'error', + loading: false, + }); + }, 200); console.log(handleCredentialOfferResponse.error); return; } const credential = handleCredentialOfferResponse.data; - setLoading(true); - setType('normal'); - setTitle('Saving Credential'); - setToastOpen(true); + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Saving credential', + type: 'normal', + loading: true, + }); + }, 200); // Save credential // TODO: Convert credential to VC first @@ -140,13 +137,15 @@ const GetCredential = () => { }); if (isError(saveCredentialResponse)) { - setToastOpen(false); - setType('error'); setTimeout(() => { - setTitle('Error while saving credential'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Error while saving credential', + type: 'error', + loading: false, + }); + }, 200); + console.log(saveCredentialResponse.error); } }; diff --git a/packages/dapp/src/components/MetaMaskProvider/index.tsx b/packages/dapp/src/components/MetaMaskProvider/index.tsx index 6fd6ba8a9..b97275b49 100644 --- a/packages/dapp/src/components/MetaMaskProvider/index.tsx +++ b/packages/dapp/src/components/MetaMaskProvider/index.tsx @@ -1,19 +1,11 @@ 'use client'; -import React, { useEffect } from 'react'; -import { enableMasca } from '@blockchain-lab-um/masca-connector'; -import { isError } from '@blockchain-lab-um/utils'; -import detectEthereumProvider from '@metamask/detect-provider'; +import React from 'react'; import { useTranslations } from 'next-intl'; import { shallow } from 'zustand/shallow'; import Button from '@/components/Button'; -import { useGeneralStore, useMascaStore } from '@/stores'; - -const snapId = - process.env.USE_LOCAL === 'true' - ? 'local:http://localhost:8081' - : 'npm:@blockchain-lab-um/masca'; +import { useGeneralStore } from '@/stores'; type MetaMaskProviderProps = { children: React.ReactNode; @@ -21,166 +13,15 @@ type MetaMaskProviderProps = { const MetaMaskProvider = ({ children }: MetaMaskProviderProps) => { const t = useTranslations('Gateway'); - const { - hasMM, - hasFlask, - address, - changeHasMetaMask, - changeIsFlask, - changeAddress, - changeIsConnected, - changeIsConnecting, - changeChainId, - } = useGeneralStore( + + const { hasMM, hasFlask } = useGeneralStore( (state) => ({ hasMM: state.hasMetaMask, hasFlask: state.isFlask, - address: state.address, - isConnected: state.isConnected, - changeHasMetaMask: state.changeHasMetaMask, - changeIsFlask: state.changeIsFlask, - changeAddress: state.changeAddress, - changeIsConnected: state.changeIsConnected, - changeIsConnecting: state.changeIsConnecting, - changeChainId: state.changeChainId, - }), - shallow - ); - - const { - changeMascaApi, - changeDID, - changeAvailableMethods, - changeCurrMethod, - changeAvailableVCStores, - } = useMascaStore( - (state) => ({ - changeMascaApi: state.changeMascaApi, - changeDID: state.changeCurrDID, - changeAvailableMethods: state.changeAvailableMethods, - changeCurrMethod: state.changeCurrDIDMethod, - changeAvailableVCStores: state.changeAvailableVCStores, }), shallow ); - const checkMetaMaskCompatibility = async () => { - try { - const provider = await detectEthereumProvider({ mustBeMetaMask: true }); - - if (!provider) { - changeHasMetaMask(false); - changeIsFlask(false); - return; - } - } catch (error) { - changeHasMetaMask(false); - changeIsFlask(false); - } - - changeHasMetaMask(true); - - const mmVersion = (await window.ethereum.request({ - method: 'web3_clientVersion', - })) as string; - - if (!mmVersion.includes('flask')) { - changeIsFlask(false); - return; - } - - changeIsFlask(true); - }; - - const enableMascaHandler = async () => { - const enableResult = await enableMasca(address, { - snapId, - version: '^0.2.1', - }); - if (isError(enableResult)) { - // FIXME: This error is shown as [Object object] - throw new Error(enableResult.error); - } - const api = enableResult.data.getMascaApi(); - - changeMascaApi(api); - - // Set currently connected address - const setAccountRes = await api.setCurrentAccount({ - currentAccount: address, - }); - - if (isError(setAccountRes)) { - console.log("Couldn't set current account"); - throw new Error(setAccountRes.error); - } - - const did = await api.getDID(); - if (isError(did)) { - console.log("Couldn't get DID"); - throw new Error(did.error); - } - - const availableMethods = await api.getAvailableMethods(); - if (isError(availableMethods)) { - console.log("Couldn't get available methods"); - throw new Error(availableMethods.error); - } - - const method = await api.getSelectedMethod(); - if (isError(method)) { - console.log("Couldn't get selected method"); - throw new Error(method.error); - } - - const accountSettings = await api.getAccountSettings(); - if (isError(accountSettings)) { - console.log("Couldn't get account settings"); - throw new Error(accountSettings.error); - } - - changeDID(did.data); - changeAvailableMethods(availableMethods.data); - changeCurrMethod(method.data); - changeAvailableVCStores(accountSettings.data.ssi.vcStore); - changeIsConnected(true); - changeIsConnecting(false); - }; - - useEffect(() => { - checkMetaMaskCompatibility().catch((error) => { - console.error(error); - }); - }, []); - - useEffect(() => { - if (hasMM && hasFlask && window.ethereum) { - window.ethereum.on('accountsChanged', (...accounts) => { - changeAddress((accounts[0] as string[])[0]); - }); - window.ethereum.on('chainChanged', (...chain) => { - changeChainId(chain[0] as string); - }); - } - - return () => { - if (window.ethereum) { - window.ethereum.removeAllListeners('accountsChanged'); - window.ethereum.removeAllListeners('chainChanged'); - } - }; - }, [hasMM, hasFlask]); - - useEffect(() => { - if (!hasMM || !hasFlask || !address) return; - console.log('Address changed to', address); - enableMascaHandler().catch((err) => { - console.error(err); - changeIsConnecting(false); - changeAddress(''); - }); - }, [hasMM, hasFlask, address]); - if (hasMM && hasFlask) { return <>{children}; } diff --git a/packages/dapp/src/components/MethodDropdownMenu/index.tsx b/packages/dapp/src/components/MethodDropdownMenu/index.tsx index ce06aa0c7..b3d799b3a 100644 --- a/packages/dapp/src/components/MethodDropdownMenu/index.tsx +++ b/packages/dapp/src/components/MethodDropdownMenu/index.tsx @@ -23,35 +23,48 @@ export default function MethodDropdownMenu() { }), shallow ); - const { setTitle, setLoading, setToastOpen, setType } = useToastStore( - (state) => ({ - setTitle: state.setTitle, - setText: state.setText, - setLoading: state.setLoading, - setToastOpen: state.setOpen, - setType: state.setType, - }), - shallow - ); const handleMethodChange = async (method: string) => { if (method !== currMethod) { if (!api) return; + + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Switching did method', + type: 'normal', + loading: true, + }); + }, 200); + const res = await api.switchDIDMethod(method as AvailableMethods); + useToastStore.setState({ + open: false, + }); - // TODO: Show toast with error message - if (!isError(res)) { - changeCurrDIDMethod(method); - changeDID(res.data); - } else { - setToastOpen(false); + if (isError(res)) { setTimeout(() => { - setTitle('Failed to change method'); - setType('error'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Failed to change method', + type: 'error', + loading: false, + }); + }, 200); + return; } + + setTimeout(() => { + useToastStore.setState({ + open: true, + title: `Successfully changed method to ${method}`, + type: 'success', + loading: false, + }); + }, 200); + + changeCurrDIDMethod(method); + changeDID(res.data); } }; diff --git a/packages/dapp/src/components/ModifyDSModal/index.tsx b/packages/dapp/src/components/ModifyDSModal/index.tsx index 2a518770e..7a56626e0 100644 --- a/packages/dapp/src/components/ModifyDSModal/index.tsx +++ b/packages/dapp/src/components/ModifyDSModal/index.tsx @@ -22,15 +22,7 @@ interface ModifyDSModalProps { function ModifyDSModal({ open, setOpen, vc }: ModifyDSModalProps) { const t = useTranslations('ModifyDataStoreModal'); const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const { setTitle, setLoading, setToastOpen, setType } = useToastStore( - (state) => ({ - setTitle: state.setTitle, - setLoading: state.setLoading, - setToastOpen: state.setOpen, - setType: state.setType, - }), - shallow - ); + const [deleteModalStore, setDeleteModalStore] = useState< AvailableVCStores | undefined >(undefined); @@ -81,33 +73,37 @@ function ModifyDSModal({ open, setOpen, vc }: ModifyDSModalProps) { return; } - setLoading(true); - setType('normal'); - setTitle('Saving Credential'); - setToastOpen(true); + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Saving credential', + type: 'normal', + loading: true, + }); + }, 200); const res = await api.saveVC(vc.data, { store }); if (isError(res)) { - setToastOpen(false); - setType('error'); setTimeout(() => { - setTitle('Error while saving credential'); - setLoading(false); - setToastOpen(true); - }, 100); - console.log(res.error); + useToastStore.setState({ + open: true, + title: 'Error while saving credential', + type: 'error', + loading: false, + }); + }, 200); return; } - setToastOpen(false); - setTimeout(() => { - setType('success'); - setTitle('Credential saved'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Credential saved', + type: 'success', + loading: false, + }); + }, 200); const vcs = await api.queryVCs(); diff --git a/packages/dapp/src/components/QRCodeScanner/index.tsx b/packages/dapp/src/components/QRCodeScanner/index.tsx new file mode 100644 index 000000000..026b2fbe3 --- /dev/null +++ b/packages/dapp/src/components/QRCodeScanner/index.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { useEffect } from 'react'; +import { Html5Qrcode, Html5QrcodeCameraScanConfig } from 'html5-qrcode'; + +import { useToastStore } from '@/stores'; + +type QRCodeScannerProps = { + onScanSuccess: (decodedText: string, _: any) => void; + scanner: Html5Qrcode | null; + setScanner: (scanner: Html5Qrcode | null) => void; + setOpen: (open: boolean) => void; +}; + +const QRCodeScanner = ({ + onScanSuccess, + scanner, + setScanner, + setOpen, +}: QRCodeScannerProps) => { + const onScanFailure = (_: any) => {}; + + useEffect(() => { + try { + setScanner( + new Html5Qrcode('reader', { + verbose: false, + }) + ); + } catch (error) { + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Error initializing QR Code Scanner', + type: 'error', + loading: false, + }); + }, 200); + setOpen(false); + } + + return () => { + if (scanner && scanner.isScanning) { + scanner.stop().catch(() => {}); + } + }; + }, []); + + useEffect(() => { + if (!scanner) return; + + const config: Html5QrcodeCameraScanConfig = { + fps: 30, + qrbox: { width: 200, height: 200 }, + }; + + scanner + .start( + { facingMode: 'environment' }, + config, + onScanSuccess, + onScanFailure + ) + .catch(() => { + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Error starting QR Code Scanner', + type: 'error', + loading: false, + }); + }, 200); + setOpen(false); + }); + }, [scanner]); + + return ( +
+
+
+ ); +}; + +export default QRCodeScanner; diff --git a/packages/dapp/src/components/QRCodeSessionProvider/index.tsx b/packages/dapp/src/components/QRCodeSessionProvider/index.tsx new file mode 100644 index 000000000..8ad828c3e --- /dev/null +++ b/packages/dapp/src/components/QRCodeSessionProvider/index.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { hexToUint8Array } from '@blockchain-lab-um/utils'; +import useSWR from 'swr'; +import { shallow } from 'zustand/shallow'; + +import { useGeneralStore, useSessionStore, useToastStore } from '@/stores'; +import CredentialOfferModal from '../CredentialOfferModal'; + +const fetcher = (url: string) => fetch(url).then((r) => r.json()); + +const QRCodeSessionProvider = () => { + const [decryptedData, setDecryptedData] = useState(null); + const [isCredentialOfferModalOpen, setIsCredentialOfferModalOpen] = + useState(false); + + const { sessionId, key, exp } = useSessionStore( + (state) => ({ + sessionId: state.sessionId, + key: state.key, + exp: state.exp, + }), + shallow + ); + + const isConnected = useGeneralStore((state) => state.isConnected); + + // Conditionally fetch session data + const { data } = useSWR( + () => + sessionId && isConnected ? `/api/qr-code-session/${sessionId}` : null, + fetcher, + { + // Refresh every 10 seconds + errorRetryInterval: 10000, + errorRetryCount: 100, + refreshInterval: 10000, + } + ); + + useEffect(() => { + if (!exp) return; + + if (exp && exp < Date.now()) { + useSessionStore.setState({ sessionId: null, key: null, exp: null }); + } + }, [exp]); + + useEffect(() => { + if (!key || !data) return; + + const { data: encryptedData, iv: encodedIV } = data; + + if (data.error_descrition || !encryptedData || !encodedIV) return; + + // Data to uint8array + const iv = hexToUint8Array(encodedIV); + + // Decrypt data + const decryptData = async () => { + const decrypted = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv, + }, + key, + hexToUint8Array(encryptedData) + ); + + const decoded = new TextDecoder().decode(decrypted); + + return decoded; + }; + + decryptData() + .then((_data) => { + if ( + !_data.startsWith('openid-credential-offer://') && + !_data.startsWith('openid://') + ) { + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Unsuported QR code data received', + type: 'error', + loading: false, + }); + }, 200); + return; + } + + setDecryptedData(_data); + }) + .catch((e) => console.log(e)); + }, [data, key]); + + useEffect(() => { + setIsCredentialOfferModalOpen(!!decryptedData); + }, [decryptedData]); + + useEffect(() => { + if (!isCredentialOfferModalOpen) { + setDecryptedData(null); + } + }, [isCredentialOfferModalOpen]); + + if (!decryptedData) { + return null; + } + + return ( + + ); +}; + +export default QRCodeSessionProvider; diff --git a/packages/dapp/src/components/ScanConnectionCard/ScanQRCodeModal.tsx b/packages/dapp/src/components/ScanConnectionCard/ScanQRCodeModal.tsx new file mode 100644 index 000000000..00d0f7778 --- /dev/null +++ b/packages/dapp/src/components/ScanConnectionCard/ScanQRCodeModal.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { Fragment, useEffect, useState } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; +import { Html5Qrcode } from 'html5-qrcode'; + +import QRCodeScanner from '../QRCodeScanner'; + +type ScanQRCodeModalProps = { + title: string; + open: boolean; + setOpen: (open: boolean) => void; + onScanSuccess: (decodedText: string, _: any) => void; +}; + +const ScanQRCodeModal = ({ + title, + open, + setOpen, + onScanSuccess, +}: ScanQRCodeModalProps) => { + const [scanner, setScanner] = useState(null); + + useEffect(() => { + if (!open && scanner && scanner.isScanning) { + scanner.stop().catch((error) => console.error(error)); + } + + if (open && scanner) { + setScanner(null); + } + }, [open]); + + return ( + + setOpen(false)}> + +
+ + +
+
+ + + + {title} + +
+ +
+
+
+
+
+
+
+ ); +}; +export default ScanQRCodeModal; diff --git a/packages/dapp/src/components/ScanConnectionCard/index.tsx b/packages/dapp/src/components/ScanConnectionCard/index.tsx new file mode 100644 index 000000000..95e960211 --- /dev/null +++ b/packages/dapp/src/components/ScanConnectionCard/index.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { useState } from 'react'; +import { uint8ArrayToHex } from '@blockchain-lab-um/utils'; +import { shallow } from 'zustand/shallow'; + +import { useSessionStore, useToastStore } from '@/stores'; +import Button from '../Button'; +import ScanQRCodesModal from './ScanQRCodeModal'; + +const ScanConnectionCard = () => { + const { sessionId, key, exp } = useSessionStore( + (state) => ({ + sessionId: state.sessionId, + key: state.key, + exp: state.exp, + }), + shallow + ); + + const [isConnectionModalOpen, setIsConnectionModalOpen] = useState(false); + const [isQRCodeModalOpen, setIsQRCodeModalOpen] = useState(false); + + const onScanSuccessConnectionQRCode = async (decodedText: string, _: any) => { + setIsConnectionModalOpen(false); + + try { + const data = JSON.parse(decodedText); + + if (!data.sessionId || !data.keyData || !data.exp) throw new Error(); + + const decryptionKey = await crypto.subtle.importKey( + 'jwk', + data.keyData, + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'] + ); + + useSessionStore.setState({ + sessionId: data.sessionId, + key: decryptionKey, + exp: data.exp, + }); + + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Connection created', + type: 'success', + loading: false, + }); + }, 200); + } catch (e) { + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Invalid QR code', + type: 'error', + loading: false, + }); + }, 200); + } + }; + + const onScanSuccessQRCode = async (decodedText: string, _: any) => { + if (!sessionId || !key || !exp) return; + setIsQRCodeModalOpen(false); + + try { + if ( + !decodedText.startsWith('openid-credential-offer://') && + !decodedText.startsWith('openid://') + ) { + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Unsupported QR code', + type: 'error', + loading: false, + }); + }, 200); + + return; + } + + // Encrypt data + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const encodedText = new TextEncoder().encode(decodedText); + + const encryptedData = new Uint8Array( + await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + }, + key, + encodedText + ) + ); + + // Send data + const response = await fetch(`/api/qr-code-session/${sessionId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: uint8ArrayToHex(encryptedData), + iv: uint8ArrayToHex(iv), + }), + }); + + if (!response.ok) throw new Error(); + + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'QR data sent', + type: 'success', + loading: false, + }); + }, 200); + } catch (e) { + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'An unexpected error occurred', + type: 'error', + loading: false, + }); + }, 200); + } + }; + + return ( + <> +
+
+

+ Use this on your mobile device to first scan the connection QR code + to astablish a connection with your browser. After that you can scan + any other QR code that you want to pass to your browser to be + handled by the Masca Dapp. +

+
+
+ + +
+
+ + + + ); +}; + +export default ScanConnectionCard; diff --git a/packages/dapp/src/components/SettingsCard/index.tsx b/packages/dapp/src/components/SettingsCard/index.tsx index 862b37dcd..56bd11518 100644 --- a/packages/dapp/src/components/SettingsCard/index.tsx +++ b/packages/dapp/src/components/SettingsCard/index.tsx @@ -9,16 +9,6 @@ import { useMascaStore, useToastStore } from '@/stores'; const SettingsCard = () => { const t = useTranslations('Settings'); - const { setTitle, setLoading, setToastOpen, setType } = useToastStore( - (state) => ({ - setTitle: state.setTitle, - setText: state.setText, - setLoading: state.setLoading, - setToastOpen: state.setOpen, - setType: state.setType, - }), - shallow - ); const { api, availableVCStores, changeAvailableVCStores } = useMascaStore( (state) => ({ api: state.mascaApi, @@ -46,14 +36,25 @@ const SettingsCard = () => { const res = await api.setVCStore(store, value); await snapGetAvailableVCStores(); if (isError(res)) { - setToastOpen(false); setTimeout(() => { - setTitle('Failed to toggle ceramic'); - setType('error'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'Failed to toggle ceramic', + type: 'error', + loading: false, + }); + }, 200); + return; } + + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Successfully toggled ceramic', + type: 'success', + loading: false, + }); + }, 200); }; const handleCeramicToggle = async (enabled: boolean) => { diff --git a/packages/dapp/src/components/ToastWrapper/index.tsx b/packages/dapp/src/components/ToastWrapper/index.tsx index a5a582c7d..e7a2b39aa 100644 --- a/packages/dapp/src/components/ToastWrapper/index.tsx +++ b/packages/dapp/src/components/ToastWrapper/index.tsx @@ -18,10 +18,9 @@ const ToastWrapper = () => { useEffect(() => () => clearTimeout(timerRef.current), []); - const { open, setOpen, loading, type, title } = useToastStore( + const { open, loading, type, title } = useToastStore( (state) => ({ open: state.open, - setOpen: state.setOpen, loading: state.loading, title: state.title, type: state.type, @@ -67,7 +66,7 @@ const ToastWrapper = () => { 'data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:transition-[transform_200ms_ease-out]' )} open={open} - onOpenChange={setOpen} + onOpenChange={(_open) => useToastStore.setState({ open: _open })} >
diff --git a/packages/dapp/src/components/VCTable/index.tsx b/packages/dapp/src/components/VCTable/index.tsx index c83f4dd65..de4b0a68b 100644 --- a/packages/dapp/src/components/VCTable/index.tsx +++ b/packages/dapp/src/components/VCTable/index.tsx @@ -75,16 +75,6 @@ const Table = () => { }), shallow ); - const { setTitle, setToastLoading, setToastOpen, setType } = useToastStore( - (state) => ({ - setTitle: state.setTitle, - setText: state.setText, - setToastLoading: state.setLoading, - setToastOpen: state.setOpen, - setType: state.setType, - }), - shallow - ); const columnHelper = createColumnHelper(); const [deleteModalOpen, setDeleteModalOpen] = useState(false); @@ -299,15 +289,16 @@ const Table = () => { const loadVCs = async () => { if (!api) return; const loadedVCs = await api.queryVCs(); + if (isError(loadedVCs)) { - setToastOpen(false); setTimeout(() => { - setTitle('Failed to load credential'); - setType('error'); - setToastLoading(false); - setToastOpen(true); - }, 100); - console.log('Failed to load VCs'); + useToastStore.setState({ + open: true, + title: 'Failed to load credentials', + type: 'error', + loading: false, + }); + }, 200); return; } @@ -316,14 +307,25 @@ const Table = () => { if (loadedVCs.data) { changeVcs(loadedVCs.data); if (loadedVCs.data.length === 0) { - setToastOpen(false); setTimeout(() => { - setTitle('No credentials found'); - setType('error'); - setLoading(false); - setToastOpen(true); - }, 100); + useToastStore.setState({ + open: true, + title: 'No credentials found', + type: 'info', + loading: false, + }); + }, 200); + return; } + + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Credentials loaded', + type: 'success', + loading: false, + }); + }, 200); } }; diff --git a/packages/dapp/src/messages/en.json b/packages/dapp/src/messages/en.json index 1a8932615..9c8ccbf63 100644 --- a/packages/dapp/src/messages/en.json +++ b/packages/dapp/src/messages/en.json @@ -15,7 +15,8 @@ "dashboard": "Dashboard", "settings": "Settings", "get-credential": "Get Credential", - "authorization-request": "Authorization Request" + "authorization-request": "Authorization Request", + "qr-code-session": "QR Code Session" }, "AppNavbar": { "connect": "Connect Wallet", @@ -25,6 +26,7 @@ "settings": "Settings", "get-credential": "Get Credential", "authorization-request": "Authorization Request", + "qr-code-session": "QR Code Session", "other": "Other" }, "dropdown": { diff --git a/packages/dapp/src/stores/index.ts b/packages/dapp/src/stores/index.ts index cc52ac96d..cbac77b12 100644 --- a/packages/dapp/src/stores/index.ts +++ b/packages/dapp/src/stores/index.ts @@ -2,3 +2,4 @@ export * from './generalStore'; export * from './snapStore'; export * from './tableStore'; export * from './toastStore'; +export * from './sessionStore'; diff --git a/packages/dapp/src/stores/sessionStore.ts b/packages/dapp/src/stores/sessionStore.ts new file mode 100644 index 000000000..58be91cc8 --- /dev/null +++ b/packages/dapp/src/stores/sessionStore.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand'; + +interface SessionStore { + sessionId: string | null; + key: CryptoKey | null; + exp: number | null; + + changeSessionId: (sessionId: string) => void; + changeKey: (key: CryptoKey) => void; + changeExp: (exp: number) => void; +} + +export const sessionStoreInitialState = { + sessionId: null, + key: null, + exp: null, +}; + +export const useSessionStore = create()((set) => ({ + ...sessionStoreInitialState, + + changeSessionId: (sessionId: string) => set({ sessionId }), + changeKey: (key: CryptoKey) => set({ key }), + changeExp: (exp: number) => set({ exp }), +})); diff --git a/packages/dapp/src/stores/toastStore.ts b/packages/dapp/src/stores/toastStore.ts index c7c208079..054c4ce8c 100644 --- a/packages/dapp/src/stores/toastStore.ts +++ b/packages/dapp/src/stores/toastStore.ts @@ -1,17 +1,13 @@ import { create } from 'zustand'; +type ToastType = 'info' | 'success' | 'error' | 'normal'; + interface ToastStore { open: boolean; loading: boolean; text: string; title: string; - type: string; - - setOpen: (open: boolean) => void; - setLoading: (loading: boolean) => void; - setText: (text: string) => void; - setTitle: (title: string) => void; - setType: (type: string) => void; + type: ToastType; } export const toastStoreInitialState = { @@ -19,15 +15,9 @@ export const toastStoreInitialState = { loading: false, text: '', title: '', - type: 'info', + type: 'info' as ToastType, }; -export const useToastStore = create()((set) => ({ +export const useToastStore = create()(() => ({ ...toastStoreInitialState, - - setType: (type) => set({ type }), - setOpen: (open) => set({ open }), - setLoading: (loading) => set({ loading }), - setText: (text) => set({ text }), - setTitle: (title) => set({ title }), })); diff --git a/packages/dapp/src/styles/globals.css b/packages/dapp/src/styles/globals.css index 3f619ca58..630720d2a 100644 --- a/packages/dapp/src/styles/globals.css +++ b/packages/dapp/src/styles/globals.css @@ -10,7 +10,9 @@ html { overflow: hidden; - max-height: 100vh; + position: fixed; + height: 100%; + width: 100%; } html, diff --git a/packages/dapp/src/utils/prisma.ts b/packages/dapp/src/utils/prisma.ts new file mode 100644 index 000000000..e2b960527 --- /dev/null +++ b/packages/dapp/src/utils/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from '@prisma/client'; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = globalForPrisma.prisma ?? new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5d20faf3..9a6a747ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1162,6 +1162,9 @@ importers: '@metamask/providers': specifier: ^10.2.0 version: 10.2.0 + '@prisma/client': + specifier: ^4.16.1 + version: 4.16.1(prisma@4.16.1) '@radix-ui/react-toast': specifier: ^1.1.4 version: 1.1.4(@types/react-dom@18.2.6)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) @@ -1186,6 +1189,9 @@ importers: ethers: specifier: ^6.5.1 version: 6.5.1 + html5-qrcode: + specifier: ^2.3.8 + version: 2.3.8 lint-staged: specifier: ^13.2.2 version: 13.2.2 @@ -1210,6 +1216,9 @@ importers: prop-types: specifier: ^15.8.1 version: 15.8.1 + qrcode.react: + specifier: ^3.1.0 + version: 3.1.0(react@18.2.0) qs: specifier: ^6.11.2 version: 6.11.2 @@ -1219,6 +1228,9 @@ importers: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + swr: + specifier: ^2.2.0 + version: 2.2.0(react@18.2.0) tailwind-scrollbar: specifier: ^3.0.4 version: 3.0.4(tailwindcss@3.3.2) @@ -1280,6 +1292,9 @@ importers: prettier-plugin-packagejson: specifier: ^2.4.3 version: 2.4.3(prettier@2.8.8) + prisma: + specifier: ^4.16.1 + version: 4.16.1 rimraf: specifier: ^5.0.1 version: 5.0.1 @@ -9800,6 +9815,28 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@prisma/client@4.16.1(prisma@4.16.1): + resolution: {integrity: sha512-CoDHu7Bt+NuDo40ijoeHP79EHtECsPBTy3yte5Yo3op8TqXt/kV0OT5OrsWewKvQGKFMHhYQ+ePed3zzjYdGAw==} + engines: {node: '>=14.17'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c + prisma: 4.16.1 + dev: false + + /@prisma/engines-version@4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c: + resolution: {integrity: sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA==} + dev: false + + /@prisma/engines@4.16.1: + resolution: {integrity: sha512-gpZG0kGGxfemgvK/LghHdBIz+crHkZjzszja94xp4oytpsXrgt/Ice82MvPsWMleVIniKuARrowtsIsim0PFJQ==} + requiresBuild: true + /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: @@ -18642,6 +18679,10 @@ packages: tapable: 2.2.1 webpack: 5.82.1(@swc/core@1.3.52)(webpack-cli@5.1.1) + /html5-qrcode@2.3.8: + resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==} + dev: false + /htmlescape@1.1.1: resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==} engines: {node: '>=0.10'} @@ -23741,6 +23782,14 @@ packages: react: 18.2.0 dev: false + /prisma@4.16.1: + resolution: {integrity: sha512-C2Xm7yxHxjFjjscBEW4tmoraPHH/Vyu/A0XABdbaFtoiOZARsxvOM7rwc2iZ0qVxbh0bGBGBWZUSXO/52/nHBQ==} + engines: {node: '>=14.17'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 4.16.1 + /prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} @@ -23884,6 +23933,14 @@ packages: hasBin: true dev: true + /qrcode.react@3.1.0(react@18.2.0): + resolution: {integrity: sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -26313,6 +26370,15 @@ packages: swagger-ui-dist: 4.19.0 dev: true + /swr@2.2.0(react@18.2.0): + resolution: {integrity: sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} From da0d7779c760fc757a1ac2e81af5369c4d2f0484 Mon Sep 17 00:00:00 2001 From: Martin Domajnko Date: Wed, 28 Jun 2023 13:26:10 +0200 Subject: [PATCH 2/3] chore: remove setToastOpen --- packages/dapp/src/components/Controlbar/Controlbar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dapp/src/components/Controlbar/Controlbar.tsx b/packages/dapp/src/components/Controlbar/Controlbar.tsx index d7de1ce07..45e4122da 100644 --- a/packages/dapp/src/components/Controlbar/Controlbar.tsx +++ b/packages/dapp/src/components/Controlbar/Controlbar.tsx @@ -100,7 +100,6 @@ const Controlbar = () => { console.log(normalizationError); setSpinner(false); - setToastOpen(false); setTimeout(() => { useToastStore.setState({ open: true, From 3d1d45624c3a05ab50cee2209455fac984e9e70a Mon Sep 17 00:00:00 2001 From: Martin Domajnko Date: Wed, 28 Jun 2023 13:46:03 +0200 Subject: [PATCH 3/3] chore: remove manifest.json --- packages/dapp/src/app/[locale]/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dapp/src/app/[locale]/layout.tsx b/packages/dapp/src/app/[locale]/layout.tsx index b40855df3..f6b9b5d4c 100644 --- a/packages/dapp/src/app/[locale]/layout.tsx +++ b/packages/dapp/src/app/[locale]/layout.tsx @@ -79,7 +79,7 @@ export const metadata: Metadata = { verification: { google: 'snsvYv9eAKOZ7FrIjpUSnUtqgoFiSXQWROVrStPBc8I', }, - manifest: '/manifest.json', + manifest: null, }; export function generateStaticParams() {