Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8170562
feat: :sparkles: ManagementPageLayout component
ScottJamesPhillips Feb 21, 2025
be4ad70
feat: wallet management route & view w. story
ScottJamesPhillips Feb 21, 2025
96641a4
feat: export LogoButton & added color prop
ScottJamesPhillips Feb 21, 2025
ec64184
feat: details dropdown component
ScottJamesPhillips Feb 21, 2025
eb71b17
feat: wallet details component
ScottJamesPhillips Feb 21, 2025
fdc7740
feat: update wallet management props & use wallet details
ScottJamesPhillips Feb 21, 2025
06ff214
chore: story updates - wallet management
ScottJamesPhillips Feb 21, 2025
9b97980
style: update details dropdown styling
ScottJamesPhillips Feb 22, 2025
8bca7fb
feat: complete wallet detail ui
ScottJamesPhillips Feb 22, 2025
913da3a
refactor: creation of wallet management types
ScottJamesPhillips Feb 23, 2025
be80195
feat: create WalletList component
ScottJamesPhillips Feb 23, 2025
bf48138
refactor:
ScottJamesPhillips Feb 23, 2025
4b860d8
chore: update wallet management story
ScottJamesPhillips Feb 23, 2025
518f050
refactor: rename wallet management components as account management
ScottJamesPhillips Feb 23, 2025
0b87b94
feat: account management routing
ScottJamesPhillips Feb 26, 2025
1462490
feat: account details using vault values
ScottJamesPhillips Feb 26, 2025
e2d4fb6
fix: fix build errors
ScottJamesPhillips Feb 26, 2025
5d46b7e
chore: account management story updates
ScottJamesPhillips Feb 26, 2025
f59c9b8
feat: default select first account
ScottJamesPhillips Feb 27, 2025
d15494e
refactor: add edit account using one component & routed
ScottJamesPhillips Mar 4, 2025
c16527a
style: formatting
ScottJamesPhillips Mar 4, 2025
0b02ad7
refactor: remove references to add and edit views
ScottJamesPhillips Mar 4, 2025
a2b3508
feat: deriveNewAccount functionality
ScottJamesPhillips Mar 4, 2025
7262e59
feat: generate new wallet account
ScottJamesPhillips Mar 6, 2025
e10ff8c
feat: Set creds and wallet on account add & change
ScottJamesPhillips Mar 7, 2025
87f3b4e
refactor: derive account into helper
ScottJamesPhillips Mar 8, 2025
b1cd174
fix: derive account from route and fix wallet detail bug
ScottJamesPhillips Mar 8, 2025
43ddba9
feat: add account w. user defined name
ScottJamesPhillips Mar 8, 2025
2c32612
fix: title update on add-edit-acc
ScottJamesPhillips Mar 8, 2025
cfe4504
feat: account details edit and remove options
ScottJamesPhillips Mar 10, 2025
24bd0c1
feat: edit and add pages with loading functionality
ScottJamesPhillips Mar 10, 2025
58fd5e2
fix: derive account from credentials
ScottJamesPhillips Mar 10, 2025
735ff9d
style: truncate account list item name
ScottJamesPhillips Mar 10, 2025
01f364c
refactor: DropdownOption onClick prop type of SingleCredState
ScottJamesPhillips Mar 10, 2025
612fc62
refactor: onCopyWalletAddress raised to route account managment route…
ScottJamesPhillips Mar 10, 2025
d7bcbfd
chore: account management stories
ScottJamesPhillips Mar 10, 2025
85f4298
fix: prevent removal of initial indexed account
ScottJamesPhillips Mar 10, 2025
cf1d7ed
docs: update readme
ScottJamesPhillips Mar 10, 2025
c873403
style: i18n titles
ScottJamesPhillips Mar 12, 2025
0f171b5
fix: default initial account name 'Personal'
ScottJamesPhillips Mar 12, 2025
45dcc2c
fix: order and delete by addressIndex & pub key on account detail
ScottJamesPhillips Mar 12, 2025
a300b7b
feat: navigate to dashboard on account derive
ScottJamesPhillips Mar 12, 2025
cabe3b9
feat(multi account): wrap up
mrcnk Mar 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { truncateString } from "@/common/lib/string"
import { LogoButton } from "@/components/menu-bar"
import { MinaKeyConst } from "@palladxyz/key-management"
import { type SingleCredentialState, useVault } from "@palladxyz/vault"
import { EyeOff, Pencil } from "lucide-react"
import { useTranslation } from "react-i18next"
import QRCode from "react-qr-code"
import { useNavigate } from "react-router-dom"
import { toast } from "sonner"
import { DetailsDropdown } from "./details-dropdown"

type AccountDetailsProps = {
account: SingleCredentialState
onCopyWalletAddress: () => void
}

function getDerivationPath(accountIndex: number, addressIndex: number) {
return `m/${MinaKeyConst.PURPOSE}'/${MinaKeyConst.MINA_COIN_TYPE}'/${accountIndex}'/0/${addressIndex}`
}

