From 482df70c6e1a8d35c9ce0e1ed57556f4ffb91222 Mon Sep 17 00:00:00 2001 From: elvira-paulin Date: Wed, 31 Jul 2024 20:51:40 +0200 Subject: [PATCH 01/16] Manage Profile functionality WIP --- next.config.mjs | 6 + package.json | 4 + src/app/api/identity/get/route.ts | 26 + src/app/api/identity/validByAddress/route.ts | 26 + src/components/Passphrase/index.tsx | 146 +-- src/components/Wallet/menu.tsx | 1054 +++++++++-------- .../manageProfile/common/CancelButton.tsx | 17 + src/signals/wallet/client.tsx | 305 ++--- src/signals/wallet/index.tsx | 24 +- src/utils/getImageFromGP.ts | 6 + src/utils/hashColor.ts | 7 + 11 files changed, 899 insertions(+), 722 deletions(-) create mode 100644 src/app/api/identity/get/route.ts create mode 100644 src/app/api/identity/validByAddress/route.ts create mode 100644 src/components/modal/manageProfile/common/CancelButton.tsx create mode 100644 src/utils/getImageFromGP.ts create mode 100644 src/utils/hashColor.ts diff --git a/next.config.mjs b/next.config.mjs index 66b6cbdf..0afdead5 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -33,6 +33,12 @@ const nextConfig = { port: "", pathname: "/**", }, + { + protocol: "https", + hostname: "ordinals.gorillapool.io", + port: "", + pathname: "/**", + }, ], }, webpack: (config, { isServer }) => { diff --git a/package.json b/package.json index a8a55bc0..92f8368f 100644 --- a/package.json +++ b/package.json @@ -31,17 +31,20 @@ "@types/three": "^0.164.1", "@types/uuid": "^9.0.8", "ai": "^3.1.15", + "bitcoin-bap": "^1.3.1", "blurhash": "^2.0.5", "bsv-wasm": "^2.1.1", "bsv-wasm-web": "^2.1.1", "bun-copy-plugin": "^0.2.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "color-hash": "^2.0.2", "copy-webpack-plugin": "^12.0.2", "critters": "^0.0.20", "daisyui": "^4.11.1", "dompurify": "^3.0.9", "framer-motion": "^11.1.7", + "generate-avatar": "^1.4.10", "isomorphic-dompurify": "^2.4.0", "jdenticon": "^3.2.0", "js-1sat-ord": "^0.1.25", @@ -75,6 +78,7 @@ }, "devDependencies": { "@types/bun": "^1.1.1", + "@types/color-hash": "^2.0.0", "@types/node": "^20", "@types/randombytes": "^2.0.3", "@types/react": "^18.3", diff --git a/src/app/api/identity/get/route.ts b/src/app/api/identity/get/route.ts new file mode 100644 index 00000000..2f482c20 --- /dev/null +++ b/src/app/api/identity/get/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + const { idKey } = await req.json(); + try { + const response = await fetch( + "https://go-bap-indexer-production.up.railway.app/v1/identity/get", + + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ idKey: idKey }), + } + ); + const data = await response.json(); + return NextResponse.json(data); + } catch (err) { + return new NextResponse(null, { + status: 500, + statusText: "Internal Server Error", + }); + } +} diff --git a/src/app/api/identity/validByAddress/route.ts b/src/app/api/identity/validByAddress/route.ts new file mode 100644 index 00000000..a4e7253f --- /dev/null +++ b/src/app/api/identity/validByAddress/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + const { address } = await req.json(); + try { + const response = await fetch( + "https://go-bap-indexer-production.up.railway.app/v1/identity/validByAddress", + + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ address: address }), + } + ); + const data = await response.json(); + return NextResponse.json(data); + } catch (err) { + return new NextResponse(null, { + status: 500, + statusText: "Internal Server Error", + }); + } +} diff --git a/src/components/Passphrase/index.tsx b/src/components/Passphrase/index.tsx index 84d8259f..c9152bda 100644 --- a/src/components/Passphrase/index.tsx +++ b/src/components/Passphrase/index.tsx @@ -202,67 +202,67 @@ const EnterPassphrase: React.FC = ({ }; return ( -
+ {!hasDownloadedKeys.value &&
- Enter a password to{" "} - {showEnterPassphrase.value === EncryptDecrypt.Decrypt - ? "decrypt" - : "encrypt"}{" "} - your saved keys. + Enter a password to{" "} + {showEnterPassphrase.value === EncryptDecrypt.Decrypt + ? "decrypt" + : "encrypt"}{" "} + your saved keys.
} {!hasDownloadedKeys.value &&
- {mode === EncryptDecrypt.Encrypt && ( -
- -
- )} + toast.success( + "Copied phrase. Careful now!", + toastProps + ); + }} + > + + +
+ )} {!hasDownloadedKeys.value && } } - {showEnterPassphrase.value === EncryptDecrypt.Encrypt && ( -
-
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} -
- {" "} - Generate a strong passphrase -
-
-
- )} -
- - {showEnterPassphrase.value === EncryptDecrypt.Encrypt - ? "You still need to keep your 12 word seed phrase." - : "Your password unlocks your wallet each time you visit."} -
+ {showEnterPassphrase.value === EncryptDecrypt.Encrypt && ( +
+
+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
+ {" "} + Generate a strong passphrase +
+
+
+ )} +
+ + {showEnterPassphrase.value === EncryptDecrypt.Encrypt + ? "You still need to keep your 12 word seed phrase." + : "Your password unlocks your wallet each time you visit."} +
-
- {!migrating.value && !download && ( +
+ {/* {!migrating.value && !download && ( - )} + )} */} - + - {hasDownloadedKeys.value && ( - - )} -
- + {hasDownloadedKeys.value && ( + + )} +
+ ); }; export default EnterPassphrase; diff --git a/src/components/Wallet/menu.tsx b/src/components/Wallet/menu.tsx index 7e9dcc79..15c9dd88 100644 --- a/src/components/Wallet/menu.tsx +++ b/src/components/Wallet/menu.tsx @@ -1,28 +1,37 @@ "use client"; -import { FetchStatus, MARKET_API_HOST, OLD_ORD_PK_KEY, OLD_PAY_PK_KEY } from "@/constants"; import { - bsv20Balances, - bsvWasmReady, - chainInfo, - encryptedBackup, - exchangeRate, - hasUnprotectedKeys, - indexers, - ordPk, - payPk, - pendingTxs, - showDepositModal, - showUnlockWalletButton, - showUnlockWalletModal, - usdRate, - utxos, + FetchStatus, + MARKET_API_HOST, + OLD_ORD_PK_KEY, + OLD_PAY_PK_KEY, +} from "@/constants"; +import { + bsv20Balances, + bsvWasmReady, + chainInfo, + encryptedBackup, + exchangeRate, + hasUnprotectedKeys, + indexers, + ordPk, + payPk, + pendingTxs, + showDepositModal, + showUnlockWalletButton, + showUnlockWalletModal, + usdRate, + utxos, } from "@/signals/wallet"; +import { selectedBapIdentity } from "@/signals/bapIdentity"; +import {loadIdentityFromSessionStorage} from "@/signals/bapIdentity/client"; +import { generateFromString } from "generate-avatar"; import { fundingAddress, ordAddress } from "@/signals/wallet/address"; import { - loadKeysFromBackupFiles, - loadKeysFromSessionStorage, + loadKeysFromBackupFiles, + loadKeysFromSessionStorage, } from "@/signals/wallet/client"; + import type { BSV20Balance } from "@/types/bsv20"; import type { ChainInfo, IndexerStats } from "@/types/common"; import type { FileEvent } from "@/types/file"; @@ -34,358 +43,411 @@ import { useSignal, useSignals } from "@preact/signals-react/runtime"; import init from "bsv-wasm-web"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import Image from "next/image"; import { useEffect } from "react"; import toast from "react-hot-toast"; import { CgSpinner } from "react-icons/cg"; +import { hashColor } from "@/utils/hashColor"; import { - FaExclamationCircle, - FaFileImport, - FaPlus, - FaUnlock, + FaExclamationCircle, + FaFileImport, + FaPlus, + FaUnlock, } from "react-icons/fa"; -import { FaCopy, FaWallet } from "react-icons/fa6"; +import { FaCopy } from "react-icons/fa6"; +import { MdAccountCircle } from "react-icons/md"; import { toBitcoin, toSatoshi } from "satoshi-bitcoin-ts"; import { useCopyToClipboard } from "usehooks-ts"; +import { getImageFromGP } from "@/utils/getImageFromGP"; import * as http from "../../utils/httpClient"; import DepositModal from "../modal/deposit"; import { EnterPassphraseModal } from "../modal/enterPassphrase"; import ImportWalletModal from "../modal/importWallet"; +import ManageProfileModal from "../modal/manageProfile"; import ProtectKeysModal from "../modal/protectKeys"; import WithdrawalModal from "../modal/withdrawal"; let initAttempted = false; const WalletMenu: React.FC = () => { - useSignals(); - const router = useRouter(); - - const fetchRateStatus = useSignal(FetchStatus.Idle); - const showWithdrawalModal = useSignal(false); - const showImportWalletModal = useSignal(false); - const showProtectKeysModal = useSignal(false); - const showDropdown = useSignal(false); - - const [eb] = useLocalStorage("encryptedBackup"); - - const [value, copy] = useCopyToClipboard(); - const ordAddressHover = useSignal(false); - - const mouseEnterOrdAddress = () => { - ordAddressHover.value = true; - }; - - const mouseLeaveOrdAddress = () => { - console.log("mouseLeaveOrdAddress"); - ordAddressHover.value = false; - }; - - // useEffect needed so that we can use localStorage - useEffect(() => { - if (bsvWasmReady.value && payPk.value && ordPk.value) { - const localTxsStr = localStorage.getItem("1satpt"); - const localTxs = localTxsStr ? JSON.parse(localTxsStr) : null; - if (localTxs) { - pendingTxs.value = localTxs as PendingTransaction[]; - } - } - }, [bsvWasmReady.value, ordPk.value, payPk.value]); - - useEffect(() => { - loadKeysFromSessionStorage(); - - if (eb && !encryptedBackup.value) { - // TODO: This is triggering upon signout! Fix this - // Reproduce: Sign in, encrypt backup - // close tab and reopen - // unlock wallet - // sign out - this will fire forcing the unlock button to show again! - console.log("showing unlock wallet button on purpose") - showUnlockWalletButton.value = true; - } - }, [encryptedBackup.value, eb]); - - useEffect(() => { - if ( - !!localStorage.getItem(OLD_PAY_PK_KEY) && - !!localStorage.getItem(OLD_ORD_PK_KEY) - ) { - hasUnprotectedKeys.value = true; - } - }, []); - - const balance = computed(() => { - if (!utxos.value) { - return 0; - } - return utxos.value.reduce((acc, utxo) => acc + utxo.satoshis, 0); - }); - - useEffect(() => { - const address = ordAddress.value; - const fire = async () => { - bsv20Balances.value = []; - try { - const { promise } = http.customFetch( - `${MARKET_API_HOST}/user/${address}/balance` - ); - const u = await promise; - bsv20Balances.value = u.sort((a, b) => { - return b.all.confirmed + b.all.pending > - a.all.confirmed + a.all.pending - ? 1 - : -1; - }); - } catch (e) { - console.log(e); - } - }; - - // console.log({ bsvWasmReady, address, bsv20Balances }) - if (bsvWasmReady.value && address && !bsv20Balances.value) { - fire(); - } - }, [bsvWasmReady.value, ordAddress.value, bsv20Balances.value]); - - useEffect(() => { - const fire = async () => { - fetchRateStatus.value = FetchStatus.Loading; - const statusUrl = - `${MARKET_API_HOST}/status`; - const { promise: promiseStatus } = http.customFetch<{ - exchangeRate: number; - chainInfo: ChainInfo; - indexers: IndexerStats; - }>(statusUrl); - const { - chainInfo: info, - exchangeRate: er, - indexers: indx, - } = await promiseStatus; - fetchRateStatus.value = FetchStatus.Success; - // console.log({ info, exchangeRate, indexers }); - chainInfo.value = info; - usdRate.value = toSatoshi(1) / er; - exchangeRate.value = er; - indexers.value = indx; - } - if (fetchRateStatus.value === FetchStatus.Idle) { - fire(); - } - }, [fetchRateStatus]) - - useEffect(() => { - const fire = async (a: string) => { - utxos.value = []; - utxos.value = await getUtxos(a); - }; - - if (bsvWasmReady.value && fundingAddress && !utxos.value) { - const address = fundingAddress.value; - if (address) { - fire(address); - } - } - }, [bsvWasmReady.value, fundingAddress.value, utxos.value]); - - effect(() => { - const fire = async () => { - await init(); - bsvWasmReady.value = true; - }; - if (!initAttempted && bsvWasmReady.value === false) { - initAttempted = true; - fire(); - } - }); - - // const importKeys = (e: SyntheticEvent) => { - // e.preventDefault(); - // const el = document.getElementById("backupFile"); - // el?.click(); - // return; - // }; - - const handleFileChange = async (e: FileEvent) => { - console.log("handleFileChange called", e.target.files[0]); - if (payPk.value || ordPk.value) { - const c = confirm( - "Are you sure you want to import this wallet? Doing so will replace your existing keys so be sure to back them up first." - ); - if (!c) { - return; - } - } - await loadKeysFromBackupFiles(e.target.files[0]); - showDropdown.value = false; - router?.push("/wallet"); - }; - - const handleUnlockWallet = () => { - showUnlockWalletModal.value = true; - showDropdown.value = false; - }; - - const handleImportWallet = () => { - showImportWalletModal.value = true; - showDropdown.value = false; - }; - - const handleProtectKeys = () => { - showProtectKeysModal.value = true; - showDropdown.value = false; - }; - - return ( -
    - {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} -
    { - showDropdown.value = true; - }} - > -
    - -
    -
    - {showDropdown.value && ( - // biome-ignore lint/a11y/useKeyWithClickEvents: -
      - tabIndex={0} - onClick={() => { - showDropdown.value = false; - }} - className="dropdown-content menu shadow border-yellow-200/25 bg-base-100 rounded-box w-64 border" - > - {payPk.value && ordPk.value && ( -
      -
      -
      - Balance -
      -
      - {balance.value === undefined ? ( - "" // user has no wallet yet - ) : usdRate.value > 0 ? ( - `$${( - balance.value / usdRate.value - ).toFixed(2)}` - ) : ( - - )} - USD -
      -
      - {toBitcoin(balance.value)}{" "} - BSV -
      -
      -
      - - -
      - -
      1Sat Wallet
      -
        - {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} -
      • { - showDropdown.value = false; - }} - > - - Ordinals - -
      • - {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} -
      • { - showDropdown.value = false; - }} - > - BSV20 -
      • - {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} -
      • { - showDropdown.value = false; - }} - > - BSV21 -
      • -
      • - -
      • -
      - -
      Keys
      -
        -
      • - -
      • -
      • - -
      • - {/*
      • + useSignals(); + const router = useRouter(); + + const fetchRateStatus = useSignal(FetchStatus.Idle); + const showWithdrawalModal = useSignal(false); + const showImportWalletModal = useSignal(false); + const showManageProfileModal = useSignal(false); + const showProtectKeysModal = useSignal(false); + const showDropdown = useSignal(false); + + const [eb] = useLocalStorage("encryptedBackup"); + + const [value, copy] = useCopyToClipboard(); + const ordAddressHover = useSignal(false); + + const mouseEnterOrdAddress = () => { + ordAddressHover.value = true; + }; + + const mouseLeaveOrdAddress = () => { + console.log("mouseLeaveOrdAddress"); + ordAddressHover.value = false; + }; + + // useEffect needed so that we can use localStorage + useEffect(() => { + if (bsvWasmReady.value && payPk.value && ordPk.value) { + const localTxsStr = localStorage.getItem("1satpt"); + const localTxs = localTxsStr ? JSON.parse(localTxsStr) : null; + if (localTxs) { + pendingTxs.value = localTxs as PendingTransaction[]; + } + } + }, [bsvWasmReady.value, ordPk.value, payPk.value]); + + useEffect(() => { + loadKeysFromSessionStorage(); + + if (eb && !encryptedBackup.value) { + // TODO: This is triggering upon signout! Fix this + // Reproduce: Sign in, encrypt backup + // close tab and reopen + // unlock wallet + // sign out - this will fire forcing the unlock button to show again! + console.log("showing unlock wallet button on purpose"); + showUnlockWalletButton.value = true; + } + }, [encryptedBackup.value, eb]); + + useEffect(() => { + if ( + !!localStorage.getItem(OLD_PAY_PK_KEY) && + !!localStorage.getItem(OLD_ORD_PK_KEY) + ) { + hasUnprotectedKeys.value = true; + } + }, []); + + const balance = computed(() => { + if (!utxos.value) { + return 0; + } + return utxos.value.reduce((acc, utxo) => acc + utxo.satoshis, 0); + }); + + useEffect(() => { + const address = ordAddress.value; + const fire = async () => { + bsv20Balances.value = []; + try { + const { promise } = http.customFetch( + `${MARKET_API_HOST}/user/${address}/balance` + ); + const u = await promise; + bsv20Balances.value = u.sort((a, b) => { + return b.all.confirmed + b.all.pending > + a.all.confirmed + a.all.pending + ? 1 + : -1; + }); + } catch (e) { + console.log(e); + } + }; + + // console.log({ bsvWasmReady, address, bsv20Balances }) + if (bsvWasmReady.value && address && !bsv20Balances.value) { + fire(); + } + }, [bsvWasmReady.value, ordAddress.value, bsv20Balances.value]); + + useEffect(() => { + const fire = async () => { + fetchRateStatus.value = FetchStatus.Loading; + const statusUrl = `${MARKET_API_HOST}/status`; + const { promise: promiseStatus } = http.customFetch<{ + exchangeRate: number; + chainInfo: ChainInfo; + indexers: IndexerStats; + }>(statusUrl); + const { + chainInfo: info, + exchangeRate: er, + indexers: indx, + } = await promiseStatus; + fetchRateStatus.value = FetchStatus.Success; + // console.log({ info, exchangeRate, indexers }); + chainInfo.value = info; + usdRate.value = toSatoshi(1) / er; + exchangeRate.value = er; + indexers.value = indx; + }; + if (fetchRateStatus.value === FetchStatus.Idle) { + fire(); + } + }, [fetchRateStatus]); + + useEffect(() => { + const fire = async (a: string) => { + utxos.value = []; + utxos.value = await getUtxos(a); + }; + + if (bsvWasmReady.value && fundingAddress && !utxos.value) { + const address = fundingAddress.value; + if (address) { + fire(address); + } + } + }, [bsvWasmReady.value, fundingAddress.value, utxos.value]); + + effect(() => { + const fire = async () => { + await init(); + bsvWasmReady.value = true; + }; + if (!initAttempted && bsvWasmReady.value === false) { + initAttempted = true; + fire(); + } + }); + + // const importKeys = (e: SyntheticEvent) => { + // e.preventDefault(); + // const el = document.getElementById("backupFile"); + // el?.click(); + // return; + // }; + + const handleFileChange = async (e: FileEvent) => { + console.log("handleFileChange called", e.target.files[0]); + if (payPk.value || ordPk.value) { + const c = confirm( + "Are you sure you want to import this wallet? Doing so will replace your existing keys so be sure to back them up first." + ); + if (!c) { + return; + } + } + await loadKeysFromBackupFiles(e.target.files[0]); + showDropdown.value = false; + router?.push("/wallet"); + }; + + const handleUnlockWallet = () => { + showUnlockWalletModal.value = true; + showDropdown.value = false; + }; + + const handleImportWallet = () => { + showImportWalletModal.value = true; + showDropdown.value = false; + }; + + const handleProtectKeys = () => { + showProtectKeysModal.value = true; + showDropdown.value = false; + }; + + const handleManageProfile = () => { + showManageProfileModal.value = true; + showDropdown.value = false; + }; + + const identityColor = + selectedBapIdentity.value && hashColor(selectedBapIdentity.value.idKey); + + const identityImage = + selectedBapIdentity.value?.identity?.image && + getImageFromGP(selectedBapIdentity.value?.identity?.image); + + return ( +
          + {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
          { + showDropdown.value = true; + }} + > +
          + {identityImage ? ( + Profile image + ) : ( + + )} +
          +
          + {showDropdown.value && ( + // biome-ignore lint/a11y/useKeyWithClickEvents: +
            + tabIndex={0} + onClick={() => { + showDropdown.value = false; + }} + className="dropdown-content menu shadow border-yellow-200/25 bg-base-100 rounded-box w-64 border" + > + {payPk.value && ordPk.value && ( +
            +
            +
            + Balance +
            +
            + {balance.value === undefined ? ( + "" // user has no wallet yet + ) : usdRate.value > 0 ? ( + `$${( + balance.value / usdRate.value + ).toFixed(2)}` + ) : ( + + )} + USD +
            +
            + {toBitcoin(balance.value)}{" "} + BSV +
            +
            +
            + + +
            + +
            Profile
            +
              +
            • + +
            • +
            + +
            1Sat Wallet
            +
              + {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
            • { + showDropdown.value = false; + }} + > + + Ordinals + +
            • + {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
            • { + showDropdown.value = false; + }} + > + BSV20 +
            • + {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
            • { + showDropdown.value = false; + }} + > + BSV21 +
            • +
            • + +
            • +
            + +
            Keys
            +
              +
            • + +
            • +
            • + +
            • + {/*
            • */} - {/*
            • + {/*
            • Swap Keys
            • */} -
            • - Sign Out -
            • -
            -
            - )} - {hasUnprotectedKeys.value && ( -
          • - -
          • - )} - {!payPk.value && !ordPk.value && ( - <> - {showUnlockWalletButton.value && ( -
              - {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} -
            • -
              - Unlock Wallet - -
              -
            • -
            - )} -
              -
            • - - Create New Wallet - - -
            • -
            -
              -
            • - -
            • -
            - - )} -
          - )} - {showDepositModal.value && ( - { - showDepositModal.value = false; - }} - /> - )} - {showWithdrawalModal.value && ( - { - showWithdrawalModal.value = false; - }} - /> - )} - { - showUnlockWalletModal.value = false; - }} - onUnlock={() => { - showUnlockWalletModal.value = false; - }} - /> - - { - showImportWalletModal.value = false; - }} - /> - - { - showProtectKeysModal.value = false; - }} - /> - - -
        - ); +
      • + Sign Out +
      • +
      +
      + )} + {hasUnprotectedKeys.value && ( +
    • + +
    • + )} + {!payPk.value && !ordPk.value && ( + <> + {showUnlockWalletButton.value && ( +
        + {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
      • +
        + Unlock Wallet + +
        +
      • +
      + )} +
        +
      • + + Create New Wallet + + +
      • +
      +
        +
      • + +
      • +
      + + )} +
    + )} + {showDepositModal.value && ( + { + showDepositModal.value = false; + }} + /> + )} + {showWithdrawalModal.value && ( + { + showWithdrawalModal.value = false; + }} + /> + )} + { + showUnlockWalletModal.value = false; + }} + onUnlock={() => { + showUnlockWalletModal.value = false; + }} + /> + { + showManageProfileModal.value = false; + }} + /> + + { + showImportWalletModal.value = false; + }} + /> + + { + showProtectKeysModal.value = false; + }} + /> + + +