export const AccountDetails = ({
account,
onCopyWalletAddress,
}: AccountDetailsProps) => {
const { t } = useTranslation()
const navigate = useNavigate()

const {
removeCredential,
credentials,
setCredential,
setCurrentWallet,
keyAgentName,
} = useVault()

const dropdownOptions = [
{
name: t("accountManagement.remove"),
icon: <EyeOff className="w-4 h-4" />,
onClick: async (account: SingleCredentialState) => {
const serializedList = Object.values(credentials)
if (
account.credential?.accountIndex === 0 ||
account.credential?.addressIndex === 0
) {
toast.error(t("accountManagement.cannotRemoveFirstCredential"))
return
}

if (
account.credential?.accountIndex === serializedList.length - 1 &&
serializedList.length > 1
) {
removeCredential(account?.credentialName)
setCredential(serializedList[0])
setCurrentWallet({
keyAgentName,
credentialName: serializedList[0]?.credentialName,
currentAccountIndex:
serializedList[0]?.credential?.accountIndex ?? 0,
currentAddressIndex:
serializedList[0]?.credential?.addressIndex ?? 0,
})
toast.success(t("accountManagement.accountRemoved"))
} else {
toast.error(t("accountManagement.onlyLastCredentialCanBeRemoved"))
}
},
},
{
name: t("accountManagement.edit"),
icon: <Pencil className="w-4 h-4" />,
onClick: async (account: SingleCredentialState) => {
navigate(`/accounts/edit/${account.credential?.addressIndex}`)
},
},
]

return (
<div className="flex flex-col text-center justify-center items-center space-y-8 card bg-accent p-3">
<nav className="flex justify-between w-full relative">
<div>
<LogoButton onClick={() => {}} color={"#191725"} />
</div>
<DetailsDropdown options={dropdownOptions} account={account} />
</nav>
<div className="flex w-full justify-between">
<div className="flex flex-col py-3 justify-end items-start text-left">
<div className="text-xl text-secondary">
{account?.credentialName &&
truncateString({
value: account?.credentialName,
endCharCount: 1,
firstCharCount: 12,
})}
</div>
<div className="text-xl text-secondary">
{account?.credentialName &&
truncateString({
value: account?.credential?.address ?? "",
endCharCount: 3,
firstCharCount: 5,
})}
</div>
<div className="text-xl text-secondary">
{getDerivationPath(
account?.credential?.accountIndex ?? 0,
account?.credential?.addressIndex ?? 0,
)}
</div>
</div>
{account?.credential?.address && (
<QRCode
value={account?.credential?.address}
bgColor={"#25233A"}
fgColor={"#D1B4F4"}
className="relative w-[120px] h-[120px]"
onClick={() => {
onCopyWalletAddress()
}}
/>
)}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { truncateString } from "@/common/lib/string"
import type { SingleCredentialState } from "@palladxyz/vault"
import clsx from "clsx"
import { useEffect, useState } from "react"

type AccountListProps = {
accounts: SingleCredentialState[]
handleSelect: (account: SingleCredentialState) => void
}

export const AccountList = ({ accounts, handleSelect }: AccountListProps) => {
const [sortedAccounts, setSortedAccounts] = useState<SingleCredentialState[]>(
[],
)

useEffect(() => {
const sorted = accounts
? [...accounts].sort(
(a, b) =>
//orderd by addressIndex - not sure if this is the correct way to sort
(a?.credential?.addressIndex ?? Number.POSITIVE_INFINITY) -
(b?.credential?.addressIndex ?? Number.POSITIVE_INFINITY),
)
: []

setSortedAccounts(sorted)
}, [accounts])

return (
<div className="space-y-2 py-5">
<div className="text-mint">Other</div>
<div className="text-2xl">Wallets</div>
{sortedAccounts?.map((account) => {
return (
<button
key={account?.credentialName}
type="button"
className={clsx(
"bg-secondary w-full px-6 py-4 flex justify-between rounded-2xl hover:bg-primary",
)}
onClick={() => handleSelect(account)}
>
<p>
{account?.credentialName &&
truncateString({
value: account?.credentialName,
endCharCount: 1,
firstCharCount: 12,
})}
</p>
<p>
{account?.credential?.address &&
truncateString({
value: account?.credential?.address,
endCharCount: 3,
firstCharCount: 5,
})}
</p>
</button>
)
})}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { SingleCredentialState } from "@palladxyz/vault"
import { Ellipsis, X } from "lucide-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import type { DropdownOption } from "../types"

type DetailsDropdownProps = {
options: DropdownOption[]
account: SingleCredentialState
}

export const DetailsDropdown = ({ options, account }: DetailsDropdownProps) => {
const [isOpen, setIsOpen] = useState(false)

const { t } = useTranslation()

return (
<div className="relative">
<button
type="button"
className="btn btn-circle bg-white min-h-10 h-10 w-10 border-none outline-none"
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? (
<X width={24} height={24} color={"#191725"} />
) : (
<Ellipsis width={24} height={24} color={"#191725"} />
)}
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-30 bg-secondary rounded-lg shadow-lg z-10">
<ul className="p-2 shadow menu dropdown-content border-2 border-secondary z-[1] bg-neutral rounded-box w-40">
{options.map((option) => (
<li key={`${option.name}`}>
<button
type="button"
onClick={() => {
if (account) {
option.onClick(account)
setIsOpen(false)
}
}}
className="flex justify-between items-center px-4 py-2 w-full text-left"
>
<span>{t(option.name)}</span>
{option.icon}
</button>
</li>
))}
</ul>
</div>
)}
</div>
)
}
90 changes: 90 additions & 0 deletions packages/features/src/account-management/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { type StoryDefault, action } from "@ladle/react"
import { Network } from "@palladxyz/pallad-core"
import type { SingleCredentialState } from "@palladxyz/vault"
import { AccountManagementView } from "./views/account-management"
import { AddEditAccountView } from "./views/add-edit-account"
const keyAgentName = "TestKeyAgent"
const walletName = "TesWallet"

const accounts: SingleCredentialState[] = [
{
credentialName: "First",
keyAgentName: keyAgentName,
credential: {
"@context": ["https://w3id.org/wallet/v1"],
id: "mina-cred-123",
type: "MinaAddress",
controller: "did:key:z6MkjQkU7hD8q1fS7P3X4f3kY9v2",
name: "Sample Mina Account",
description: "This is a dummy Mina credential for testing purposes.",
chain: Network.Mina,
addressIndex: 0,
accountIndex: 0,
address: "B62qjzAXN9NkX8K6JfGzYz4qVQoMrkL4uJ3FgkFjVZd4W5X5cQFQkCG",
encryptedPrivateKeyBytes: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
},
},
{
credentialName: "Second",
keyAgentName: keyAgentName,
credential: {
"@context": ["https://w3id.org/wallet/v1"],
id: "mina-cred-123",
type: "MinaAddress",
controller: "did:key:z6MkjQkU7hD8q1fS7P3X4f3kY9v2",
name: "Sample Mina Account",
description: "This is a dummy Mina credential for testing purposes.",
chain: Network.Mina,
addressIndex: 1,
accountIndex: 1,
address: "B62qjzAXN9NkX8K6JfGzYz4qVQoMrkL4uJ3FgkFjVZd4W5X5cQFQkCF",
encryptedPrivateKeyBytes: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 11]),
},
},
]

export const AccountManagement = () => {
const selectedIndex = 0
return (
<AccountManagementView
onGoBack={action("Go Back")}
walletName={walletName}
accounts={accounts}
onSelectAccount={(account: SingleCredentialState): void => {
action(`${account}`)
}}
selectedAccount={accounts[selectedIndex]}
onCopyWalletAddress={action("Copy Wallet Address")}
/>
)
}

export const AddAccount = () => {
return (
<AddEditAccountView
title={"Add Account"}
handleAddEditAccount={async (credentialName: string): Promise<void> => {
action(`${credentialName}`)
}}
isLoading={false}
/>
)
}

export const EditAccount = () => {
const selectedIndex = 0
return (
<AddEditAccountView
title={"Edit Account"}
account={accounts[selectedIndex]}
handleAddEditAccount={async (credentialName: string): Promise<void> => {
action(`${credentialName}`)
}}
isLoading={false}
/>
)
}

export default {
title: "Wallet Management",
} satisfies StoryDefault
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useAccount } from "@/common/hooks/use-account"
import { type SingleCredentialState, useVault } from "@palladxyz/vault"
import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { AccountManagementView } from "../views/account-management"

export const AccountManagementRoute = () => {
const navigate = useNavigate()
const {
setCurrentWallet,
setCredential,
keyAgentName,
credentials,
walletName,
getCurrentWallet,
} = useVault()
const [accountList, setAccountlist] = useState<SingleCredentialState[]>([])
const [selectedAccount, setSelectedAccount] = useState<SingleCredentialState>(
accountList[0],
)
const { copyWalletAddress } = useAccount()

const selectAccount = (account: SingleCredentialState) => {
setSelectedAccount(account)
setCredential(account)
setCurrentWallet({
keyAgentName,
credentialName: account?.credentialName,
currentAccountIndex: account?.credential?.accountIndex ?? 0,
currentAddressIndex: account?.credential?.addressIndex ?? 0,
})
}

useEffect(() => {
const serializedList = Object.values(credentials)
setAccountlist(serializedList)
setSelectedAccount(getCurrentWallet().credential)
}, [credentials, getCurrentWallet])

return (
<AccountManagementView
onGoBack={() => navigate(-1)}
walletName={walletName}
accounts={accountList}
onSelectAccount={selectAccount}
selectedAccount={selectedAccount}
onCopyWalletAddress={copyWalletAddress}
/>
)
}
Loading
Loading