+ ); }; export default WalletMenu; export const exportKeysViaFragment = () => { - // redirect to https://1sat.market/wallet/import#import= - const fk = localStorage.getItem("1satfk"); - const ok = localStorage.getItem("1satok"); - let data = "" - if (!fk || !ok) { - if (!payPk.value || !ordPk.value) { - toast.error("No keys to export. Encrypt your keys first."); - } - data = JSON.stringify({ payPk: payPk.value, ordPk: ordPk.value }); - } else { - data = JSON.stringify({ payPk: JSON.parse(fk), ordPk: JSON.parse(ok) }); - } - const b64 = btoa(data); - const base = "http://localhost:3000" // "https://1sat.market" - // window.location.href = `${base}/wallet/import#import=${b64}`; -} + // redirect to https://1sat.market/wallet/import#import= + const fk = localStorage.getItem("1satfk"); + const ok = localStorage.getItem("1satok"); + let data = ""; + if (!fk || !ok) { + if (!payPk.value || !ordPk.value) { + toast.error("No keys to export. Encrypt your keys first."); + } + data = JSON.stringify({ payPk: payPk.value, ordPk: ordPk.value }); + } else { + data = JSON.stringify({ payPk: JSON.parse(fk), ordPk: JSON.parse(ok) }); + } + const b64 = btoa(data); + const base = "http://localhost:3000"; // "https://1sat.market" + // window.location.href = `${base}/wallet/import#import=${b64}`; +}; export const backupKeys = () => { - const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent( - JSON.stringify({ payPk: payPk.value, ordPk: ordPk.value }) - )}`; - - const clicker = document.createElement("a"); - clicker.setAttribute("href", dataStr); - clicker.setAttribute("download", "1sat.json"); - clicker.click(); + const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent( + JSON.stringify({ payPk: payPk.value, ordPk: ordPk.value }) + )}`; + + const clicker = document.createElement("a"); + clicker.setAttribute("href", dataStr); + clicker.setAttribute("download", "1sat.json"); + clicker.click(); }; export const swapKeys = () => { - // swaps paypk with ordpk values - const tempPayPk = payPk.value; - const tempOrdPk = ordPk.value; - if (!tempPayPk || !tempOrdPk) { - return; - } - ordPk.value = tempPayPk; - payPk.value = tempOrdPk; - toast.success("Keys Swapped"); + // swaps paypk with ordpk values + const tempPayPk = payPk.value; + const tempOrdPk = ordPk.value; + if (!tempPayPk || !tempOrdPk) { + return; + } + ordPk.value = tempPayPk; + payPk.value = tempOrdPk; + toast.success("Keys Swapped"); }; diff --git a/src/components/modal/manageProfile/common/CancelButton.tsx b/src/components/modal/manageProfile/common/CancelButton.tsx new file mode 100644 index 00000000..a5944bfc --- /dev/null +++ b/src/components/modal/manageProfile/common/CancelButton.tsx @@ -0,0 +1,17 @@ +type Props = { handleCancel: Function }; + +const CancelButton = ({ handleCancel }: Props) => { + return ( + + ); +}; + +export default CancelButton; diff --git a/src/signals/wallet/client.tsx b/src/signals/wallet/client.tsx index 41202ea3..739c5f9c 100644 --- a/src/signals/wallet/client.tsx +++ b/src/signals/wallet/client.tsx @@ -3,174 +3,189 @@ import { encryptionPrefix } from "@/constants"; import type { PendingTransaction } from "@/types/preview"; import { - CreateWalletStep, - type Keys, - type DecryptedBackupJson, - type EncryptedBackupJson, + CreateWalletStep, + type Keys, + type DecryptedBackupJson, + type EncryptedBackupJson, } from "@/types/wallet"; +import { removeIdentity } from "@/signals/bapIdentity/client"; import { - decryptData, - generateEncryptionKeyFromPassphrase, + decryptData, + generateEncryptionKeyFromPassphrase, } from "@/utils/encryption"; import { - bsv20Balances, - bsv20Utxos, - changeAddressPath, - createWalletStep, - encryptedBackup, - encryptionKey, - mnemonic, - ordAddressPath, - ordPk, - passphrase, - payPk, - pendingTxs, - showUnlockWalletButton, - utxos, + bsv20Balances, + bsv20Utxos, + changeAddressPath, + createWalletStep, + encryptedBackup, + encryptionKey, + mnemonic, + ordAddressPath, + ordPk, + passphrase, + payPk, + pendingTxs, + showUnlockWalletButton, + utxos, } from "."; export const setPendingTxs = (txs: PendingTransaction[]) => { - pendingTxs.value = [...txs]; - localStorage.setItem("1satpt", JSON.stringify(txs)); + pendingTxs.value = [...txs]; + localStorage.setItem("1satpt", JSON.stringify(txs)); }; export const loadKeysFromBackupFiles = (backupFile: File): Promise => { - return new Promise((resolve, reject) => { - if (!backupFile) { - return reject(); - } - const f = new FileReader(); - - f.onload = (e) => { - // get the creation date of the file - //const lastModified = new Date(backupFile.lastModified); - - //const badDateStart = new Date("2021-03-01T00:00:00.000Z"); - //const badDateEnd = new Date("2021-03-05T00:00:00.000Z"); - - // check if the file was modified in the bad range - // if (lastModified > badDateStart && lastModified < badDateEnd) { - // console.log("Invalid backup file based on creation date", lastModified); - // toast.error( - // "Invalid backup file. Please visit our discord here https://discord.gg/D6HKMKUpmV.", - // { - // duration: 999999999, - // }, - // ); - // } - const backup = JSON.parse(e.target?.result as string) as { - payPk: string; - ordPk: string; - }; - // console.log({ backup }); - - setPendingTxs([]); - utxos.value = null; - payPk.value = backup.payPk; - ordPk.value = backup.ordPk; - return resolve(); - }; - f.onerror = (e) => { - console.error(e); - return reject(e); - }; - f.readAsText(backupFile); - }); + return new Promise((resolve, reject) => { + if (!backupFile) { + return reject(); + } + const f = new FileReader(); + + f.onload = (e) => { + // get the creation date of the file + //const lastModified = new Date(backupFile.lastModified); + + //const badDateStart = new Date("2021-03-01T00:00:00.000Z"); + //const badDateEnd = new Date("2021-03-05T00:00:00.000Z"); + + // check if the file was modified in the bad range + // if (lastModified > badDateStart && lastModified < badDateEnd) { + // console.log("Invalid backup file based on creation date", lastModified); + // toast.error( + // "Invalid backup file. Please visit our discord here https://discord.gg/D6HKMKUpmV.", + // { + // duration: 999999999, + // }, + // ); + // } + const backup = JSON.parse(e.target?.result as string) as { + payPk: string; + ordPk: string; + }; + // console.log({ backup }); + + setPendingTxs([]); + utxos.value = null; + payPk.value = backup.payPk; + ordPk.value = backup.ordPk; + return resolve(); + }; + f.onerror = (e) => { + console.error(e); + return reject(e); + }; + f.readAsText(backupFile); + }); }; export const clearKeys = () => { - payPk.value = null; - ordPk.value = null; - pendingTxs.value = null; - utxos.value = null; - bsv20Utxos.value = null; - bsv20Balances.value = null; - localStorage.removeItem("1satfk"); - localStorage.removeItem("1satok"); - localStorage.removeItem("1satpt"); - localStorage.removeItem("encryptedBackup"); - - sessionStorage.removeItem("1satfk"); - sessionStorage.removeItem("1satok"); - - encryptedBackup.value = null; - - encryptionKey.value = null; - passphrase.value = null; - mnemonic.value = null; - - showUnlockWalletButton.value = false; - createWalletStep.value = CreateWalletStep.Create; - console.log("Cleared keys"); + payPk.value = null; + ordPk.value = null; + pendingTxs.value = null; + utxos.value = null; + bsv20Utxos.value = null; + bsv20Balances.value = null; + localStorage.removeItem("1satfk"); + localStorage.removeItem("1satok"); + localStorage.removeItem("1satpt"); + localStorage.removeItem("encryptedBackup"); + + sessionStorage.removeItem("1satfk"); + sessionStorage.removeItem("1satok"); + + encryptedBackup.value = null; + + encryptionKey.value = null; + passphrase.value = null; + mnemonic.value = null; + + showUnlockWalletButton.value = false; + createWalletStep.value = CreateWalletStep.Create; + + removeIdentity(); + console.log("Cleared keys"); }; export const setKeys = (keys: Keys) => { - payPk.value = keys.payPk; - ordPk.value = keys.ordPk; - mnemonic.value = keys.mnemonic ?? null; - changeAddressPath.value = keys.changeAddressPath ?? null; - ordAddressPath.value = keys.ordAddressPath ?? null; - - sessionStorage.setItem("1satfk", keys.payPk); - sessionStorage.setItem("1satok", keys.ordPk); + payPk.value = keys.payPk; + ordPk.value = keys.ordPk; + mnemonic.value = keys.mnemonic ?? null; + changeAddressPath.value = keys.changeAddressPath ?? null; + ordAddressPath.value = keys.ordAddressPath ?? null; + + sessionStorage.setItem("1satfk", keys.payPk); + sessionStorage.setItem("1satok", keys.ordPk); }; export const loadKeysFromSessionStorage = () => { - const payPk = sessionStorage.getItem("1satfk"); - const ordPk = sessionStorage.getItem("1satok"); + const payPk = sessionStorage.getItem("1satfk"); + const ordPk = sessionStorage.getItem("1satok"); + + console.log("KEYS!!! -----------"); + console.log(`${payPk} --- ${ordPk}`); if (payPk && ordPk) { - setKeys({ payPk, ordPk }); + setKeys({ payPk, ordPk }); } }; export const loadKeysFromEncryptedStorage = async (passphrase: string) => { - const encryptedKeysStr = localStorage.getItem("encryptedBackup"); - - if (!encryptedKeysStr) { - return; - } - - const encryptedKeys = JSON.parse(encryptedKeysStr) as EncryptedBackupJson; - - if (!encryptedKeys.pubKey || !encryptedKeys.encryptedBackup) { - throw new Error( - "Load keys error - No public key or encryptedBackup props found in encrypted backup" - ); - } - - const encryptionKey = await generateEncryptionKeyFromPassphrase( - passphrase, - encryptedKeys.pubKey - ); - - if (!encryptionKey) { - throw new Error("No encryption key found. Unable to decrypt."); - } - - const decryptedBackupBin = decryptData( - Buffer.from( - encryptedKeys.encryptedBackup.replace(encryptionPrefix, ""), - "base64" - ), - encryptionKey - ); - - const decryptedBackupStr = - Buffer.from(decryptedBackupBin).toString("utf-8"); - - const decryptedBackup = JSON.parse( - decryptedBackupStr - ) as DecryptedBackupJson; - - if (!decryptedBackup.payPk || !decryptedBackup.ordPk) { - throw new Error( - "Load keys error - No payPk or ordPk props found in decrypted backup" - ); - } - - setKeys({ - payPk: decryptedBackup.payPk, - ordPk: decryptedBackup.ordPk, - }); + const encryptedKeysStr = localStorage.getItem("encryptedBackup"); + + if (!encryptedKeysStr) { + return; + } + + const encryptedKeys = JSON.parse(encryptedKeysStr) as EncryptedBackupJson; + + if (!encryptedKeys.pubKey || !encryptedKeys.encryptedBackup) { + throw new Error( + "Load keys error - No public key or encryptedBackup props found in encrypted backup" + ); + } + + const encryptionKey = await generateEncryptionKeyFromPassphrase( + passphrase, + encryptedKeys.pubKey + ); + + if (!encryptionKey) { + throw new Error("No encryption key found. Unable to decrypt."); + } + + let decryptedBackupBin; + + try { + decryptedBackupBin = decryptData( + Buffer.from( + encryptedKeys.encryptedBackup.replace(encryptionPrefix, ""), + "base64" + ), + encryptionKey + ); + } catch (error) { + console.log(error); + return "FAILED"; + } + + const decryptedBackupStr = + Buffer.from(decryptedBackupBin).toString("utf-8"); + + const decryptedBackup = JSON.parse( + decryptedBackupStr + ) as DecryptedBackupJson; + + if (!decryptedBackup.payPk || !decryptedBackup.ordPk) { + throw new Error( + "Load keys error - No payPk or ordPk props found in decrypted backup" + ); + } + + setKeys({ + payPk: decryptedBackup.payPk, + ordPk: decryptedBackup.ordPk, + }); + + return "SUCCESS"; }; diff --git a/src/signals/wallet/index.tsx b/src/signals/wallet/index.tsx index a71b7dd7..c99bb9e4 100644 --- a/src/signals/wallet/index.tsx +++ b/src/signals/wallet/index.tsx @@ -34,6 +34,8 @@ export const showEnterPassphrase = signal(null); export const encryptedBackup = signal(null); export const encryptionKey = signal(null); export const passphrase = signal(""); +export const passphraseVerified = signal(false); +export const passphraseCheckInProgress = signal(false); export const migrating = signal(false); /** * Unlock Wallet @@ -41,7 +43,6 @@ export const migrating = signal(false); export const showUnlockWalletModal = signal(false); export const showUnlockWalletButton = signal(false); - /** * Wallet keys */ @@ -73,24 +74,25 @@ export const showDepositModal = signal(false); * Import Wallet */ export enum ImportWalletTab { - FromBackupJson = 0, - FromMnemonic = 1, - FromFragment = 2, + FromBackupJson = 0, + FromMnemonic = 1, + FromFragment = 2, } export enum ImportWalletFromBackupJsonStep { - SelectFile = 0, - EnterPassphrase = 1, - Done = 2, + SelectFile = 0, + EnterPassphrase = 1, + Done = 2, } export enum ImportWalletFromMnemonicStep { - EnterMnemonic = 0, - GenerateWallet = 1, - EnterPassphrase = 2, - Done = 3, + EnterMnemonic = 0, + GenerateWallet = 1, + EnterPassphrase = 2, + Done = 3, } + export const importWalletTab = signal(null); export const importWalletFromBackupJsonStep = signal( diff --git a/src/utils/getImageFromGP.ts b/src/utils/getImageFromGP.ts new file mode 100644 index 00000000..e4076659 --- /dev/null +++ b/src/utils/getImageFromGP.ts @@ -0,0 +1,6 @@ +export function getImageFromGP(imageTxId: string) { + return `https://ordinals.gorillapool.io/content/${imageTxId.replace( + "b://", + "" + )}`; +} diff --git a/src/utils/hashColor.ts b/src/utils/hashColor.ts new file mode 100644 index 00000000..2c658ec5 --- /dev/null +++ b/src/utils/hashColor.ts @@ -0,0 +1,7 @@ +import ColorHash from "color-hash"; +const colorHash = new ColorHash(); + +export function hashColor(id: string) { + const color = colorHash.rgb(id); + return color.toString(); +} From e322bc5f488e09473c901f62777b25bbc3f80528 Mon Sep 17 00:00:00 2001 From: elvira-paulin Date: Fri, 2 Aug 2024 12:33:26 +0200 Subject: [PATCH 02/16] Import Profile functionality - finishing touches --- src/components/Passphrase/index.tsx | 200 ++++++++--------- src/components/Wallet/menu.tsx | 22 +- src/components/modal/manageProfile/index.tsx | 51 +++++ .../manageProfile/steps/ChooseIdentity.tsx | 182 ++++++++++++++++ .../modal/manageProfile/steps/Done.tsx | 34 +++ .../steps/EnterSelectedPassphrase.tsx | 203 ++++++++++++++++++ .../manageProfile/steps/SelectProfileJson.tsx | 119 ++++++++++ src/signals/bapIdentity/client.ts | 167 ++++++++++++++ src/signals/bapIdentity/index.ts | 26 +++ src/signals/wallet/client.tsx | 3 - src/types/identity.ts | 62 ++++++ 11 files changed, 956 insertions(+), 113 deletions(-) create mode 100644 src/components/modal/manageProfile/index.tsx create mode 100644 src/components/modal/manageProfile/steps/ChooseIdentity.tsx create mode 100644 src/components/modal/manageProfile/steps/Done.tsx create mode 100644 src/components/modal/manageProfile/steps/EnterSelectedPassphrase.tsx create mode 100644 src/components/modal/manageProfile/steps/SelectProfileJson.tsx create mode 100644 src/signals/bapIdentity/client.ts create mode 100644 src/signals/bapIdentity/index.ts create mode 100644 src/types/identity.ts diff --git a/src/components/Passphrase/index.tsx b/src/components/Passphrase/index.tsx index 8a940990..0d5e5615 100644 --- a/src/components/Passphrase/index.tsx +++ b/src/components/Passphrase/index.tsx @@ -3,14 +3,10 @@ import { encryptionPrefix, toastErrorProps, toastProps } from "@/constants"; import { ImportWalletFromBackupJsonStep, - changeAddressPath, encryptionKey, - identityAddressPath, - identityPk, importWalletFromBackupJsonStep, migrating, mnemonic, - ordAddressPath, ordPk, passphrase, payPk, @@ -22,17 +18,19 @@ import { generateEncryptionKeyFromPassphrase, } from "@/utils/encryption"; import { generatePassphrase } from "@/utils/passphrase"; -import { backupKeys } from "@/utils/wallet"; import { effect, useSignal } from "@preact/signals-react"; import { useSignals } from "@preact/signals-react/runtime"; import { PrivateKey } from "bsv-wasm-web"; import randomBytes from "randombytes"; import { useCallback, useEffect, useRef, useState, type FormEvent } from "react"; +import CopyToClipboard from "react-copy-to-clipboard"; import toast from "react-hot-toast"; import { FiCopy } from "react-icons/fi"; import { RiErrorWarningFill } from "react-icons/ri"; import { TbDice } from "react-icons/tb"; import { useCopyToClipboard } from "usehooks-ts"; +import { hasIdentityBackup } from "@/signals/bapIdentity/index"; +import {loadIdentityFromEncryptedStorage} from "@/signals/bapIdentity/client"; type Props = { mode: EncryptDecrypt; @@ -76,6 +74,21 @@ const EnterPassphrase: React.FC = ({ } }); + const backupKeys = (keys: EncryptedBackupJson) => { + const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent( + JSON.stringify({ + payPk: payPk.value, + ordPk: ordPk.value, + mnemonic: mnemonic.value, + }) + )}`; + + const clicker = document.createElement("a"); + clicker.setAttribute("href", dataStr); + clicker.setAttribute("download", "1sat.json"); + clicker.click(); + }; + const handlePassphraseChange = (e: any) => { e.preventDefault(); e.stopPropagation(); @@ -115,13 +128,8 @@ const EnterPassphrase: React.FC = ({ const encrypted = encryptData( Buffer.from( JSON.stringify({ - mnemonic: mnemonic.value, payPk: payPk.value, ordPk: ordPk.value, - payDerivationPath: changeAddressPath.value, - ordDerivationPath: ordAddressPath.value, - ...(!!identityPk.value && { identityPk: identityPk.value }), - ...(!!identityAddressPath.value && { identityDerivationPath: identityAddressPath.value }), }), "utf-8" ), @@ -139,7 +147,7 @@ const EnterPassphrase: React.FC = ({ }; if (download) { - backupKeys(); + backupKeys(keys); } if (migrating.value) { @@ -163,27 +171,19 @@ const EnterPassphrase: React.FC = ({ toast.error("Failed to encrypt keys", toastErrorProps); } } - }, [ - download, - encryptionKey.value, - hasDownloadedKeys, - migrating.value, - ordPk.value, - passphrase.value, - payPk.value, - mnemonic.value, - changeAddressPath.value, - ordAddressPath.value, - identityPk.value, - identityAddressPath.value - ]); + }, [download, encryptionKey.value, hasDownloadedKeys, migrating.value, ordPk.value, passphrase.value, payPk.value]); const handleClickDecrypt = async () => { if (passphrase.value) { - console.log("decrypt keys w passphrase"); - try { - await loadKeysFromEncryptedStorage(passphrase.value); + const succeeded = await loadKeysFromEncryptedStorage(passphrase.value); + if (succeeded && !!hasIdentityBackup.value) { + const decryptedId = await loadIdentityFromEncryptedStorage(passphrase.value); + + if (!decryptedId) { + toast.error("Failed to decrypt identity, please import it again.", toastErrorProps); + } + } onSubmit(); } catch (e) { console.error(e); @@ -209,98 +209,88 @@ const EnterPassphrase: React.FC = ({ }; return ( -
+ {!hasDownloadedKeys.value &&
- Enter a password to{" "} - {showEnterPassphrase.value === EncryptDecrypt.Decrypt - ? "decrypt" - : "encrypt"}{" "} - your saved keys. + Enter a password to{" "} + {showEnterPassphrase.value === EncryptDecrypt.Decrypt + ? "decrypt" + : "encrypt"}{" "} + your saved keys.
} {!hasDownloadedKeys.value &&
- {mode === EncryptDecrypt.Encrypt && ( -
- -
- )} + toast.success( + "Copied phrase. Careful now!", + toastProps + ); + }} + > + + +
+ )} {!hasDownloadedKeys.value && } } - {showEnterPassphrase.value === EncryptDecrypt.Encrypt && ( -
-
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} -
- {" "} - Generate a strong passphrase -
-
-
- )} -
- - {showEnterPassphrase.value === EncryptDecrypt.Encrypt - ? "You still need to keep your 12 word seed phrase." - : "Your password unlocks your wallet each time you visit."} -
+ {showEnterPassphrase.value === EncryptDecrypt.Encrypt && ( +
+
+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
+ {" "} + Generate a strong passphrase +
+
+
+ )} +
+ + {showEnterPassphrase.value === EncryptDecrypt.Encrypt + ? "You still need to keep your 12 word seed phrase." + : "Your password unlocks your wallet each time you visit."} +
+ +
+ -
- {/* {!migrating.value && !download && ( + {hasDownloadedKeys.value && ( - )} */} - - - - {hasDownloadedKeys.value && ( - - )} -
- + )} +
+ ); }; export default EnterPassphrase; diff --git a/src/components/Wallet/menu.tsx b/src/components/Wallet/menu.tsx index b7f3098b..fea10094 100644 --- a/src/components/Wallet/menu.tsx +++ b/src/components/Wallet/menu.tsx @@ -23,9 +23,8 @@ import { usdRate, utxos, } from "@/signals/wallet"; -import { selectedBapIdentity } from "@/signals/bapIdentity"; +import { activeBapIdentity, hasIdentityBackup } from "@/signals/bapIdentity"; import {loadIdentityFromSessionStorage} from "@/signals/bapIdentity/client"; -import { generateFromString } from "generate-avatar"; import { fundingAddress, ordAddress } from "@/signals/wallet/address"; import { loadKeysFromBackupFiles, @@ -81,6 +80,18 @@ const WalletMenu: React.FC = () => { const showDropdown = useSignal(false); const [eb] = useLocalStorage("encryptedBackup"); + + useEffect(() => { + if (!activeBapIdentity.value) { + loadIdentityFromSessionStorage(); + } + + },[activeBapIdentity.value]); + + useEffect(()=> { + hasIdentityBackup.value = Boolean(localStorage.getItem("encryptedIdentity")); + },[]); + const [value, copy] = useCopyToClipboard(); const ordAddressHover = useSignal(false); @@ -255,11 +266,11 @@ const WalletMenu: React.FC = () => { }; const identityColor = - selectedBapIdentity.value && hashColor(selectedBapIdentity.value.idKey); + activeBapIdentity.value && hashColor(activeBapIdentity.value.idKey); const identityImage = - selectedBapIdentity.value?.identity?.image && - getImageFromGP(selectedBapIdentity.value?.identity?.image); + activeBapIdentity.value?.identity?.image && + getImageFromGP(activeBapIdentity.value?.identity?.image); return (
    @@ -290,6 +301,7 @@ const WalletMenu: React.FC = () => { src={identityImage} alt="Profile image" fill={true} + sizes="(max-width: 768px) 5vw, (max-width: 1200px) 2vw, 1vw" /> ) : ( void; +} + +const ManageProfileModal = ({ open, onClose }: ManageProfileModalProps) => { + useSignals(); + + return ( + +
    +

    Manage Profile

    + + {importProfileFromBackupJsonStep.value === + ImportProfileFromBackupJsonStep.SelectFile && ( + + )} + {importProfileFromBackupJsonStep.value === + ImportProfileFromBackupJsonStep.ChooseIdentity && ( + + )} + {importProfileFromBackupJsonStep.value === + ImportProfileFromBackupJsonStep.EnterPassphrase && ( + + )} + {importProfileFromBackupJsonStep.value === + ImportProfileFromBackupJsonStep.Done && ( + + )} + +
    +
    + ); +}; + +export default ManageProfileModal; diff --git a/src/components/modal/manageProfile/steps/ChooseIdentity.tsx b/src/components/modal/manageProfile/steps/ChooseIdentity.tsx new file mode 100644 index 00000000..a506c13e --- /dev/null +++ b/src/components/modal/manageProfile/steps/ChooseIdentity.tsx @@ -0,0 +1,182 @@ +"use client"; +import Image from "next/image"; +import { MdAccountCircle } from "react-icons/md"; + +import { + bapIdentities, + identitiesLoading, + selectedBapIdentity, + ImportProfileFromBackupJsonStep, + importProfileFromBackupJsonStep, +} from "@/signals/bapIdentity/index"; +import { Identity } from "@/types/identity"; +import { useSignals } from "@preact/signals-react/runtime"; +import CancelButton from "../common/CancelButton"; +import { getImageFromGP } from "@/utils/getImageFromGP"; +import { hashColor } from "@/utils/hashColor"; + +interface Props { + onClose: () => void; +} + +export default function ChooseIdentity({ onClose }: Props) { + useSignals(); + const hiddenFields = ["@context", "@type"]; // not shown to user + + const handleNext = () => { + importProfileFromBackupJsonStep.value = + ImportProfileFromBackupJsonStep.EnterPassphrase; + }; + + const handleCancel = () => { + selectedBapIdentity.value = null; + importProfileFromBackupJsonStep.value = + ImportProfileFromBackupJsonStep.SelectFile; + bapIdentities.value = null; + + onClose(); + }; + + const setActiveBapIdentity = (id: string) => { + const selectedIdentity = bapIdentities?.value?.find( + (identity) => identity.idKey === id + ); + selectedBapIdentity.value = selectedIdentity || null; + }; + + const processValue = (value: any) => { + let processedValue: any = ""; + + if (typeof value === "object") { + Object.keys(value).forEach((k) => { + processedValue += `${k}: ${value[k]}, `; + }); + } else if (typeof value === "string" && value.indexOf("b://") >= 0) { + const imageUrl = getImageFromGP(value); + + processedValue = ( + + Click to view image + + ); + } else { + processedValue = value; + } + return {processedValue}; + }; + + const makeIdentityAvatar = ( + imageUrl: string | undefined, + idKey: string + ) => { + const url = imageUrl && getImageFromGP(imageUrl); + + return ( +
    + {url ? ( + Profile Image + ) : ( + + )} +
    + ); + }; + + const buildTable = (identity: Identity) => { + return ( + + + {Object.entries(identity!) + .filter(([key, value]) => !hiddenFields.includes(key)) + .map(([key, value], index) => ( + 0 ? "" : "bg-neutral-800" + }`} + key={index} + > + + {processValue(value)} + + ))} + +
    + {key} +
    + ); + }; + + return ( + <> + {identitiesLoading.value && ( +
    + +
    + )} + {bapIdentities.value?.length && ( +
    +
    + Select the identity you want to use: +
    + + {bapIdentities.value.map((identity) => ( +
    { + setActiveBapIdentity(identity.idKey); + }} + > + +
    + {makeIdentityAvatar( + identity?.identity?.image, + identity?.idKey + )} +

    + {identity?.identity?.alternateName} +

    +
    +
    + {buildTable(identity?.identity)} +
    +
    + ))} +
    + )} +
    + + +
    + + ); +} diff --git a/src/components/modal/manageProfile/steps/Done.tsx b/src/components/modal/manageProfile/steps/Done.tsx new file mode 100644 index 00000000..ef61ef97 --- /dev/null +++ b/src/components/modal/manageProfile/steps/Done.tsx @@ -0,0 +1,34 @@ +import { + ImportProfileFromBackupJsonStep, + importProfileFromBackupJsonStep, + +} from "@/signals/bapIdentity"; + +interface Props { + onClose: () => void; +} + +export default function Done({ onClose }: Props) { + const onDone = () => { + importProfileFromBackupJsonStep.value = + ImportProfileFromBackupJsonStep.SelectFile; + onClose(); + }; + return ( + <> +

    + Your Identity has been successfully imported. +

    + +
    + +
    + + ); +} diff --git a/src/components/modal/manageProfile/steps/EnterSelectedPassphrase.tsx b/src/components/modal/manageProfile/steps/EnterSelectedPassphrase.tsx new file mode 100644 index 00000000..69e4fb83 --- /dev/null +++ b/src/components/modal/manageProfile/steps/EnterSelectedPassphrase.tsx @@ -0,0 +1,203 @@ +import { useState, useCallback } from "react"; +import { useSignals } from "@preact/signals-react/runtime"; + +import CancelButton from "../common/CancelButton"; +import { + ImportProfileFromBackupJsonStep, + importProfileFromBackupJsonStep, + bapIdentities, + selectedBapIdentity, + bapIdentityRaw, + bapIdEncryptionKey, + hasIdentityBackup, + activeBapIdentity +} from "@/signals/bapIdentity"; +import { setIdentitySessionStorage } from "@/signals/bapIdentity/client"; +import { + passphrase, + payPk, +} from "@/signals/wallet"; +import { + encryptData, + generateEncryptionKeyFromPassphrase, +} from "@/utils/encryption"; +import randomBytes from "randombytes"; +import { loadKeysFromEncryptedStorage } from "@/signals/wallet/client"; +import toast from "react-hot-toast"; +import { PrivateKey } from "bsv-wasm-web"; +import { EncryptedIdentityJson } from "@/types/identity"; +import { encryptionPrefix, toastErrorProps, toastProps } from "@/constants"; + +interface Props { + onClose: () => void; +} +export default function EnterSelectedPassphrase({ onClose }: Props) { + useSignals(); + const [password, setPassword] = useState(""); + const [error, setError] = useState(false); + + const handleCancel = () => { + cleanup(); + onClose(); + }; + + const cleanup = () => { + setError(false); + setPassword(""); + passphrase.value = ""; + bapIdentityRaw.value = null; + selectedBapIdentity.value = null; + bapIdentities.value = null; + + importProfileFromBackupJsonStep.value = + ImportProfileFromBackupJsonStep.SelectFile; + }; + + const onSubmit = () => { + passphrase.value = ""; + importProfileFromBackupJsonStep.value = + ImportProfileFromBackupJsonStep.Done; + }; + + const passwordCanDecrypt = async () => { + try { + const succeeded = await loadKeysFromEncryptedStorage(password); + if (succeeded === "SUCCESS") { + return true; + } + return false; + } catch (e) { + console.error(e); + } + }; + + const handleEncryptProfile = useCallback(async () => { + if (!passphrase.value || !payPk.value) { + return; + } + + try { + const pubKey = PrivateKey.from_wif(payPk.value) + .to_public_key() + .to_hex(); + bapIdEncryptionKey.value = + (await generateEncryptionKeyFromPassphrase( + passphrase.value, + pubKey + )) ?? null; + + if (!bapIdEncryptionKey.value) { + console.error("No encryption key found. Unable to encrypt."); + return; + } + + const iv = new Uint8Array(randomBytes(16).buffer); + + const encrypted = encryptData( + Buffer.from( + JSON.stringify({ + activeBapIdentity: selectedBapIdentity.value, + }), + "utf-8" + ), + bapIdEncryptionKey.value, + iv + ); + + const encryptedIdentity = + encryptionPrefix + + Buffer.concat([iv, encrypted]).toString("base64"); + + const activeIdentityBackup: EncryptedIdentityJson = { + encryptedIdentity, + pubKey, + }; + + const encryptedIdentityString = JSON.stringify(activeIdentityBackup); + + localStorage.setItem( + "encryptedIdentity", + encryptedIdentityString + ); + + activeBapIdentity.value = selectedBapIdentity.value; + hasIdentityBackup.value = true; + setIdentitySessionStorage(selectedBapIdentity.value!); + + return true; + } catch (e) { + console.log(e); + toast.error("Failed to encrypt identity", toastErrorProps); + } + }, [ + bapIdEncryptionKey.value, + passphrase.value, + payPk.value, + selectedBapIdentity.value, + ]); + + const handleDecryptEncrypt = async () => { + const passwordCorrect = await passwordCanDecrypt(); + + if (passwordCorrect) { + passphrase.value = password; + const identityEncrypted = await handleEncryptProfile(); + if (identityEncrypted) { + onSubmit(); + } + } else { + setError(true); + } + setPassword(""); + }; + + return ( + <> +
    + Enter your password (the password should be the same one used to + encrypt your keys): + + {error && ( +

    + The password is incorrect. +

    + )} +
    +
    + + +
    + + ); +} diff --git a/src/components/modal/manageProfile/steps/SelectProfileJson.tsx b/src/components/modal/manageProfile/steps/SelectProfileJson.tsx new file mode 100644 index 00000000..a5fbf489 --- /dev/null +++ b/src/components/modal/manageProfile/steps/SelectProfileJson.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { toastErrorProps } from "@/constants"; +import { + ImportProfileFromBackupJsonStep, + importProfileFromBackupJsonStep, + activeBapIdentity +} from "@/signals/bapIdentity/index"; +import { setBapIdentity, removeIdentity } from "@/signals/bapIdentity/client"; // set IDs, setMnemonic +import { useSignals, useSignalEffect} from "@preact/signals-react/runtime"; +import toast from "react-hot-toast"; +import { IoMdWarning } from "react-icons/io"; +import CancelButton from "../common/CancelButton"; + +interface Props { + onClose: () => void; +} + +export default function SelectProfileJson({ onClose }: Props) { + useSignals(); + + const handleCancel = () => { + onClose(); + importProfileFromBackupJsonStep.value = + ImportProfileFromBackupJsonStep.SelectFile; + }; + + const handleLogout = () => { + removeIdentity(); + onClose(); + }; + + const validateJson = (json: Record) => { + if (!json || typeof json !== "object") { + throw new Error("Invalid JSON"); + } + + if (!json.xprv || typeof json.xprv !== "string") { + throw new Error("Invalid JSON"); + } + + if ( + !json.ids || + !(typeof json.ids === "string" || typeof json.ids === "object") + ) { + throw new Error("Invalid JSON"); + } + }; + + const handleSelectFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result; + + if (typeof content !== "string") { + return; + } + + try { + const json = JSON.parse(content); + validateJson(json); + setBapIdentity(json); + handleNext(); + } catch (error) { + toast.error( + "Invalid JSON file. Please select a backup json.", + toastErrorProps + ); + } + }; + reader.readAsText(file); + }; + + const handleNext = () => { + importProfileFromBackupJsonStep.value = + ImportProfileFromBackupJsonStep.ChooseIdentity; + }; + + return ( + <> + {activeBapIdentity.value ? ( +

    + You already have an active Identity. If you wish to select + another one, please log out first. +

    + ) : ( + <> +

    + Select the backup JSON file you want to import +

    + +
    + +
    + + )} + +
    + {activeBapIdentity.value && ( + + )} + +
    + + ); +} diff --git a/src/signals/bapIdentity/client.ts b/src/signals/bapIdentity/client.ts new file mode 100644 index 00000000..c52c93c5 --- /dev/null +++ b/src/signals/bapIdentity/client.ts @@ -0,0 +1,167 @@ +"use client"; + +import { BAP } from "bitcoin-bap"; +import { encryptionPrefix } from "@/constants"; +import { + decryptData, + generateEncryptionKeyFromPassphrase, +} from "@/utils/encryption"; +import { + ResultObj, + IdentityResult, + ProfileFromJson, + EncryptedIdentityJson, +} from "@/types/identity"; +import { + identitiesLoading, + bapIdentityRaw, + bapIdentities, + bapIdEncryptionKey, + activeBapIdentity, + selectedBapIdentity, + hasIdentityBackup +} from "./index"; + +export const setBapIdentity = (importedProfile: ProfileFromJson) => { + identitiesLoading.value = true; + bapIdentityRaw.value = importedProfile; + extractIdentities(); +}; + +const getIdentityAddress = async (idKey: string) => { + const resp = fetch(`/api/identity/get`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ idKey: idKey }), + }); + return (await resp).json(); +}; + +const getIdentityByAddress = async (resultObj: ResultObj) => { + const resp = fetch(`/api/identity/validByAddress`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + address: resultObj?.result?.currentAddress, + }), + }); + return (await resp).json(); +}; + +export const extractIdentities = async () => { + if (!bapIdentityRaw.value) return; + + const bapIdRaw = bapIdentityRaw.value; + const bapId = new BAP(bapIdRaw.xprv); + bapId.importIds(bapIdRaw.ids); + const ids = bapId.listIds(); + + const resultsWithAddresses = + ids?.length && + (await Promise.all(ids.map((id: string) => getIdentityAddress(id)))); + + const resultsWithIdentities = + resultsWithAddresses?.length && + (await Promise.all( + resultsWithAddresses.map((resultObj: ResultObj) => + getIdentityByAddress(resultObj) + ) + )); + + bapIdentities.value = + resultsWithIdentities.length && + resultsWithIdentities.map((resultObj: ResultObj) => { + if (resultObj.status === "OK") { + return resultObj.result; + } + }); + + identitiesLoading.value = false; +}; + + +export const loadIdentityFromSessionStorage = () => { + if (!!sessionStorage.getItem("activeIdentity")) { + activeBapIdentity.value = JSON.parse(sessionStorage.getItem("activeIdentity")!); + } +}; + +export const removeIdentity = () => { + bapIdEncryptionKey.value = null; + bapIdentityRaw.value = null; + bapIdentities.value = null; + identitiesLoading.value = false; + activeBapIdentity.value = null; + selectedBapIdentity.value = null; + hasIdentityBackup.value = false; + + localStorage.removeItem("encryptedIdentity"); + sessionStorage.removeItem("activeIdentity"); +}; + + +export const loadIdentityFromEncryptedStorage = async (passphrase: string) => { + const encryptedIdentityStr = localStorage.getItem("encryptedIdentity"); + + if (!encryptedIdentityStr) { + return false; + } + + const encryptedIdentityParts = JSON.parse(encryptedIdentityStr) as EncryptedIdentityJson; + + if (!encryptedIdentityParts.pubKey || !encryptedIdentityParts.encryptedIdentity) { + throw new Error( + "Load identity error - No public key or encryptedIdentity props found in encrypted backup" + ); + } + + const encryptionKey = await generateEncryptionKeyFromPassphrase( + passphrase, + encryptedIdentityParts.pubKey + ); + + if (!encryptionKey) { + throw new Error("No encryption key found. Unable to decrypt."); + } + + let decryptedBackupBin; + + try { + decryptedBackupBin = decryptData( + Buffer.from( + encryptedIdentityParts.encryptedIdentity.replace(encryptionPrefix, ""), + "base64" + ), + encryptionKey + ); + } catch (error) { + console.log(error); + return false; + } + + const decryptedBackupStr = + Buffer.from(decryptedBackupBin).toString("utf-8"); + + const { activeBapIdentity: activeIdentityBackup } = JSON.parse( + decryptedBackupStr + ); + + if (!activeIdentityBackup || !activeIdentityBackup?.identity) { + return false; + } + + activeBapIdentity.value = activeIdentityBackup; + setIdentitySessionStorage(activeIdentityBackup); + + return true; +}; + +export const setIdentitySessionStorage = (identity: IdentityResult) => { + if (!identity) return; + const activeIdentityString = JSON.stringify(identity); + sessionStorage.setItem("activeIdentity", activeIdentityString); +}; \ No newline at end of file diff --git a/src/signals/bapIdentity/index.ts b/src/signals/bapIdentity/index.ts new file mode 100644 index 00000000..4eda6e0e --- /dev/null +++ b/src/signals/bapIdentity/index.ts @@ -0,0 +1,26 @@ +import { signal } from "@preact/signals-react"; + +import { + IdentityResult, + ProfileFromJson, +} from "@/types/identity"; + +export enum ImportProfileFromBackupJsonStep { + SelectFile = 1, + ChooseIdentity = 2, + EnterPassphrase = 3, + Done = 4, +} + +// signals +export const importProfileFromBackupJsonStep = + signal( + ImportProfileFromBackupJsonStep.SelectFile + ); +export const bapIdEncryptionKey = signal(null); +export const bapIdentityRaw = signal(null); +export const bapIdentities = signal(null); +export const identitiesLoading = signal(false); +export const activeBapIdentity = signal(null); +export const selectedBapIdentity = signal(null); +export const hasIdentityBackup = signal(false); diff --git a/src/signals/wallet/client.tsx b/src/signals/wallet/client.tsx index 88dae6ad..07ff6f08 100644 --- a/src/signals/wallet/client.tsx +++ b/src/signals/wallet/client.tsx @@ -131,9 +131,6 @@ export const loadKeysFromSessionStorage = () => { const payPk = sessionStorage.getItem("1satfk"); const ordPk = sessionStorage.getItem("1satok"); - console.log("KEYS!!! -----------"); - console.log(`${payPk} --- ${ordPk}`); - if (payPk && ordPk) { setKeys({ payPk, ordPk }); } diff --git a/src/types/identity.ts b/src/types/identity.ts new file mode 100644 index 00000000..e2971444 --- /dev/null +++ b/src/types/identity.ts @@ -0,0 +1,62 @@ +interface Place { + name?: string; + "@type": "Place"; +} + +export interface IdentityResult { + identity: Identity; + addresses: Address[]; + block: number; + currentAddress: string; + firstSeen: number; + rootAddress: string; + timestamp: number; + valid: boolean; + idKey: string; +} + +export interface Identity { + "@context"?: string; + "@type"?: string; + alternateName?: string; + description?: string; + homeLocation?: Place; + url?: string; + banner?: string; + logo?: string; + image?: string; + email?: string; + paymail?: string; + address?: string; + bitcoinAddress?: string; +} + +export interface Address { + block: number; + address: string; + txId: string; +} + +export interface IdentityAddressResult { + idKey: string; + addresses: Address[]; + firstSeen: number; + currentAddress: string; + rootAddress: string; + timestamp: number; +} + +export interface ResultObj { + status: string; + result: IdentityAddressResult | IdentityResult; +} + +export type ProfileFromJson = { + xprv: string; + ids: string | string[]; +}; + +export interface EncryptedIdentityJson { + encryptedIdentity?: string; + pubKey?: string; +} From 90158ae78664dec7fd81b06bfbd58c8a59591f7f Mon Sep 17 00:00:00 2001 From: elvira-paulin Date: Fri, 2 Aug 2024 13:00:31 +0200 Subject: [PATCH 03/16] removed unused package --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 8b4ebf0d..8c506a89 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "daisyui": "^4.11.1", "dompurify": "^3.0.9", "framer-motion": "^11.1.7", - "generate-avatar": "^1.4.10", "isomorphic-dompurify": "^2.4.0", "jdenticon": "^3.2.0", "js-1sat-ord": "^0.1.29", From 5c109ccf87f8a7aaeee292604bbea7ab1a513ccf Mon Sep 17 00:00:00 2001 From: elvira-paulin Date: Tue, 6 Aug 2024 16:59:12 +0200 Subject: [PATCH 04/16] Added Profile page --- src/app/profile/page.tsx | 29 ++++ src/components/Passphrase/index.tsx | 5 +- src/components/Wallet/menu.tsx | 3 + .../manageProfile/steps/ChooseIdentity.tsx | 135 +-------------- .../steps/EnterSelectedPassphrase.tsx | 37 +++- src/components/pages/profile/index.tsx | 49 ++++++ src/components/profileAccordion/index.tsx | 160 ++++++++++++++++++ src/signals/bapIdentity/client.ts | 110 ++++++++++-- src/signals/bapIdentity/index.ts | 1 + 9 files changed, 378 insertions(+), 151 deletions(-) create mode 100644 src/app/profile/page.tsx create mode 100644 src/components/pages/profile/index.tsx create mode 100644 src/components/profileAccordion/index.tsx diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 00000000..c4ddc853 --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,29 @@ + +import ProfilePage from "@/components/pages/profile"; +import { Suspense } from "react"; + +const Profile = () => { + return ( + + + + ); +}; +export default Profile; + +export async function generateMetadata() { + return { + title: "Profile Page - 1SatOrdinals", + description: "View your profile information.", + openGraph: { + title: "Profile Page - 1SatOrdinals", + description: "View your profile information.", + type: "website", + }, + twitter: { + card: "ummary_large_image", + title: "Profile Page - 1SatOrdinals", + description: "Profile page on 1SatOrdinals.", + }, + }; +} diff --git a/src/components/Passphrase/index.tsx b/src/components/Passphrase/index.tsx index 0d5e5615..3262e228 100644 --- a/src/components/Passphrase/index.tsx +++ b/src/components/Passphrase/index.tsx @@ -30,7 +30,7 @@ import { RiErrorWarningFill } from "react-icons/ri"; import { TbDice } from "react-icons/tb"; import { useCopyToClipboard } from "usehooks-ts"; import { hasIdentityBackup } from "@/signals/bapIdentity/index"; -import {loadIdentityFromEncryptedStorage} from "@/signals/bapIdentity/client"; +import { loadAvailableIdentitiesFromEncryptedStorage, loadIdentityFromEncryptedStorage } from "@/signals/bapIdentity/client"; type Props = { mode: EncryptDecrypt; @@ -179,8 +179,9 @@ const EnterPassphrase: React.FC = ({ const succeeded = await loadKeysFromEncryptedStorage(passphrase.value); if (succeeded && !!hasIdentityBackup.value) { const decryptedId = await loadIdentityFromEncryptedStorage(passphrase.value); + const decryptedIdentities = await loadAvailableIdentitiesFromEncryptedStorage(passphrase.value); - if (!decryptedId) { + if (!decryptedId || !decryptedIdentities) { toast.error("Failed to decrypt identity, please import it again.", toastErrorProps); } } diff --git a/src/components/Wallet/menu.tsx b/src/components/Wallet/menu.tsx index fea10094..aa954d50 100644 --- a/src/components/Wallet/menu.tsx +++ b/src/components/Wallet/menu.tsx @@ -380,6 +380,9 @@ const WalletMenu: React.FC = () => { Manage Profile +
  • + Profile Details +
1Sat Wallet
diff --git a/src/components/modal/manageProfile/steps/ChooseIdentity.tsx b/src/components/modal/manageProfile/steps/ChooseIdentity.tsx index a506c13e..c9147731 100644 --- a/src/components/modal/manageProfile/steps/ChooseIdentity.tsx +++ b/src/components/modal/manageProfile/steps/ChooseIdentity.tsx @@ -1,6 +1,4 @@ "use client"; -import Image from "next/image"; -import { MdAccountCircle } from "react-icons/md"; import { bapIdentities, @@ -9,11 +7,11 @@ import { ImportProfileFromBackupJsonStep, importProfileFromBackupJsonStep, } from "@/signals/bapIdentity/index"; -import { Identity } from "@/types/identity"; + import { useSignals } from "@preact/signals-react/runtime"; import CancelButton from "../common/CancelButton"; -import { getImageFromGP } from "@/utils/getImageFromGP"; -import { hashColor } from "@/utils/hashColor"; + +import ProfileAccordion from "@/components/profileAccordion"; interface Props { onClose: () => void; @@ -21,7 +19,6 @@ interface Props { export default function ChooseIdentity({ onClose }: Props) { useSignals(); - const hiddenFields = ["@context", "@type"]; // not shown to user const handleNext = () => { importProfileFromBackupJsonStep.value = @@ -37,93 +34,6 @@ export default function ChooseIdentity({ onClose }: Props) { onClose(); }; - const setActiveBapIdentity = (id: string) => { - const selectedIdentity = bapIdentities?.value?.find( - (identity) => identity.idKey === id - ); - selectedBapIdentity.value = selectedIdentity || null; - }; - - const processValue = (value: any) => { - let processedValue: any = ""; - - if (typeof value === "object") { - Object.keys(value).forEach((k) => { - processedValue += `${k}: ${value[k]}, `; - }); - } else if (typeof value === "string" && value.indexOf("b://") >= 0) { - const imageUrl = getImageFromGP(value); - - processedValue = ( - - Click to view image - - ); - } else { - processedValue = value; - } - return {processedValue}; - }; - - const makeIdentityAvatar = ( - imageUrl: string | undefined, - idKey: string - ) => { - const url = imageUrl && getImageFromGP(imageUrl); - - return ( -
- {url ? ( - Profile Image - ) : ( - - )} -
- ); - }; - - const buildTable = (identity: Identity) => { - return ( - - - {Object.entries(identity!) - .filter(([key, value]) => !hiddenFields.includes(key)) - .map(([key, value], index) => ( - 0 ? "" : "bg-neutral-800" - }`} - key={index} - > - - {processValue(value)} - - ))} - -
- {key} -
- ); - }; - return ( <> {identitiesLoading.value && ( @@ -131,42 +41,11 @@ export default function ChooseIdentity({ onClose }: Props) { )} - {bapIdentities.value?.length && ( -
-
- Select the identity you want to use: -
+ - {bapIdentities.value.map((identity) => ( -
{ - setActiveBapIdentity(identity.idKey); - }} - > - -
- {makeIdentityAvatar( - identity?.identity?.image, - identity?.idKey - )} -

- {identity?.identity?.alternateName} -

-
-
- {buildTable(identity?.identity)} -
-
- ))} -
- )}
+ {!activeBapIdentity.value && ( +
  • + + Create Profile + +
  • + )} {activeBapIdentity.value && (
  • Profile Details diff --git a/src/components/pages/profile/index.tsx b/src/components/pages/profile/index.tsx index e8262d22..5ec678c5 100644 --- a/src/components/pages/profile/index.tsx +++ b/src/components/pages/profile/index.tsx @@ -32,7 +32,7 @@ const ProfilePage = () => { return availableIdentities.value ? (
    -
    +

    diff --git a/src/components/profileAccordion/index.tsx b/src/components/profileAccordion/index.tsx index 8528d2f7..2f29f2b5 100644 --- a/src/components/profileAccordion/index.tsx +++ b/src/components/profileAccordion/index.tsx @@ -3,11 +3,11 @@ import { useSignals } from "@preact/signals-react/runtime"; import Image from "next/image"; import { MdAccountCircle } from "react-icons/md"; -import type { IdentityResult } from "@/types/identity"; +import type { IdentityResult, Identity } from "@/types/identity"; import { bapIdentities, selectedBapIdentity } from "@/signals/bapIdentity"; -import type { Identity } from "@/types/identity"; import { getImageFromGP } from "@/utils/getImageFromGP"; import { hashColor } from "@/utils/hashColor"; +import type { ReactNode } from "react"; type Props = { canSetActiveBapIdentity: boolean; @@ -62,8 +62,8 @@ const ProfileAccordion = ({ canSetActiveBapIdentity, identities }: Props) => { ); }; - const processValue = (value: any) => { - let processedValue: any = ""; + const processValue = (value: string | Record) => { + let processedValue: string | ReactNode = ""; if (typeof value === "object") { const values = []; @@ -120,7 +120,7 @@ const ProfileAccordion = ({ canSetActiveBapIdentity, identities }: Props) => { return ( 0 ? "" : "bg-[#121212]"}`} - key={index} + key={key} > {key} {processValue(value)} @@ -140,22 +140,24 @@ const ProfileAccordion = ({ canSetActiveBapIdentity, identities }: Props) => { className={`${ canSetActiveBapIdentity && selectedBapIdentity.value?.idKey === identity.idKey - ? "border border-amber-400" + ? "border border-[#555]" : "" } collapse bg-base-200 rounded-lg mt-5 ${ canSetActiveBapIdentity ? "collapse-plus" : "collapse-open" }`} onClick={() => handleClick(identity.idKey)} > - -
    + {canSetActiveBapIdentity && } +
    {makeIdentityAvatar( identity?.identity?.image || identity?.identity?.logo, identity?.idKey, )}
    {identity?.identity?.alternateName}
    -
    + {canSetActiveBapIdentity &&
    + {selectedBapIdentity.value?.idKey}
    } + {!canSetActiveBapIdentity && +
    }
    Date: Fri, 30 Aug 2024 08:19:52 -0400 Subject: [PATCH 12/16] set id key in session storage for signing --- .../manageProfile/steps/ChooseIdentity.tsx | 3 +- src/components/pages/inscribe/html.tsx | 19 +-- src/components/profileAccordion/index.tsx | 16 +- src/signals/bapIdentity/client.ts | 57 ++++--- src/signals/bapIdentity/index.ts | 2 +- src/signals/wallet/client.tsx | 95 ++++++----- src/utils/inscribe.ts | 157 +++++++++--------- 7 files changed, 183 insertions(+), 166 deletions(-) diff --git a/src/components/modal/manageProfile/steps/ChooseIdentity.tsx b/src/components/modal/manageProfile/steps/ChooseIdentity.tsx index c9147731..1ae3d71e 100644 --- a/src/components/modal/manageProfile/steps/ChooseIdentity.tsx +++ b/src/components/modal/manageProfile/steps/ChooseIdentity.tsx @@ -38,7 +38,7 @@ export default function ChooseIdentity({ onClose }: Props) { <> {identitiesLoading.value && (
    - +
    )}