From 09271f44a3c21079c08aa975c4e8de62534f3886 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 14:33:32 +0800 Subject: [PATCH 01/50] Init recovery pages, #6919 --- .../next-common/components/recovery/index.jsx | 98 +++++++++++++++++++ .../next-common/utils/consts/menu/index.js | 9 ++ packages/next/pages/recovery/help-others.jsx | 31 ++++++ packages/next/pages/recovery/inheritants.jsx | 31 ++++++ packages/next/pages/recovery/my-recovery.jsx | 31 ++++++ 5 files changed, 200 insertions(+) create mode 100644 packages/next-common/components/recovery/index.jsx create mode 100644 packages/next/pages/recovery/help-others.jsx create mode 100644 packages/next/pages/recovery/inheritants.jsx create mode 100644 packages/next/pages/recovery/my-recovery.jsx diff --git a/packages/next-common/components/recovery/index.jsx b/packages/next-common/components/recovery/index.jsx new file mode 100644 index 0000000000..cdf3c0d7cf --- /dev/null +++ b/packages/next-common/components/recovery/index.jsx @@ -0,0 +1,98 @@ +"use client"; + +import ListLayout from "next-common/components/layout/ListLayout"; +import Tabs from "next-common/components/tabs"; + +const TABS = Object.freeze([ + { + value: "my_recovery", + label: "My Recovery", + url: "/recovery/my-recovery", + }, + { + value: "help_others", + label: "Help Others", + url: "/recovery/help-others", + }, + { + value: "inheritors", + label: "Inheritants", + url: "/recovery/inheritants", + }, +]); + +const TITLE_MAPS = Object.freeze({ + my_recovery: "My Recovery", + help_others: "Help Others", + inheritors: "Inheritants", +}); + +const DESCRIPTION_MAPS = Object.freeze({ + my_recovery: "Manage your recovery configurations and recoverable accounts", + help_others: + "View and manage recovery requests from others where you are a friend", + inheritors: + "View recovery configurations where you are designated as an inheritor", +}); + +function MyRecoveryPlaceholder() { + return ( +
+ My Recovery placeholder content. Manage your recovery configurations here. +
+ ); +} + +function HelpOthersPlaceholder() { + return ( +
+ Help Others placeholder content. View recovery requests from others where + you are a friend. +
+ ); +} + +function InheritorsPlaceholder() { + return ( +
+ Inheritants placeholder content. View recovery configurations where you + are designated as an inheritor. +
+ ); +} + +function RecoveryContent({ activeTab }) { + if (activeTab === "help_others") { + return ; + } else if (activeTab === "inheritors") { + return ; + } + return ; +} + +function HeaderTabs({ activeTab }) { + return ( +
+ +
+ ); +} + +export default function Recovery({ activeTab = "my_recovery" }) { + return ( + } + > +
+ +
+
+ ); +} diff --git a/packages/next-common/utils/consts/menu/index.js b/packages/next-common/utils/consts/menu/index.js index e1cf81ebc7..8be9ae933c 100644 --- a/packages/next-common/utils/consts/menu/index.js +++ b/packages/next-common/utils/consts/menu/index.js @@ -69,6 +69,15 @@ export function getHomeMenu({ modules?.vesting && vesting, modules?.scheduler && scheduler, modules?.whitelist && whitelist, + modules?.recovery && { + name: "Recovery", + value: "recovery", + pathname: "/recovery/my-recovery", + extraMatchNavMenuActivePathnames: [ + "/recovery/help-others", + "/recovery/inheritants", + ], + }, (modules?.proxy || hasMultisig) && getDataMenu(), calendarMenu, votingSpace && votingMenu, diff --git a/packages/next/pages/recovery/help-others.jsx b/packages/next/pages/recovery/help-others.jsx new file mode 100644 index 0000000000..17d6dfb8cb --- /dev/null +++ b/packages/next/pages/recovery/help-others.jsx @@ -0,0 +1,31 @@ +import Recovery from "next-common/components/recovery"; +import NoWalletConnected from "next-common/components/assethubMigrationAssets/noWalletConnected"; +import { withCommonProps } from "next-common/lib"; +import useRealAddress from "next-common/utils/hooks/useRealAddress"; +import getChainSettings from "next-common/utils/consts/settings"; +import { CHAIN } from "next-common/utils/constants"; + +export default function HelpOthersPage() { + const realAddress = useRealAddress(); + + if (!realAddress) { + return ; + } + + return ; +} + +export const getServerSideProps = async (ctx) => { + const { modules } = getChainSettings(CHAIN); + if (!modules?.recovery) { + return { + notFound: true, + }; + } + + return withCommonProps(async () => { + return { + props: {}, + }; + })(ctx); +}; diff --git a/packages/next/pages/recovery/inheritants.jsx b/packages/next/pages/recovery/inheritants.jsx new file mode 100644 index 0000000000..94625c7169 --- /dev/null +++ b/packages/next/pages/recovery/inheritants.jsx @@ -0,0 +1,31 @@ +import Recovery from "next-common/components/recovery"; +import NoWalletConnected from "next-common/components/assethubMigrationAssets/noWalletConnected"; +import { withCommonProps } from "next-common/lib"; +import useRealAddress from "next-common/utils/hooks/useRealAddress"; +import getChainSettings from "next-common/utils/consts/settings"; +import { CHAIN } from "next-common/utils/constants"; + +export default function InheritorsPage() { + const realAddress = useRealAddress(); + + if (!realAddress) { + return ; + } + + return ; +} + +export const getServerSideProps = async (ctx) => { + const { modules } = getChainSettings(CHAIN); + if (!modules?.recovery) { + return { + notFound: true, + }; + } + + return withCommonProps(async () => { + return { + props: {}, + }; + })(ctx); +}; diff --git a/packages/next/pages/recovery/my-recovery.jsx b/packages/next/pages/recovery/my-recovery.jsx new file mode 100644 index 0000000000..1338e411fe --- /dev/null +++ b/packages/next/pages/recovery/my-recovery.jsx @@ -0,0 +1,31 @@ +import Recovery from "next-common/components/recovery"; +import NoWalletConnected from "next-common/components/assethubMigrationAssets/noWalletConnected"; +import { withCommonProps } from "next-common/lib"; +import useRealAddress from "next-common/utils/hooks/useRealAddress"; +import getChainSettings from "next-common/utils/consts/settings"; +import { CHAIN } from "next-common/utils/constants"; + +export default function MyRecoveryPage() { + const realAddress = useRealAddress(); + + if (!realAddress) { + return ; + } + + return ; +} + +export const getServerSideProps = async (ctx) => { + const { modules } = getChainSettings(CHAIN); + if (!modules?.recovery) { + return { + notFound: true, + }; + } + + return withCommonProps(async () => { + return { + props: {}, + }; + })(ctx); +}; From 48e518aec9b3eb133f043e1c2b3b51802b031604 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 15:55:30 +0800 Subject: [PATCH 02/50] feat: implement my recovery page content, #6919 --- .../next-common/components/recovery/index.jsx | 15 +- .../recovery/myRecovery/attemptsTable.jsx | 246 ++++++++++++++++++ .../myRecovery/friendGroupsSection.jsx | 114 ++++++++ .../myRecovery/hooks/useMyFriendGroups.js | 53 ++++ .../myRecovery/hooks/useMyRecoveryAttempts.js | 110 ++++++++ .../components/recovery/myRecovery/index.jsx | 16 ++ .../myRecovery/recoveryAttemptsSection.jsx | 32 +++ 7 files changed, 576 insertions(+), 10 deletions(-) create mode 100644 packages/next-common/components/recovery/myRecovery/attemptsTable.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js create mode 100644 packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js create mode 100644 packages/next-common/components/recovery/myRecovery/index.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx diff --git a/packages/next-common/components/recovery/index.jsx b/packages/next-common/components/recovery/index.jsx index cdf3c0d7cf..bb84274175 100644 --- a/packages/next-common/components/recovery/index.jsx +++ b/packages/next-common/components/recovery/index.jsx @@ -2,6 +2,7 @@ import ListLayout from "next-common/components/layout/ListLayout"; import Tabs from "next-common/components/tabs"; +import MyRecoveryContent from "next-common/components/recovery/myRecovery"; const TABS = Object.freeze([ { @@ -35,14 +36,6 @@ const DESCRIPTION_MAPS = Object.freeze({ "View recovery configurations where you are designated as an inheritor", }); -function MyRecoveryPlaceholder() { - return ( -
- My Recovery placeholder content. Manage your recovery configurations here. -
- ); -} - function HelpOthersPlaceholder() { return (
@@ -62,12 +55,14 @@ function InheritorsPlaceholder() { } function RecoveryContent({ activeTab }) { - if (activeTab === "help_others") { + if (activeTab === "my_recovery") { + return ; + } else if (activeTab === "help_others") { return ; } else if (activeTab === "inheritors") { return ; } - return ; + return null; } function HeaderTabs({ activeTab }) { diff --git a/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx b/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx new file mode 100644 index 0000000000..bb7ed3c7ae --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx @@ -0,0 +1,246 @@ +"use client"; + +import AddressUser from "next-common/components/user/addressUser"; +import Tooltip from "next-common/components/tooltip"; +import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import { useRelayChainApi } from "next-common/context/relayChain"; +import { useChainSettings } from "next-common/context/chain"; +import { estimateBlocksTime } from "next-common/utils"; +import useCall from "next-common/utils/hooks/useCall"; +import { isNil } from "lodash-es"; +import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useEffect, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; + +function BlockNumberWithTooltip({ height }) { + const api = useRelayChainApi(); + const { blockTime } = useChainSettings(); + const { value: currentNumber } = useCall(api?.query?.system?.number, []); + const currentHeight = currentNumber?.toNumber(); + + if (isNil(height) || isNil(currentHeight)) { + return ( + + #{height?.toLocaleString() || 0} + + ); + } + + const diff = Math.max(0, currentHeight - height); + const estimatedTime = diff > 0 ? estimateBlocksTime(diff, blockTime) : null; + + return ( + + + #{height?.toLocaleString() || 0} + + + ); +} + +const desktopColumns = [ + { + name: "Group Index", + className: "w-[120px] text-left", + render: (item) => ( + + ) + } + > + + #{item.friendGroupIndex} + + + ), + }, + { + name: "Initiator", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Init Block", + className: "w-[180px] text-left", + render: (item) => , + }, + { + name: "Last Approval Block", + className: "w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Threshold / Approvals", + className: "w-[160px] text-right", + render: (item) => ( + 0 && ( + + ) + } + > + + {item.fgGroup && ( + + {item.fgGroup?.friendsNeeded || 0} /{" "} + + )} + {item.approvalsCount} + + + ), + }, +]; + +const mobileColumns = [ + { + name: "Group Index", + className: "text-right", + render: (item) => ( + + ) + } + > + + #{item.friendGroupIndex} + + + ), + }, + { + name: "Initiator", + className: "text-left", + render: (item) => , + }, + { + name: "Init Block", + className: "text-right", + render: (item) => , + }, + { + name: "Last Approval Block", + className: "text-right", + render: (item) => ( + + ), + }, + { + name: "Threshold / Approvals", + className: "text-right", + render: (item) => ( + 0 && ( + + ) + } + > + + {item.fgGroup && ( + + {item.fgGroup?.friendsNeeded || 0} /{" "} + + )} + {item.approvalsCount} + + + ), + }, +]; + +function enhanceAttemptWithFriendGroup(attempt, friendGroups = []) { + const fgList = friendGroups?.find((fg) => fg.account === attempt.lostAccount); + const fgGroup = fgList?.friendGroups?.[attempt.friendGroupIndex]; + return { ...attempt, fgGroup }; +} + +export default function MyRecoveryAttemptsTable({ + data, + loading: isLoading, + friendGroups, +}) { + const [navCollapsed] = useNavCollapsed(); + const [dataList, setDataList] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + + const { page, component: pageComponent } = usePaginationComponent( + totalCount, + defaultPageSize, + ); + + const total = data?.length || 0; + + useEffect(() => { + setLoading(true); + }, [page]); + + useEffect(() => { + if (isLoading || isNil(data)) { + return; + } + + const enhanced = (data || []).map((attempt) => + enhanceAttemptWithFriendGroup(attempt, friendGroups), + ); + + setTotalCount(total); + const startIndex = (page - 1) * defaultPageSize; + const endIndex = startIndex + defaultPageSize; + setDataList(enhanced?.slice(startIndex, endIndex)); + setLoading(false); + }, [data, isLoading, page, total, friendGroups]); + + return ( + + + + + + + {total > 0 && pageComponent} + + ); +} diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx new file mode 100644 index 0000000000..d6a29275d8 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -0,0 +1,114 @@ +"use client"; + +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import Loading from "next-common/components/loading"; +import AddressUser from "next-common/components/user/addressUser"; +import Tooltip from "next-common/components/tooltip"; +import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import useMyFriendGroups from "./hooks/useMyFriendGroups"; + +function Field({ label, value }) { + return ( +
+ {label} + {value} +
+ ); +} + +function FriendGroupCard({ group }) { + return ( + + {/* Card Header */} +
+ + Group Index #{group.index} + +
+ + +
+
+ + {/* Card Content */} +
+ + + } + > + + {group.friends?.length || 0} + + + } + /> + + + ) : ( + None + ) + } + /> +
+
+ ); +} + +export default function FriendGroupsSection({ address }) { + const { data, loading } = useMyFriendGroups(address); + + return ( +
+
+ + Friend Groups + + +
+
+ {loading ? ( +
+ +
+ ) : data.length === 0 ? ( + +
+ No friend groups found +
+
+ ) : ( + data.map((group) => ( + + )) + )} +
+
+ ); +} diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js new file mode 100644 index 0000000000..e78eb0a908 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js @@ -0,0 +1,53 @@ +import { useContextApi } from "next-common/context/api"; +import { useEffect, useState } from "react"; + +export default function useMyFriendGroups(address) { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!api || !address) { + return; + } + + if (!api?.query.recovery?.friendGroups) { + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery + .friendGroups(address) + .then((value) => { + if (cancelled) return; + const json = value.toJSON(); + const groups = Array.isArray(json?.[0]) ? json[0] : []; + const mapped = groups.map((group, index) => ({ + index, + friends: group.friends || [], + friendsNeeded: parseInt(group.friendsNeeded) || 0, + inheritor: group.inheritor || "", + inheritancePriority: parseInt(group.inheritancePriority) || 0, + inheritanceDelay: parseInt(group.inheritanceDelay) || 0, + cancelDelay: parseInt(group.cancelDelay) || 0, + })); + setData(mapped); + setLoading(false); + }) + .catch((error) => { + console.error("Failed to query friend groups", error); + if (!cancelled) { + setData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, address]); + + return { data, loading }; +} diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js new file mode 100644 index 0000000000..e2de5e335d --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js @@ -0,0 +1,110 @@ +import { useContextApi } from "next-common/context/api"; +import { useEffect, useState } from "react"; + +function bitfieldToIndices(bitfield) { + const indices = []; + let bitIndex = 0; + for (const byte of bitfield) { + if (typeof byte === "number") { + for (let b = 0; b < 8; b++) { + if (byte & (1 << b)) indices.push(bitIndex + b); + } + } + bitIndex += 8; + } + return indices; +} + +export default function useMyRecoveryAttempts(address) { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!api || !address) { + return; + } + + if (!api?.query.recovery?.attempt) { + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.attempt + .entries() + .then(async (entries) => { + if (cancelled) return; + + // Filter entries where lostAccount matches current address + const myEntries = entries.filter( + ([key]) => key.args?.[0]?.toString() === address, + ); + + // Collect unique lostAccounts to fetch their friend groups + const lostAccounts = [ + ...new Set(myEntries.map(([key]) => key.args?.[0]?.toString())), + ]; + const friendGroupsMap = {}; + + await Promise.all( + lostAccounts.map(async (account) => { + try { + const value = await api.query.recovery.friendGroups(account); + const json = value.toJSON(); + friendGroupsMap[account] = json?.[0] || []; + } catch { + friendGroupsMap[account] = []; + } + }), + ); + + const result = myEntries.map(([storageKey, value]) => { + const friendGroupIndex = storageKey.args?.[1]?.toNumber(); + const json = value.toJSON(); + const attempt = json?.[0] || {}; + + const approvalsBitfield = attempt.approvals || []; + const approvedIndices = bitfieldToIndices(approvalsBitfield); + const approvalsCount = approvedIndices.length; + + const friendGroup = + (friendGroupsMap[address] || [])[friendGroupIndex] || {}; + const friends = friendGroup.friends || []; + const approvedAddresses = approvedIndices + .filter((i) => i < friends.length) + .map((i) => friends[i]); + + return { + lostAccount: address, + friendGroupIndex, + initiator: attempt.initiator || "", + initBlock: parseInt(attempt.initBlock) || 0, + lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, + approvalsCount, + approvedAddresses, + friendsNeeded: friendGroup.friendsNeeded || 0, + }; + }); + + if (!cancelled) { + setData(result); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query recovery attempts", error); + if (!cancelled) { + setData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, address]); + + return { data, loading }; +} diff --git a/packages/next-common/components/recovery/myRecovery/index.jsx b/packages/next-common/components/recovery/myRecovery/index.jsx new file mode 100644 index 0000000000..a8cda49d29 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/index.jsx @@ -0,0 +1,16 @@ +"use client"; + +import useRealAddress from "next-common/utils/hooks/useRealAddress"; +import FriendGroupsSection from "./friendGroupsSection"; +import RecoveryAttemptsSection from "./recoveryAttemptsSection"; + +export default function MyRecoveryContent() { + const address = useRealAddress(); + + return ( +
+ + +
+ ); +} diff --git a/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx b/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx new file mode 100644 index 0000000000..ffa88392b0 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx @@ -0,0 +1,32 @@ +"use client"; + +import useMyRecoveryAttempts from "./hooks/useMyRecoveryAttempts"; +import useMyFriendGroups from "./hooks/useMyFriendGroups"; +import MyRecoveryAttemptsTable from "./attemptsTable"; + +export default function RecoveryAttemptsSection({ address }) { + const { data: attempts, loading: attemptsLoading } = + useMyRecoveryAttempts(address); + const { data: friendGroups } = useMyFriendGroups(address); + + const friendGroupsFormatted = address + ? [{ account: address, friendGroups: friendGroups }] + : []; + + return ( +
+
+ + Ongoing Recovery Attempts + +
+
+ +
+
+ ); +} From 852ce34a2060cb58b0f7f2dca695ae275dfa7067 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 15:59:59 +0800 Subject: [PATCH 03/50] Update, #6919 --- .../recovery/myRecovery/friendGroupsSection.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index d6a29275d8..83d80d783b 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -22,20 +22,20 @@ function FriendGroupCard({ group }) { {/* Card Header */}
- Group Index #{group.index} + Group #{group.index}
@@ -92,9 +92,9 @@ export default function FriendGroupsSection({ address }) { Add
-
+
{loading ? ( -
+
) : data.length === 0 ? ( From 67d7d45839ed549e3895b76627ddb695ffd13aca Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 16:09:17 +0800 Subject: [PATCH 04/50] refactor: list all friends in place, #6919 --- .../myRecovery/friendGroupsSection.jsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 83d80d783b..8b27d8b818 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -3,8 +3,6 @@ import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; import Loading from "next-common/components/loading"; import AddressUser from "next-common/components/user/addressUser"; -import Tooltip from "next-common/components/tooltip"; -import { AddressesTooltip } from "next-common/components/multisigs/fields"; import useMyFriendGroups from "./hooks/useMyFriendGroups"; function Field({ label, value }) { @@ -43,23 +41,6 @@ function FriendGroupCard({ group }) { {/* Card Content */}
- - } - > - - {group.friends?.length || 0} - - - } - />
+ +
+ Friends +
+ {(group.friends || []).map((friend, idx) => ( +
+ +
+ ))} +
+
); } From 49b51f071fe1af56aaf01a38fa03d30fd3ab54c1 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 16:15:15 +0800 Subject: [PATCH 05/50] Update layout, #6919 --- .../myRecovery/friendGroupsSection.jsx | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 8b27d8b818..9a1ce443d7 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -39,8 +39,27 @@ function FriendGroupCard({ group }) {
{/* Card Content */} -
+
+ + {(group.friends || []).map((friend, idx) => ( +
+ +
+ ))} +
+ } + />
- -
- Friends -
- {(group.friends || []).map((friend, idx) => ( -
- -
- ))} -
-
); } From 451fb07f072e0543e6aeae8a95494c8b54e13a80 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 16:18:02 +0800 Subject: [PATCH 06/50] Update border color, #6919 --- .../components/recovery/myRecovery/friendGroupsSection.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 9a1ce443d7..2c5ab1071f 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -48,7 +48,7 @@ function FriendGroupCard({ group }) { {(group.friends || []).map((friend, idx) => (
Date: Thu, 25 Jun 2026 17:29:55 +0800 Subject: [PATCH 07/50] feat: add friend group dialog, #6919 --- .../myRecovery/addFriendGroupDialog.jsx | 204 ++++++++++++++++++ .../myRecovery/friendGroupsSection.jsx | 7 + 2 files changed, 211 insertions(+) create mode 100644 packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx diff --git a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx new file mode 100644 index 0000000000..57a90f50d3 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx @@ -0,0 +1,204 @@ +"use client"; + +import { useState } from "react"; +import SignerPopupWrapper from "next-common/components/popupWithSigner/signerPopupWrapper"; +import SignerWithBalance from "next-common/components/signerPopup/signerWithBalance"; +import PopupLabel from "next-common/components/popup/label"; +import Select from "next-common/components/select"; +import NumberInput from "next-common/lib/input/number"; +import AddressCombo from "next-common/components/addressCombo"; +import { + useExtensionAccounts, + usePopupParams, +} from "next-common/components/popupWithSigner/context"; +import Popup from "next-common/components/popup/wrapper/Popup"; +import { PopupButtonWrapper } from "next-common/components/popup/wrapper"; +import SecondaryButton from "next-common/lib/button/secondary"; +import PrimaryButton from "next-common/lib/button/primary"; +import { SystemClose } from "@osn/icons/subsquare"; + +const PRIORITY_OPTIONS = Array.from({ length: 10 }, (_, i) => ({ + label: String(i), + value: String(i), +})); + +function PriorityField({ value, onChange }) { + return ( +
+ + onChange(value)} + small + /> +
+ ); +} + +function DelayField({ label, tooltip, value, onChange }) { + return ( +
+ + +
+ ); +} + +function AddFriendGroupForm() { + const { onClose } = usePopupParams(); + const extensionAccounts = useExtensionAccounts(); + const accounts = (extensionAccounts || []).map((acc) => ({ + address: acc.address, + name: acc.meta?.name, + disabled: acc?.disabled ?? false, + })); + + const [priority, setPriority] = useState("0"); + const [friends, setFriends] = useState([""]); + const [threshold, setThreshold] = useState("1"); + const [inheritorDelay, setInheritorDelay] = useState(""); + const [cancelDelay, setCancelDelay] = useState(""); + + const addFriend = () => setFriends([...friends, ""]); + const removeFriend = (idx) => { + if (friends.length <= 1) return; + const next = friends.filter((_, i) => i !== idx); + setFriends(next); + if (parseInt(threshold) > next.length) { + setThreshold(String(next.length)); + } + }; + const updateFriend = (idx, addr) => { + const next = [...friends]; + next[idx] = addr; + setFriends(next); + }; + + const validFriendsCount = friends.filter(Boolean).length; + + return ( + + + + + + + + + + + + + + + Cancel + Submit + + + ); +} + +export default function AddFriendGroupDialog({ onClose }) { + return ( + + + + ); +} diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 2c5ab1071f..aa88ce6056 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -1,9 +1,11 @@ "use client"; +import { useState } from "react"; import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; import Loading from "next-common/components/loading"; import AddressUser from "next-common/components/user/addressUser"; import useMyFriendGroups from "./hooks/useMyFriendGroups"; +import AddFriendGroupDialog from "./addFriendGroupDialog"; function Field({ label, value }) { return ( @@ -77,10 +79,14 @@ function FriendGroupCard({ group }) { } export default function FriendGroupsSection({ address }) { + const [showDialog, setShowDialog] = useState(false); const { data, loading } = useMyFriendGroups(address); return (
+ {showDialog && ( + setShowDialog(false)} /> + )}
Friend Groups @@ -88,6 +94,7 @@ export default function FriendGroupsSection({ address }) { From 383e5b902ca37d4addb5d223833896c0a6fb0e52 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 17:50:04 +0800 Subject: [PATCH 08/50] fix: empty friend groups, #6919 --- .../myRecovery/friendGroupsSection.jsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index aa88ce6056..24039debe5 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -99,23 +99,23 @@ export default function FriendGroupsSection({ address }) { Add
-
- {loading ? ( -
- -
- ) : data.length === 0 ? ( - -
- No friend groups found -
-
- ) : ( - data.map((group) => ( + {loading ? ( +
+ +
+ ) : data.length === 0 ? ( + + + No friend groups found + + + ) : ( +
+ {data.map((group) => ( - )) - )} -
+ ))} +
+ )}
); } From cb836dc58b7d27a2ccf8ec8699fb5109c22d6476 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 18:09:19 +0800 Subject: [PATCH 09/50] feat: implement add friend group submission, #6919 --- .../myRecovery/addFriendGroupDialog.jsx | 74 ++++++++++++++++--- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx index 57a90f50d3..d4a04546a4 100644 --- a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx @@ -1,6 +1,4 @@ -"use client"; - -import { useState } from "react"; +import { useCallback, useState } from "react"; import SignerPopupWrapper from "next-common/components/popupWithSigner/signerPopupWrapper"; import SignerWithBalance from "next-common/components/signerPopup/signerWithBalance"; import PopupLabel from "next-common/components/popup/label"; @@ -10,12 +8,15 @@ import AddressCombo from "next-common/components/addressCombo"; import { useExtensionAccounts, usePopupParams, + useSignerAccount, } from "next-common/components/popupWithSigner/context"; import Popup from "next-common/components/popup/wrapper/Popup"; -import { PopupButtonWrapper } from "next-common/components/popup/wrapper"; -import SecondaryButton from "next-common/lib/button/secondary"; -import PrimaryButton from "next-common/lib/button/primary"; import { SystemClose } from "@osn/icons/subsquare"; +import TxSubmissionButton from "next-common/components/common/tx/txSubmissionButton"; +import AdvanceSettings from "next-common/components/summary/newProposalQuickStart/common/advanceSettings"; +import EstimatedGas from "next-common/components/estimatedGas"; +import { useContextApi } from "next-common/context/api"; +import { isSameAddress } from "next-common/utils"; const PRIORITY_OPTIONS = Array.from({ length: 10 }, (_, i) => ({ label: String(i), @@ -124,6 +125,9 @@ function DelayField({ label, tooltip, value, onChange }) { function AddFriendGroupForm() { const { onClose } = usePopupParams(); const extensionAccounts = useExtensionAccounts(); + const api = useContextApi(); + const signerAccount = useSignerAccount(); + const address = signerAccount?.realAddress; const accounts = (extensionAccounts || []).map((acc) => ({ address: acc.address, name: acc.meta?.name, @@ -153,6 +157,55 @@ function AddFriendGroupForm() { const validFriendsCount = friends.filter(Boolean).length; + const getTxFunc = useCallback(async () => { + const validFriends = friends.filter(Boolean); + if (validFriends.length === 0) { + throw new Error("Please add at least one friend address"); + } + + if (validFriends.some((friend) => isSameAddress(friend, address))) { + throw new Error("Cannot add yourself as a friend"); + } + + const thresholdNum = parseInt(threshold); + if (!thresholdNum || thresholdNum < 1) { + throw new Error("Please enter a valid threshold"); + } + if (thresholdNum > validFriends.length) { + throw new Error("Threshold cannot exceed the number of friends"); + } + + const priorityNum = parseInt(priority); + if (isNaN(priorityNum)) { + throw new Error("Please select a valid priority"); + } + + const inheritorDelayNum = parseInt(inheritorDelay); + if (!inheritorDelay || isNaN(inheritorDelayNum) || inheritorDelayNum < 0) { + throw new Error("Please enter a valid inheritor delay"); + } + + const cancelDelayNum = parseInt(cancelDelay); + if (!cancelDelay || isNaN(cancelDelayNum) || cancelDelayNum < 0) { + throw new Error("Please enter a valid cancel delay"); + } + + const raw = await api.query.recovery.friendGroups(address); + const json = raw.toJSON(); + const currentGroups = Array.isArray(json?.[0]) ? json[0] : []; + + const newGroup = [ + validFriends, + thresholdNum, + null, + priorityNum, + inheritorDelayNum, + cancelDelayNum, + ]; + + return api.tx.recovery.setFriendGroups([...currentGroups, newGroup]); + }, [api, address, friends, threshold, priority, inheritorDelay, cancelDelay]); + return ( @@ -187,10 +240,11 @@ function AddFriendGroupForm() { onChange={setCancelDelay} /> - - Cancel - Submit - + + + + + ); } From f2b5d7d2ef9b2021345c348e19fd6f577cb83238 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 18:17:09 +0800 Subject: [PATCH 10/50] feat: add inheritor field to friend group form, #6919 --- .../myRecovery/addFriendGroupDialog.jsx | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx index d4a04546a4..ab43f3b4ae 100644 --- a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx @@ -137,6 +137,7 @@ function AddFriendGroupForm() { const [priority, setPriority] = useState("0"); const [friends, setFriends] = useState([""]); const [threshold, setThreshold] = useState("1"); + const [inheritor, setInheritor] = useState(""); const [inheritorDelay, setInheritorDelay] = useState(""); const [cancelDelay, setCancelDelay] = useState(""); @@ -175,6 +176,10 @@ function AddFriendGroupForm() { throw new Error("Threshold cannot exceed the number of friends"); } + if (!inheritor) { + throw new Error("Please enter the inheritor address"); + } + const priorityNum = parseInt(priority); if (isNaN(priorityNum)) { throw new Error("Please select a valid priority"); @@ -194,17 +199,26 @@ function AddFriendGroupForm() { const json = raw.toJSON(); const currentGroups = Array.isArray(json?.[0]) ? json[0] : []; - const newGroup = [ - validFriends, - thresholdNum, - null, - priorityNum, - inheritorDelayNum, - cancelDelayNum, - ]; + const newGroup = { + friends: validFriends, + friendsNeeded: thresholdNum, + inheritor, + inheritancePriority: priorityNum, + inheritanceDelay: inheritorDelayNum, + cancelDelay: cancelDelayNum, + }; return api.tx.recovery.setFriendGroups([...currentGroups, newGroup]); - }, [api, address, friends, threshold, priority, inheritorDelay, cancelDelay]); + }, [ + api, + address, + friends, + threshold, + priority, + inheritor, + inheritorDelay, + cancelDelay, + ]); return ( @@ -226,6 +240,21 @@ function AddFriendGroupForm() { max={validFriendsCount} /> +
+ + setInheritor(value || "")} + placeholder="Enter inheritor address" + size="small" + canEdit + /> +
+ Date: Thu, 25 Jun 2026 18:28:42 +0800 Subject: [PATCH 11/50] Update, #6919 --- .../next-common/components/recovery/index.jsx | 18 ++---------------- .../myRecovery/friendGroupsSection.jsx | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/packages/next-common/components/recovery/index.jsx b/packages/next-common/components/recovery/index.jsx index bb84274175..d477b2bd11 100644 --- a/packages/next-common/components/recovery/index.jsx +++ b/packages/next-common/components/recovery/index.jsx @@ -22,20 +22,6 @@ const TABS = Object.freeze([ }, ]); -const TITLE_MAPS = Object.freeze({ - my_recovery: "My Recovery", - help_others: "Help Others", - inheritors: "Inheritants", -}); - -const DESCRIPTION_MAPS = Object.freeze({ - my_recovery: "Manage your recovery configurations and recoverable accounts", - help_others: - "View and manage recovery requests from others where you are a friend", - inheritors: - "View recovery configurations where you are designated as an inheritor", -}); - function HelpOthersPlaceholder() { return (
@@ -81,8 +67,8 @@ export default function Recovery({ activeTab = "my_recovery" }) { return ( } >
diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 24039debe5..da6d140632 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -33,7 +33,7 @@ function FriendGroupCard({ group }) { From 6fa9a354c82aac3fa28aebbef5467baa81cf8d70 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 21:52:08 +0800 Subject: [PATCH 12/50] feat: add edit friend group dialog, #6919 --- .../myRecovery/addFriendGroupDialog.jsx | 135 ++---------- .../myRecovery/editFriendGroupDialog.jsx | 192 ++++++++++++++++++ .../recovery/myRecovery/friendGroupFields.jsx | 128 ++++++++++++ .../myRecovery/friendGroupsSection.jsx | 32 ++- 4 files changed, 358 insertions(+), 129 deletions(-) create mode 100644 packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/friendGroupFields.jsx diff --git a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx index ab43f3b4ae..eb55c24b6d 100644 --- a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx @@ -1,126 +1,24 @@ import { useCallback, useState } from "react"; import SignerPopupWrapper from "next-common/components/popupWithSigner/signerPopupWrapper"; import SignerWithBalance from "next-common/components/signerPopup/signerWithBalance"; -import PopupLabel from "next-common/components/popup/label"; -import Select from "next-common/components/select"; -import NumberInput from "next-common/lib/input/number"; -import AddressCombo from "next-common/components/addressCombo"; import { useExtensionAccounts, usePopupParams, useSignerAccount, } from "next-common/components/popupWithSigner/context"; import Popup from "next-common/components/popup/wrapper/Popup"; -import { SystemClose } from "@osn/icons/subsquare"; import TxSubmissionButton from "next-common/components/common/tx/txSubmissionButton"; import AdvanceSettings from "next-common/components/summary/newProposalQuickStart/common/advanceSettings"; import EstimatedGas from "next-common/components/estimatedGas"; import { useContextApi } from "next-common/context/api"; import { isSameAddress } from "next-common/utils"; - -const PRIORITY_OPTIONS = Array.from({ length: 10 }, (_, i) => ({ - label: String(i), - value: String(i), -})); - -function PriorityField({ value, onChange }) { - return ( -
- - onChange(value)} - small - /> -
- ); -} - -function DelayField({ label, tooltip, value, onChange }) { - return ( -
- - -
- ); -} +import { + PriorityField, + FriendsField, + ThresholdField, + InheritorField, + DelayField, +} from "./friendGroupFields"; function AddFriendGroupForm() { const { onClose } = usePopupParams(); @@ -240,20 +138,11 @@ function AddFriendGroupForm() { max={validFriendsCount} /> -
- - setInheritor(value || "")} - placeholder="Enter inheritor address" - size="small" - canEdit - /> -
+ ({ + address: acc.address, + name: acc.meta?.name, + disabled: acc?.disabled ?? false, + })); + + const [priority, setPriority] = useState( + String(group.inheritancePriority ?? 0), + ); + const [friends, setFriends] = useState( + group.friends?.length ? group.friends : [""], + ); + const [threshold, setThreshold] = useState(String(group.friendsNeeded ?? 1)); + const [inheritor, setInheritor] = useState(group.inheritor || ""); + const [inheritorDelay, setInheritorDelay] = useState( + String(group.inheritanceDelay ?? ""), + ); + const [cancelDelay, setCancelDelay] = useState( + String(group.cancelDelay ?? ""), + ); + + const addFriend = () => setFriends([...friends, ""]); + const removeFriend = (idx) => { + if (friends.length <= 1) return; + const next = friends.filter((_, i) => i !== idx); + setFriends(next); + if (parseInt(threshold) > next.length) { + setThreshold(String(next.length)); + } + }; + const updateFriend = (idx, addr) => { + const next = [...friends]; + next[idx] = addr; + setFriends(next); + }; + + const validFriendsCount = friends.filter(Boolean).length; + + const getTxFunc = useCallback(async () => { + const validFriends = friends.filter(Boolean); + if (validFriends.length === 0) { + throw new Error("Please add at least one friend address"); + } + + if (validFriends.some((friend) => isSameAddress(friend, address))) { + throw new Error("Cannot add yourself as a friend"); + } + + const thresholdNum = parseInt(threshold); + if (!thresholdNum || thresholdNum < 1) { + throw new Error("Please enter a valid threshold"); + } + if (thresholdNum > validFriends.length) { + throw new Error("Threshold cannot exceed the number of friends"); + } + + if (!inheritor) { + throw new Error("Please enter the inheritor address"); + } + + const priorityNum = parseInt(priority); + if (isNaN(priorityNum)) { + throw new Error("Please select a valid priority"); + } + + const inheritorDelayNum = parseInt(inheritorDelay); + if (!inheritorDelay || isNaN(inheritorDelayNum) || inheritorDelayNum < 0) { + throw new Error("Please enter a valid inheritor delay"); + } + + const cancelDelayNum = parseInt(cancelDelay); + if (!cancelDelay || isNaN(cancelDelayNum) || cancelDelayNum < 0) { + throw new Error("Please enter a valid cancel delay"); + } + + const raw = await api.query.recovery.friendGroups(address); + const json = raw.toJSON(); + const currentGroups = Array.isArray(json?.[0]) ? json[0] : []; + + const updatedGroups = currentGroups.map((g, idx) => { + if (idx === group.index) { + return { + friends: validFriends, + friendsNeeded: thresholdNum, + inheritor, + inheritancePriority: priorityNum, + inheritanceDelay: inheritorDelayNum, + cancelDelay: cancelDelayNum, + }; + } + return g; + }); + + return api.tx.recovery.setFriendGroups(updatedGroups); + }, [ + api, + address, + friends, + threshold, + priority, + inheritor, + inheritorDelay, + cancelDelay, + group.index, + ]); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default function EditFriendGroupDialog({ onClose, group }) { + return ( + + + + ); +} diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupFields.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupFields.jsx new file mode 100644 index 0000000000..77dc998652 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/friendGroupFields.jsx @@ -0,0 +1,128 @@ +import PopupLabel from "next-common/components/popup/label"; +import Select from "next-common/components/select"; +import NumberInput from "next-common/lib/input/number"; +import AddressCombo from "next-common/components/addressCombo"; +import { SystemClose } from "@osn/icons/subsquare"; + +const PRIORITY_OPTIONS = Array.from({ length: 10 }, (_, i) => ({ + label: String(i), + value: String(i), +})); + +export function PriorityField({ value, onChange }) { + return ( +
+ + onChange(value)} + small + /> +
+ ); +} + +export function InheritorField({ accounts, value, onChange }) { + return ( +
+ + onChange(addr || "")} + placeholder="Enter inheritor address" + size="small" + canEdit + /> +
+ ); +} + +export function DelayField({ label, tooltip, value, onChange }) { + return ( +
+ + +
+ ); +} diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index da6d140632..9ee5299365 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -6,6 +6,7 @@ import Loading from "next-common/components/loading"; import AddressUser from "next-common/components/user/addressUser"; import useMyFriendGroups from "./hooks/useMyFriendGroups"; import AddFriendGroupDialog from "./addFriendGroupDialog"; +import EditFriendGroupDialog from "./editFriendGroupDialog"; function Field({ label, value }) { return ( @@ -16,7 +17,7 @@ function Field({ label, value }) { ); } -function FriendGroupCard({ group }) { +function FriendGroupCard({ group, onEdit }) { return ( {/* Card Header */} @@ -28,6 +29,7 @@ function FriendGroupCard({ group }) { @@ -79,13 +81,24 @@ function FriendGroupCard({ group }) { } export default function FriendGroupsSection({ address }) { - const [showDialog, setShowDialog] = useState(false); + const [showAddDialog, setShowAddDialog] = useState(false); + const [showEditDialog, setShowEditDialog] = useState(false); + const [editingGroup, setEditingGroup] = useState(null); const { data, loading } = useMyFriendGroups(address); return (
- {showDialog && ( - setShowDialog(false)} /> + {showAddDialog && ( + setShowAddDialog(false)} /> + )} + {showEditDialog && editingGroup && ( + { + setShowEditDialog(false); + setEditingGroup(null); + }} + group={editingGroup} + /> )}
@@ -94,7 +107,7 @@ export default function FriendGroupsSection({ address }) { @@ -112,7 +125,14 @@ export default function FriendGroupsSection({ address }) { ) : (
{data.map((group) => ( - + { + setEditingGroup(g); + setShowEditDialog(true); + }} + /> ))}
)} From 94a9b7f27266fabab60039c1f75e59ca00a35113 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 22:08:10 +0800 Subject: [PATCH 13/50] feat: wrap Recovery component with RelayChainApiProvider --- .../next-common/components/recovery/index.jsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/next-common/components/recovery/index.jsx b/packages/next-common/components/recovery/index.jsx index d477b2bd11..aaac81d0d6 100644 --- a/packages/next-common/components/recovery/index.jsx +++ b/packages/next-common/components/recovery/index.jsx @@ -3,6 +3,7 @@ import ListLayout from "next-common/components/layout/ListLayout"; import Tabs from "next-common/components/tabs"; import MyRecoveryContent from "next-common/components/recovery/myRecovery"; +import { RelayChainApiProvider } from "next-common/context/relayChain"; const TABS = Object.freeze([ { @@ -65,15 +66,17 @@ function HeaderTabs({ activeTab }) { export default function Recovery({ activeTab = "my_recovery" }) { return ( - } - > -
- -
-
+ + } + > +
+ +
+
+
); } From 2beb89dc7f7fe782150dda72d767865bc019d891 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 25 Jun 2026 22:21:00 +0800 Subject: [PATCH 14/50] feat: add remove friend group dialog, #6919 --- .../myRecovery/friendGroupsSection.jsx | 22 +++++++++++++-- .../myRecovery/removeFriendGroupDialog.jsx | 27 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/next-common/components/recovery/myRecovery/removeFriendGroupDialog.jsx diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 9ee5299365..73cd067d92 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -7,6 +7,7 @@ import AddressUser from "next-common/components/user/addressUser"; import useMyFriendGroups from "./hooks/useMyFriendGroups"; import AddFriendGroupDialog from "./addFriendGroupDialog"; import EditFriendGroupDialog from "./editFriendGroupDialog"; +import RemoveFriendGroupDialog from "./removeFriendGroupDialog"; function Field({ label, value }) { return ( @@ -17,7 +18,7 @@ function Field({ label, value }) { ); } -function FriendGroupCard({ group, onEdit }) { +function FriendGroupCard({ group, onEdit, onRemove }) { return ( {/* Card Header */} @@ -35,7 +36,8 @@ function FriendGroupCard({ group, onEdit }) { @@ -84,6 +86,8 @@ export default function FriendGroupsSection({ address }) { const [showAddDialog, setShowAddDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); const [editingGroup, setEditingGroup] = useState(null); + const [showRemoveDialog, setShowRemoveDialog] = useState(false); + const [removingIndex, setRemovingIndex] = useState(null); const { data, loading } = useMyFriendGroups(address); return ( @@ -100,6 +104,16 @@ export default function FriendGroupsSection({ address }) { group={editingGroup} /> )} + {showRemoveDialog && ( + { + setShowRemoveDialog(false); + setRemovingIndex(null); + }} + index={removingIndex} + address={address} + /> + )}
Friend Groups @@ -132,6 +146,10 @@ export default function FriendGroupsSection({ address }) { setEditingGroup(g); setShowEditDialog(true); }} + onRemove={(index) => { + setRemovingIndex(index); + setShowRemoveDialog(true); + }} /> ))}
diff --git a/packages/next-common/components/recovery/myRecovery/removeFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/removeFriendGroupDialog.jsx new file mode 100644 index 0000000000..fe99531fb8 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/removeFriendGroupDialog.jsx @@ -0,0 +1,27 @@ +"use client"; + +import React, { useCallback } from "react"; +import SimpleTxPopup from "next-common/components/simpleTxPopup"; +import { useContextApi } from "next-common/context/api"; + +export default function RemoveFriendGroupDialog({ onClose, index, address }) { + const api = useContextApi(); + + const getTxFunc = useCallback(async () => { + const raw = await api.query.recovery.friendGroups(address); + const json = raw.toJSON(); + const currentGroups = Array.isArray(json?.[0]) ? json[0] : []; + + const updatedGroups = currentGroups.filter((_, idx) => idx !== index); + + return api.tx.recovery.setFriendGroups(updatedGroups); + }, [api, address, index]); + + return ( + + ); +} From a1f7f8063ce569d13d480b67383e052e10b2a957 Mon Sep 17 00:00:00 2001 From: chaojun Date: Fri, 26 Jun 2026 09:52:39 +0800 Subject: [PATCH 15/50] Update, #6919 --- .../components/recovery/myRecovery/addFriendGroupDialog.jsx | 2 ++ .../components/recovery/myRecovery/friendGroupsSection.jsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx index eb55c24b6d..077466e5e8 100644 --- a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx @@ -1,3 +1,5 @@ +"use client"; + import { useCallback, useState } from "react"; import SignerPopupWrapper from "next-common/components/popupWithSigner/signerPopupWrapper"; import SignerWithBalance from "next-common/components/signerPopup/signerWithBalance"; diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 73cd067d92..7f2265a1ba 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -36,7 +36,7 @@ function FriendGroupCard({ group, onEdit, onRemove }) {
{/* Card Content */} -
- +
+
+ + + + + ) : ( + None + ) + } + /> + } + /> + } + /> +
+ - {(group.friends || []).map((friend, idx) => ( + {displayedFriends.map((friend, idx) => (
))} + {hasMore && !showAllFriends && ( + + )} + {showAllFriends && ( + + )}
} /> - - - ) : ( - None - ) - } - />
); @@ -114,7 +151,7 @@ export default function FriendGroupsSection({ address }) { address={address} /> )} -
+
Friend Groups @@ -137,7 +174,7 @@ export default function FriendGroupsSection({ address }) { ) : ( -
+
{data.map((group) => ( Date: Fri, 26 Jun 2026 11:07:21 +0800 Subject: [PATCH 17/50] feat: add slash button, #6919 --- .../recovery/myRecovery/attemptsTable.jsx | 35 ++++++++++++++++++- .../myRecovery/slashAttemptDialog.jsx | 21 +++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 packages/next-common/components/recovery/myRecovery/slashAttemptDialog.jsx diff --git a/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx b/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx index bb7ed3c7ae..84c079ae45 100644 --- a/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx +++ b/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx @@ -16,6 +16,7 @@ import { MapDataList } from "next-common/components/dataList"; import ScrollerX from "next-common/components/styled/containers/scrollerX"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; +import SlashAttemptDialog from "./slashAttemptDialog"; function BlockNumberWithTooltip({ height }) { const api = useRelayChainApi(); @@ -85,7 +86,7 @@ const desktopColumns = [ }, { name: "Threshold / Approvals", - className: "w-[160px] text-right", + className: "w-[160px] text-left", render: (item) => ( ), }, + { + name: "Action", + className: "w-[100px] text-right", + render: (item) => , + }, ]; const mobileColumns = [ @@ -173,8 +179,35 @@ const mobileColumns = [ ), }, + { + name: "Action", + className: "text-left", + render: (item) => , + }, ]; +function SlashButton({ friendGroupIndex }) { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + {showDialog && ( + setShowDialog(false)} + friendGroupIndex={friendGroupIndex} + /> + )} + + + ); +} + function enhanceAttemptWithFriendGroup(attempt, friendGroups = []) { const fgList = friendGroups?.find((fg) => fg.account === attempt.lostAccount); const fgGroup = fgList?.friendGroups?.[attempt.friendGroupIndex]; diff --git a/packages/next-common/components/recovery/myRecovery/slashAttemptDialog.jsx b/packages/next-common/components/recovery/myRecovery/slashAttemptDialog.jsx new file mode 100644 index 0000000000..748306ea4b --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/slashAttemptDialog.jsx @@ -0,0 +1,21 @@ +"use client"; + +import { useCallback } from "react"; +import SimpleTxPopup from "next-common/components/simpleTxPopup"; +import { useContextApi } from "next-common/context/api"; + +export default function SlashAttemptDialog({ onClose, friendGroupIndex }) { + const api = useContextApi(); + + const getTxFunc = useCallback(async () => { + return api.tx.recovery.slashAttempt(friendGroupIndex); + }, [api, friendGroupIndex]); + + return ( + + ); +} From a01f0db356f65ebc21e805f9c8b28467b0dd8cce Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 13:43:35 +0800 Subject: [PATCH 18/50] Move recovery content into account page, #6919 --- .../components/overview/account/subTabs.js | 10 ++++- .../components/recovery/helpOthers/index.jsx | 15 +++++++ .../components/recovery/inheritors/index.jsx | 15 +++++++ .../components/recovery/myRecovery/index.jsx | 2 + .../components/recovery/subTabs.jsx | 42 +++++++++++++++++++ .../next-common/utils/consts/menu/common.js | 3 ++ .../next-common/utils/consts/menu/data.js | 12 ------ .../next-common/utils/consts/menu/index.js | 9 ---- packages/next/pages/account/help-recover.jsx | 17 ++++++++ packages/next/pages/account/inherited.jsx | 17 ++++++++ packages/next/pages/account/my-recovery.jsx | 17 ++++++++ packages/next/pages/recovery/help-others.jsx | 31 -------------- packages/next/pages/recovery/inheritants.jsx | 31 -------------- packages/next/pages/recovery/my-recovery.jsx | 31 -------------- 14 files changed, 137 insertions(+), 115 deletions(-) create mode 100644 packages/next-common/components/recovery/helpOthers/index.jsx create mode 100644 packages/next-common/components/recovery/inheritors/index.jsx create mode 100644 packages/next-common/components/recovery/subTabs.jsx create mode 100644 packages/next/pages/account/help-recover.jsx create mode 100644 packages/next/pages/account/inherited.jsx create mode 100644 packages/next/pages/account/my-recovery.jsx delete mode 100644 packages/next/pages/recovery/help-others.jsx delete mode 100644 packages/next/pages/recovery/inheritants.jsx delete mode 100644 packages/next/pages/recovery/my-recovery.jsx diff --git a/packages/next-common/components/overview/account/subTabs.js b/packages/next-common/components/overview/account/subTabs.js index 3d3e75560f..8db358e014 100644 --- a/packages/next-common/components/overview/account/subTabs.js +++ b/packages/next-common/components/overview/account/subTabs.js @@ -17,7 +17,7 @@ function TabTitle({ active, children }) { export default function AccountSubTabs({ className = "" }) { const { hasMultisig, - modules: { proxy }, + modules: { proxy, recovery }, } = useChainSettings(); const chain = useChain(); @@ -76,5 +76,13 @@ export default function AccountSubTabs({ className = "" }) { }); } + if (recovery) { + tabs.push({ + value: "recovery", + label: ({ active }) => Recovery, + url: "/account/my-recovery", + }); + } + return ; } diff --git a/packages/next-common/components/recovery/helpOthers/index.jsx b/packages/next-common/components/recovery/helpOthers/index.jsx new file mode 100644 index 0000000000..8a6fe431cd --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/index.jsx @@ -0,0 +1,15 @@ +"use client"; + +import RecoverySubTabs from "next-common/components/recovery/subTabs"; + +export default function HelpOthersContent() { + return ( +
+ +
+ Help Others placeholder content. View recovery requests from others + where you are a friend. +
+
+ ); +} diff --git a/packages/next-common/components/recovery/inheritors/index.jsx b/packages/next-common/components/recovery/inheritors/index.jsx new file mode 100644 index 0000000000..269bb78321 --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/index.jsx @@ -0,0 +1,15 @@ +"use client"; + +import RecoverySubTabs from "next-common/components/recovery/subTabs"; + +export default function InheritorsContent() { + return ( +
+ +
+ Inheritants placeholder content. View recovery configurations where you + are designated as an inheritor. +
+
+ ); +} diff --git a/packages/next-common/components/recovery/myRecovery/index.jsx b/packages/next-common/components/recovery/myRecovery/index.jsx index a8cda49d29..a93c6c0840 100644 --- a/packages/next-common/components/recovery/myRecovery/index.jsx +++ b/packages/next-common/components/recovery/myRecovery/index.jsx @@ -3,12 +3,14 @@ import useRealAddress from "next-common/utils/hooks/useRealAddress"; import FriendGroupsSection from "./friendGroupsSection"; import RecoveryAttemptsSection from "./recoveryAttemptsSection"; +import RecoverySubTabs from "next-common/components/recovery/subTabs"; export default function MyRecoveryContent() { const address = useRealAddress(); return (
+
diff --git a/packages/next-common/components/recovery/subTabs.jsx b/packages/next-common/components/recovery/subTabs.jsx new file mode 100644 index 0000000000..6b10e2c507 --- /dev/null +++ b/packages/next-common/components/recovery/subTabs.jsx @@ -0,0 +1,42 @@ +"use client"; + +import Tabs from "next-common/components/tabs"; +import { Title } from "next-common/components/overview/account/styled"; + +function TabTitle({ active, children }) { + return ( + + {children} + + ); +} + +const SUB_TABS = [ + { + value: "my_recovery", + label: ({ active }) => My Recovery, + url: "/account/my-recovery", + }, + { + value: "help_recover", + label: ({ active }) => Help Others, + url: "/account/help-recover", + }, + { + value: "inherited", + label: ({ active }) => Inherited, + url: "/account/inherited", + }, +]; + +export default function RecoverySubTabs({ activeTab, className = "" }) { + return ( + + ); +} diff --git a/packages/next-common/utils/consts/menu/common.js b/packages/next-common/utils/consts/menu/common.js index 2aa27606ae..592f0fe799 100644 --- a/packages/next-common/utils/consts/menu/common.js +++ b/packages/next-common/utils/consts/menu/common.js @@ -27,7 +27,10 @@ export const accountMenu = { "/account/been-delegated", "/account/delegations", "/account/deposits", + "/account/help-recover", + "/account/inheritants", "/account/multisigs", + "/account/my-recovery", "/account/proxies", ], icon: , diff --git a/packages/next-common/utils/consts/menu/data.js b/packages/next-common/utils/consts/menu/data.js index 513038f54d..e050dc283c 100644 --- a/packages/next-common/utils/consts/menu/data.js +++ b/packages/next-common/utils/consts/menu/data.js @@ -1,10 +1,6 @@ import { MenuData } from "@osn/icons/subsquare"; -import getChainSettings from "../settings"; -import { CHAIN } from "next-common/utils/constants"; function getDataMenu() { - const { modules } = getChainSettings(CHAIN); - const children = [ { name: "Proxies", @@ -18,14 +14,6 @@ function getDataMenu() { }, ]; - if (modules?.recovery) { - children.push({ - name: "Recovery", - value: "recovery", - pathname: "/recovery", - }); - } - return { name: "Data", value: "data", diff --git a/packages/next-common/utils/consts/menu/index.js b/packages/next-common/utils/consts/menu/index.js index 8be9ae933c..e1cf81ebc7 100644 --- a/packages/next-common/utils/consts/menu/index.js +++ b/packages/next-common/utils/consts/menu/index.js @@ -69,15 +69,6 @@ export function getHomeMenu({ modules?.vesting && vesting, modules?.scheduler && scheduler, modules?.whitelist && whitelist, - modules?.recovery && { - name: "Recovery", - value: "recovery", - pathname: "/recovery/my-recovery", - extraMatchNavMenuActivePathnames: [ - "/recovery/help-others", - "/recovery/inheritants", - ], - }, (modules?.proxy || hasMultisig) && getDataMenu(), calendarMenu, votingSpace && votingMenu, diff --git a/packages/next/pages/account/help-recover.jsx b/packages/next/pages/account/help-recover.jsx new file mode 100644 index 0000000000..306d97b205 --- /dev/null +++ b/packages/next/pages/account/help-recover.jsx @@ -0,0 +1,17 @@ +import HelpOthersContent from "next-common/components/recovery/helpOthers"; +import AccountLayout from "next-common/components/layout/AccountLayout"; +import AccountSubTabs from "next-common/components/overview/account/subTabs"; +import { withCommonProps } from "next-common/lib"; + +export default function AccountHelpRecoverPage() { + return ( + +
+ + +
+
+ ); +} + +export const getServerSideProps = withCommonProps(); diff --git a/packages/next/pages/account/inherited.jsx b/packages/next/pages/account/inherited.jsx new file mode 100644 index 0000000000..275e5b0640 --- /dev/null +++ b/packages/next/pages/account/inherited.jsx @@ -0,0 +1,17 @@ +import InheritorsContent from "next-common/components/recovery/inheritors"; +import AccountLayout from "next-common/components/layout/AccountLayout"; +import AccountSubTabs from "next-common/components/overview/account/subTabs"; +import { withCommonProps } from "next-common/lib"; + +export default function AccountInheritantsPage() { + return ( + +
+ + +
+
+ ); +} + +export const getServerSideProps = withCommonProps(); diff --git a/packages/next/pages/account/my-recovery.jsx b/packages/next/pages/account/my-recovery.jsx new file mode 100644 index 0000000000..78d51142d6 --- /dev/null +++ b/packages/next/pages/account/my-recovery.jsx @@ -0,0 +1,17 @@ +import MyRecoveryContent from "next-common/components/recovery/myRecovery"; +import AccountLayout from "next-common/components/layout/AccountLayout"; +import AccountSubTabs from "next-common/components/overview/account/subTabs"; +import { withCommonProps } from "next-common/lib"; + +export default function AccountMyRecoveryPage() { + return ( + +
+ + +
+
+ ); +} + +export const getServerSideProps = withCommonProps(); diff --git a/packages/next/pages/recovery/help-others.jsx b/packages/next/pages/recovery/help-others.jsx deleted file mode 100644 index 17d6dfb8cb..0000000000 --- a/packages/next/pages/recovery/help-others.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import Recovery from "next-common/components/recovery"; -import NoWalletConnected from "next-common/components/assethubMigrationAssets/noWalletConnected"; -import { withCommonProps } from "next-common/lib"; -import useRealAddress from "next-common/utils/hooks/useRealAddress"; -import getChainSettings from "next-common/utils/consts/settings"; -import { CHAIN } from "next-common/utils/constants"; - -export default function HelpOthersPage() { - const realAddress = useRealAddress(); - - if (!realAddress) { - return ; - } - - return ; -} - -export const getServerSideProps = async (ctx) => { - const { modules } = getChainSettings(CHAIN); - if (!modules?.recovery) { - return { - notFound: true, - }; - } - - return withCommonProps(async () => { - return { - props: {}, - }; - })(ctx); -}; diff --git a/packages/next/pages/recovery/inheritants.jsx b/packages/next/pages/recovery/inheritants.jsx deleted file mode 100644 index 94625c7169..0000000000 --- a/packages/next/pages/recovery/inheritants.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import Recovery from "next-common/components/recovery"; -import NoWalletConnected from "next-common/components/assethubMigrationAssets/noWalletConnected"; -import { withCommonProps } from "next-common/lib"; -import useRealAddress from "next-common/utils/hooks/useRealAddress"; -import getChainSettings from "next-common/utils/consts/settings"; -import { CHAIN } from "next-common/utils/constants"; - -export default function InheritorsPage() { - const realAddress = useRealAddress(); - - if (!realAddress) { - return ; - } - - return ; -} - -export const getServerSideProps = async (ctx) => { - const { modules } = getChainSettings(CHAIN); - if (!modules?.recovery) { - return { - notFound: true, - }; - } - - return withCommonProps(async () => { - return { - props: {}, - }; - })(ctx); -}; diff --git a/packages/next/pages/recovery/my-recovery.jsx b/packages/next/pages/recovery/my-recovery.jsx deleted file mode 100644 index 1338e411fe..0000000000 --- a/packages/next/pages/recovery/my-recovery.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import Recovery from "next-common/components/recovery"; -import NoWalletConnected from "next-common/components/assethubMigrationAssets/noWalletConnected"; -import { withCommonProps } from "next-common/lib"; -import useRealAddress from "next-common/utils/hooks/useRealAddress"; -import getChainSettings from "next-common/utils/consts/settings"; -import { CHAIN } from "next-common/utils/constants"; - -export default function MyRecoveryPage() { - const realAddress = useRealAddress(); - - if (!realAddress) { - return ; - } - - return ; -} - -export const getServerSideProps = async (ctx) => { - const { modules } = getChainSettings(CHAIN); - if (!modules?.recovery) { - return { - notFound: true, - }; - } - - return withCommonProps(async () => { - return { - props: {}, - }; - })(ctx); -}; From 7bdd1c420442c4767c62bf173dea8623ae8c0636 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 14:36:27 +0800 Subject: [PATCH 19/50] Refresh data after extrinsic inblock, #6919 --- .../myRecovery/addFriendGroupDialog.jsx | 15 +- .../recovery/myRecovery/attemptsTable.jsx | 203 +----------------- .../myRecovery/blockNumberWithTooltip.jsx | 34 +++ .../myRecovery/editFriendGroupDialog.jsx | 16 +- .../myRecovery/friendGroupsSection.jsx | 9 +- .../myRecovery/hooks/useMyFriendGroups.js | 9 +- .../myRecovery/hooks/useMyRecoveryAttempts.js | 9 +- .../hooks/useRecoveryAttemptsTableColumns.jsx | 166 ++++++++++++++ .../myRecovery/recoveryAttemptsSection.jsx | 8 +- .../myRecovery/removeFriendGroupDialog.jsx | 8 +- .../myRecovery/slashAttemptDialog.jsx | 7 +- .../recovery/myRecovery/slashButton.jsx | 27 +++ 12 files changed, 293 insertions(+), 218 deletions(-) create mode 100644 packages/next-common/components/recovery/myRecovery/blockNumberWithTooltip.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/slashButton.jsx diff --git a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx index 077466e5e8..3751d21308 100644 --- a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx @@ -22,7 +22,7 @@ import { DelayField, } from "./friendGroupFields"; -function AddFriendGroupForm() { +function AddFriendGroupForm({ onInBlock = () => {} }) { const { onClose } = usePopupParams(); const extensionAccounts = useExtensionAccounts(); const api = useContextApi(); @@ -164,15 +164,22 @@ function AddFriendGroupForm() { - + ); } -export default function AddFriendGroupDialog({ onClose }) { +export default function AddFriendGroupDialog({ + onClose, + onInBlock = () => {}, +}) { return ( - + ); } diff --git a/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx b/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx index 84c079ae45..68d9c53e94 100644 --- a/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx +++ b/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx @@ -1,12 +1,5 @@ "use client"; -import AddressUser from "next-common/components/user/addressUser"; -import Tooltip from "next-common/components/tooltip"; -import { AddressesTooltip } from "next-common/components/multisigs/fields"; -import { useRelayChainApi } from "next-common/context/relayChain"; -import { useChainSettings } from "next-common/context/chain"; -import { estimateBlocksTime } from "next-common/utils"; -import useCall from "next-common/utils/hooks/useCall"; import { isNil } from "lodash-es"; import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; import { defaultPageSize } from "next-common/utils/constants"; @@ -16,197 +9,7 @@ import { MapDataList } from "next-common/components/dataList"; import ScrollerX from "next-common/components/styled/containers/scrollerX"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; -import SlashAttemptDialog from "./slashAttemptDialog"; - -function BlockNumberWithTooltip({ height }) { - const api = useRelayChainApi(); - const { blockTime } = useChainSettings(); - const { value: currentNumber } = useCall(api?.query?.system?.number, []); - const currentHeight = currentNumber?.toNumber(); - - if (isNil(height) || isNil(currentHeight)) { - return ( - - #{height?.toLocaleString() || 0} - - ); - } - - const diff = Math.max(0, currentHeight - height); - const estimatedTime = diff > 0 ? estimateBlocksTime(diff, blockTime) : null; - - return ( - - - #{height?.toLocaleString() || 0} - - - ); -} - -const desktopColumns = [ - { - name: "Group Index", - className: "w-[120px] text-left", - render: (item) => ( - - ) - } - > - - #{item.friendGroupIndex} - - - ), - }, - { - name: "Initiator", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Init Block", - className: "w-[180px] text-left", - render: (item) => , - }, - { - name: "Last Approval Block", - className: "w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Threshold / Approvals", - className: "w-[160px] text-left", - render: (item) => ( - 0 && ( - - ) - } - > - - {item.fgGroup && ( - - {item.fgGroup?.friendsNeeded || 0} /{" "} - - )} - {item.approvalsCount} - - - ), - }, - { - name: "Action", - className: "w-[100px] text-right", - render: (item) => , - }, -]; - -const mobileColumns = [ - { - name: "Group Index", - className: "text-right", - render: (item) => ( - - ) - } - > - - #{item.friendGroupIndex} - - - ), - }, - { - name: "Initiator", - className: "text-left", - render: (item) => , - }, - { - name: "Init Block", - className: "text-right", - render: (item) => , - }, - { - name: "Last Approval Block", - className: "text-right", - render: (item) => ( - - ), - }, - { - name: "Threshold / Approvals", - className: "text-right", - render: (item) => ( - 0 && ( - - ) - } - > - - {item.fgGroup && ( - - {item.fgGroup?.friendsNeeded || 0} /{" "} - - )} - {item.approvalsCount} - - - ), - }, - { - name: "Action", - className: "text-left", - render: (item) => , - }, -]; - -function SlashButton({ friendGroupIndex }) { - const [showDialog, setShowDialog] = useState(false); - - return ( - <> - {showDialog && ( - setShowDialog(false)} - friendGroupIndex={friendGroupIndex} - /> - )} - - - ); -} +import useRecoveryAttemptsTableColumns from "./hooks/useRecoveryAttemptsTableColumns"; function enhanceAttemptWithFriendGroup(attempt, friendGroups = []) { const fgList = friendGroups?.find((fg) => fg.account === attempt.lostAccount); @@ -218,12 +21,16 @@ export default function MyRecoveryAttemptsTable({ data, loading: isLoading, friendGroups, + onSlash = () => {}, }) { const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); const [totalCount, setTotalCount] = useState(0); const [loading, setLoading] = useState(true); + const { desktopColumns, mobileColumns } = + useRecoveryAttemptsTableColumns(onSlash); + const { page, component: pageComponent } = usePaginationComponent( totalCount, defaultPageSize, diff --git a/packages/next-common/components/recovery/myRecovery/blockNumberWithTooltip.jsx b/packages/next-common/components/recovery/myRecovery/blockNumberWithTooltip.jsx new file mode 100644 index 0000000000..94bbf7a6f0 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/blockNumberWithTooltip.jsx @@ -0,0 +1,34 @@ +"use client"; + +import Tooltip from "next-common/components/tooltip"; +import { useRelayChainApi } from "next-common/context/relayChain"; +import { useChainSettings } from "next-common/context/chain"; +import { estimateBlocksTime } from "next-common/utils"; +import useCall from "next-common/utils/hooks/useCall"; +import { isNil } from "lodash-es"; + +export default function BlockNumberWithTooltip({ height }) { + const api = useRelayChainApi(); + const { blockTime } = useChainSettings(); + const { value: currentNumber } = useCall(api?.query?.system?.number, []); + const currentHeight = currentNumber?.toNumber(); + + if (isNil(height) || isNil(currentHeight)) { + return ( + + #{height?.toLocaleString() || 0} + + ); + } + + const diff = Math.max(0, currentHeight - height); + const estimatedTime = diff > 0 ? estimateBlocksTime(diff, blockTime) : null; + + return ( + + + #{height?.toLocaleString() || 0} + + + ); +} diff --git a/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx index 3b091b44de..5f083d07a8 100644 --- a/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx @@ -22,7 +22,7 @@ import { DelayField, } from "./friendGroupFields"; -function EditFriendGroupForm() { +function EditFriendGroupForm({ onInBlock = () => {} }) { const { onClose, group } = usePopupParams(); const extensionAccounts = useExtensionAccounts(); const api = useContextApi(); @@ -178,15 +178,23 @@ function EditFriendGroupForm() { - + ); } -export default function EditFriendGroupDialog({ onClose, group }) { +export default function EditFriendGroupDialog({ + onClose, + group, + onInBlock = () => {}, +}) { return ( - + ); } diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 61b4733574..9a60e7d356 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -125,12 +125,15 @@ export default function FriendGroupsSection({ address }) { const [editingGroup, setEditingGroup] = useState(null); const [showRemoveDialog, setShowRemoveDialog] = useState(false); const [removingIndex, setRemovingIndex] = useState(null); - const { data, loading } = useMyFriendGroups(address); + const { data, loading, fetch } = useMyFriendGroups(address); return (
{showAddDialog && ( - setShowAddDialog(false)} /> + setShowAddDialog(false)} + onInBlock={fetch} + /> )} {showEditDialog && editingGroup && ( )} {showRemoveDialog && ( @@ -149,6 +153,7 @@ export default function FriendGroupsSection({ address }) { }} index={removingIndex} address={address} + onInBlock={fetch} /> )}
diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js index e78eb0a908..26106f086b 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js @@ -1,10 +1,13 @@ import { useContextApi } from "next-common/context/api"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; export default function useMyFriendGroups(address) { const api = useContextApi(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); useEffect(() => { if (!api || !address) { @@ -47,7 +50,7 @@ export default function useMyFriendGroups(address) { return () => { cancelled = true; }; - }, [api, address]); + }, [api, address, fetchCount]); - return { data, loading }; + return { data, loading, fetch }; } diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js index e2de5e335d..4b4b2e6905 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js @@ -1,5 +1,5 @@ import { useContextApi } from "next-common/context/api"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; function bitfieldToIndices(bitfield) { const indices = []; @@ -19,6 +19,9 @@ export default function useMyRecoveryAttempts(address) { const api = useContextApi(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); useEffect(() => { if (!api || !address) { @@ -104,7 +107,7 @@ export default function useMyRecoveryAttempts(address) { return () => { cancelled = true; }; - }, [api, address]); + }, [api, address, fetchCount]); - return { data, loading }; + return { data, loading, fetch }; } diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx b/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx new file mode 100644 index 0000000000..83199b9aa5 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx @@ -0,0 +1,166 @@ +"use client"; + +import { useMemo } from "react"; +import Tooltip from "next-common/components/tooltip"; +import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import AddressUser from "next-common/components/user/addressUser"; +import BlockNumberWithTooltip from "../blockNumberWithTooltip"; +import SlashButton from "../slashButton"; + +export default function useRecoveryAttemptsTableColumns(onSlash) { + return useMemo(() => { + const desktopColumns = [ + { + name: "Group Index", + className: "w-[120px] text-left", + render: (item) => ( + + ) + } + > + + #{item.friendGroupIndex} + + + ), + }, + { + name: "Initiator", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Init Block", + className: "w-[180px] text-left", + render: (item) => , + }, + { + name: "Last Approval Block", + className: "w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Threshold / Approvals", + className: "w-[160px] text-left", + render: (item) => ( + 0 && ( + + ) + } + > + + {item.fgGroup && ( + + {item.fgGroup?.friendsNeeded || 0} /{" "} + + )} + {item.approvalsCount} + + + ), + }, + { + name: "Action", + className: "w-[100px] text-right", + render: (item) => ( + + ), + }, + ]; + + const mobileColumns = [ + { + name: "Group Index", + className: "text-right", + render: (item) => ( + + ) + } + > + + #{item.friendGroupIndex} + + + ), + }, + { + name: "Initiator", + className: "text-left", + render: (item) => , + }, + { + name: "Init Block", + className: "text-right", + render: (item) => , + }, + { + name: "Last Approval Block", + className: "text-right", + render: (item) => ( + + ), + }, + { + name: "Threshold / Approvals", + className: "text-right", + render: (item) => ( + 0 && ( + + ) + } + > + + {item.fgGroup && ( + + {item.fgGroup?.friendsNeeded || 0} /{" "} + + )} + {item.approvalsCount} + + + ), + }, + { + name: "Action", + className: "text-left", + render: (item) => ( + + ), + }, + ]; + + return { desktopColumns, mobileColumns }; + }, [onSlash]); +} diff --git a/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx b/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx index ffa88392b0..ec3e4cea7d 100644 --- a/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx @@ -5,8 +5,11 @@ import useMyFriendGroups from "./hooks/useMyFriendGroups"; import MyRecoveryAttemptsTable from "./attemptsTable"; export default function RecoveryAttemptsSection({ address }) { - const { data: attempts, loading: attemptsLoading } = - useMyRecoveryAttempts(address); + const { + data: attempts, + loading: attemptsLoading, + fetch, + } = useMyRecoveryAttempts(address); const { data: friendGroups } = useMyFriendGroups(address); const friendGroupsFormatted = address @@ -25,6 +28,7 @@ export default function RecoveryAttemptsSection({ address }) { data={attempts} loading={attemptsLoading} friendGroups={friendGroupsFormatted} + onSlash={fetch} />
diff --git a/packages/next-common/components/recovery/myRecovery/removeFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/removeFriendGroupDialog.jsx index fe99531fb8..c843fb57b9 100644 --- a/packages/next-common/components/recovery/myRecovery/removeFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/removeFriendGroupDialog.jsx @@ -4,7 +4,12 @@ import React, { useCallback } from "react"; import SimpleTxPopup from "next-common/components/simpleTxPopup"; import { useContextApi } from "next-common/context/api"; -export default function RemoveFriendGroupDialog({ onClose, index, address }) { +export default function RemoveFriendGroupDialog({ + onClose, + index, + address, + onInBlock = () => {}, +}) { const api = useContextApi(); const getTxFunc = useCallback(async () => { @@ -22,6 +27,7 @@ export default function RemoveFriendGroupDialog({ onClose, index, address }) { title={`Remove Friend Group #${index}`} getTxFunc={getTxFunc} onClose={onClose} + onInBlock={onInBlock} /> ); } diff --git a/packages/next-common/components/recovery/myRecovery/slashAttemptDialog.jsx b/packages/next-common/components/recovery/myRecovery/slashAttemptDialog.jsx index 748306ea4b..29a7788cb9 100644 --- a/packages/next-common/components/recovery/myRecovery/slashAttemptDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/slashAttemptDialog.jsx @@ -4,7 +4,11 @@ import { useCallback } from "react"; import SimpleTxPopup from "next-common/components/simpleTxPopup"; import { useContextApi } from "next-common/context/api"; -export default function SlashAttemptDialog({ onClose, friendGroupIndex }) { +export default function SlashAttemptDialog({ + onClose, + friendGroupIndex, + onInBlock = () => {}, +}) { const api = useContextApi(); const getTxFunc = useCallback(async () => { @@ -16,6 +20,7 @@ export default function SlashAttemptDialog({ onClose, friendGroupIndex }) { title={`Slash Attempt #${friendGroupIndex}`} getTxFunc={getTxFunc} onClose={onClose} + onInBlock={onInBlock} /> ); } diff --git a/packages/next-common/components/recovery/myRecovery/slashButton.jsx b/packages/next-common/components/recovery/myRecovery/slashButton.jsx new file mode 100644 index 0000000000..88a923022c --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/slashButton.jsx @@ -0,0 +1,27 @@ +"use client"; + +import { useState } from "react"; +import SlashAttemptDialog from "./slashAttemptDialog"; + +export default function SlashButton({ friendGroupIndex, onSlash }) { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + {showDialog && ( + setShowDialog(false)} + friendGroupIndex={friendGroupIndex} + onInBlock={onSlash} + /> + )} + + + ); +} From ba9a045cad61e9c84b360a1e3c6b09f4036f03fa Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 15:31:28 +0800 Subject: [PATCH 20/50] Implement help others to recover account, #6919 --- .../helpOthers/approveAttemptDialog.jsx | 27 ++ .../helpOthers/cancelAttemptDialog.jsx | 27 ++ .../helpOthers/helpOthersAttemptsSection.jsx | 97 +++++++ .../helpOthers/hooks/useHelpOthersAttempts.js | 140 +++++++++ .../hooks/useHelpOthersAttemptsColumns.jsx | 269 ++++++++++++++++++ .../hooks/useHelpOthersFriendGroups.js | 82 ++++++ .../helpOthers/inFriendGroupsSection.jsx | 81 ++++++ .../components/recovery/helpOthers/index.jsx | 18 +- .../components/recovery/inheritors/index.jsx | 15 +- .../components/recovery/myRecovery/index.jsx | 13 +- 10 files changed, 752 insertions(+), 17 deletions(-) create mode 100644 packages/next-common/components/recovery/helpOthers/approveAttemptDialog.jsx create mode 100644 packages/next-common/components/recovery/helpOthers/cancelAttemptDialog.jsx create mode 100644 packages/next-common/components/recovery/helpOthers/helpOthersAttemptsSection.jsx create mode 100644 packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js create mode 100644 packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx create mode 100644 packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js create mode 100644 packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx diff --git a/packages/next-common/components/recovery/helpOthers/approveAttemptDialog.jsx b/packages/next-common/components/recovery/helpOthers/approveAttemptDialog.jsx new file mode 100644 index 0000000000..b8b10a5e7c --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/approveAttemptDialog.jsx @@ -0,0 +1,27 @@ +"use client"; + +import { useCallback } from "react"; +import SimpleTxPopup from "next-common/components/simpleTxPopup"; +import { useContextApi } from "next-common/context/api"; + +export default function ApproveAttemptDialog({ + onClose, + lostAccount, + friendGroupIndex, + onInBlock = () => {}, +}) { + const api = useContextApi(); + + const getTxFunc = useCallback(async () => { + return api.tx.recovery.approveAttempt(lostAccount, friendGroupIndex); + }, [api, lostAccount, friendGroupIndex]); + + return ( + + ); +} diff --git a/packages/next-common/components/recovery/helpOthers/cancelAttemptDialog.jsx b/packages/next-common/components/recovery/helpOthers/cancelAttemptDialog.jsx new file mode 100644 index 0000000000..12c6343ba1 --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/cancelAttemptDialog.jsx @@ -0,0 +1,27 @@ +"use client"; + +import { useCallback } from "react"; +import SimpleTxPopup from "next-common/components/simpleTxPopup"; +import { useContextApi } from "next-common/context/api"; + +export default function CancelAttemptDialog({ + onClose, + lostAccount, + friendGroupIndex, + onInBlock = () => {}, +}) { + const api = useContextApi(); + + const getTxFunc = useCallback(async () => { + return api.tx.recovery.cancelAttempt(lostAccount, friendGroupIndex); + }, [api, lostAccount, friendGroupIndex]); + + return ( + + ); +} diff --git a/packages/next-common/components/recovery/helpOthers/helpOthersAttemptsSection.jsx b/packages/next-common/components/recovery/helpOthers/helpOthersAttemptsSection.jsx new file mode 100644 index 0000000000..84736169af --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/helpOthersAttemptsSection.jsx @@ -0,0 +1,97 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; +import { isNil } from "lodash-es"; +import useHelpOthersAttempts from "./hooks/useHelpOthersAttempts"; +import useHelpOthersAttemptsColumns from "./hooks/useHelpOthersAttemptsColumns"; + +function enhanceAttemptWithFriendGroup(attempt, friendGroups = []) { + const fgList = friendGroups?.find((fg) => fg.account === attempt.lostAccount); + const fgGroup = fgList?.friendGroups?.[attempt.friendGroupIndex]; + return { ...attempt, fgGroup }; +} + +export default function HelpOthersAttemptsSection({ address }) { + const { + data: rawData, + loading: isLoading, + friendGroupsData, + fetch, + } = useHelpOthersAttempts(address); + const { desktopColumns, mobileColumns } = useHelpOthersAttemptsColumns( + address, + fetch, + ); + const [navCollapsed] = useNavCollapsed(); + const [dataList, setDataList] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + + const { page, component: pageComponent } = usePaginationComponent( + totalCount, + defaultPageSize, + ); + + const total = rawData?.length || 0; + + useEffect(() => { + setLoading(true); + }, [page]); + + useEffect(() => { + if (isLoading || isNil(rawData)) { + return; + } + + const enhanced = rawData.map((attempt) => + enhanceAttemptWithFriendGroup(attempt, friendGroupsData), + ); + + setTotalCount(total); + const startIndex = (page - 1) * defaultPageSize; + const endIndex = startIndex + defaultPageSize; + setDataList(enhanced?.slice(startIndex, endIndex)); + setLoading(false); + }, [rawData, isLoading, page, total, friendGroupsData]); + + return ( +
+
+ + Ongoing Recovery Attempts + +
+
+ + + + + + {total > 0 && pageComponent} + +
+
+ ); +} diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js new file mode 100644 index 0000000000..aebf9bd63c --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js @@ -0,0 +1,140 @@ +import { useContextApi } from "next-common/context/api"; +import { useCallback, useEffect, useState } from "react"; + +export default function useHelpOthersAttempts(address) { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [friendGroupsData, setFriendGroupsData] = useState([]); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); + + useEffect(() => { + if (!api || !address) { + return; + } + + if (!api?.query.recovery?.attempt) { + setLoading(false); + setData([]); + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.attempt + .entries() + .then(async (entries) => { + if (cancelled) return; + + // Collect unique lostAccounts to fetch their friend groups + const lostAccounts = [ + ...new Set(entries.map(([key]) => key.args?.[0]?.toString())), + ]; + const friendGroupsMap = {}; + + await Promise.all( + lostAccounts.map(async (account) => { + try { + const value = await api.query.recovery.friendGroups(account); + const json = value.toJSON(); + friendGroupsMap[account] = json?.[0] || []; + } catch { + friendGroupsMap[account] = []; + } + }), + ); + + // Build raw friend groups data for enhanceAttemptWithFriendGroup + const rawFriendGroups = Object.entries(friendGroupsMap).map( + ([account, groups]) => ({ + account, + friendGroups: groups.map((group, index) => ({ + index, + friends: group.friends || [], + friendsNeeded: parseInt(group.friendsNeeded) || 0, + inheritor: group.inheritor || "", + inheritancePriority: parseInt(group.inheritancePriority) || 0, + inheritanceDelay: parseInt(group.inheritanceDelay) || 0, + cancelDelay: parseInt(group.cancelDelay) || 0, + })), + }), + ); + + // Filter entries where current address is a friend of the lost account + const filtered = entries.filter(([storageKey]) => { + const lostAccount = storageKey.args?.[0]?.toString(); + const friendGroups = friendGroupsMap[lostAccount] || []; + return friendGroups.some((group) => + (group.friends || []).some( + (f) => f?.toLowerCase() === address?.toLowerCase(), + ), + ); + }); + + if (cancelled) return; + + const result = filtered.map(([storageKey, value]) => { + const lostAccount = storageKey.args?.[0]?.toString(); + const friendGroupIndex = storageKey.args?.[1]?.toNumber(); + const json = value.toJSON(); + const attempt = json?.[0] || {}; + + const approvalsBitfield = attempt.approvals || []; + // Simple bitfield to indices + const approvedIndices = []; + let bitIndex = 0; + for (const byte of approvalsBitfield) { + if (typeof byte === "number") { + for (let b = 0; b < 8; b++) { + if (byte & (1 << b)) approvedIndices.push(bitIndex + b); + } + } + bitIndex += 8; + } + + const approvalsCount = approvedIndices.length; + + const friendGroup = + (friendGroupsMap[lostAccount] || [])[friendGroupIndex] || {}; + const friends = friendGroup.friends || []; + const approvedAddresses = approvedIndices + .filter((i) => i < friends.length) + .map((i) => friends[i]); + + return { + lostAccount, + friendGroupIndex, + initiator: attempt.initiator || "", + initBlock: parseInt(attempt.initBlock) || 0, + lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, + approvalsCount, + approvedAddresses, + friendsNeeded: friendGroup.friendsNeeded || 0, + }; + }); + + if (!cancelled) { + setData(result); + setFriendGroupsData(rawFriendGroups); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query help others attempts", error); + if (!cancelled) { + setData([]); + setFriendGroupsData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, address, fetchCount]); + + return { data, loading, friendGroupsData, fetch }; +} diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx new file mode 100644 index 0000000000..a5a90775d5 --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx @@ -0,0 +1,269 @@ +"use client"; + +import { useMemo, useState } from "react"; +import Tooltip from "next-common/components/tooltip"; +import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import AddressUser from "next-common/components/user/addressUser"; +import { isSameAddress } from "next-common/utils"; +import BlockNumberWithTooltip from "../../myRecovery/blockNumberWithTooltip"; +import CancelAttemptDialog from "../cancelAttemptDialog"; +import ApproveAttemptDialog from "../approveAttemptDialog"; + +function CancelButton({ lostAccount, friendGroupIndex, onCancel }) { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + {showDialog && ( + setShowDialog(false)} + lostAccount={lostAccount} + friendGroupIndex={friendGroupIndex} + onInBlock={onCancel} + /> + )} + + + ); +} + +function ApproveButton({ + lostAccount, + friendGroupIndex, + onApprove, + alreadyApproved, +}) { + const [showDialog, setShowDialog] = useState(false); + + if (alreadyApproved) { + return ( + + + Approve + + + ); + } + + return ( + <> + {showDialog && ( + setShowDialog(false)} + lostAccount={lostAccount} + friendGroupIndex={friendGroupIndex} + onInBlock={onApprove} + /> + )} + + + ); +} + +export default function useHelpOthersAttemptsColumns(address, onAction) { + return useMemo(() => { + const desktopColumns = [ + { + name: "Lost Account", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Group Index", + className: "w-[120px] text-left", + render: (item) => ( + + ) + } + > + + #{item.friendGroupIndex} + + + ), + }, + { + name: "Initiator", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Init Block", + className: "w-[180px] text-left", + render: (item) => , + }, + { + name: "Last Approval Block", + className: "w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Threshold / Approvals", + className: "w-[160px] text-left", + render: (item) => ( + 0 && ( + + ) + } + > + + {item.fgGroup && ( + + {item.fgGroup?.friendsNeeded || 0} /{" "} + + )} + {item.approvalsCount} + + + ), + }, + { + name: "Action", + className: "w-[100px] text-right", + render: (item) => + isSameAddress(item.initiator, address) ? ( + + ) : ( + + isSameAddress(a, address), + )} + /> + ), + }, + ]; + + const mobileColumns = [ + { + name: "Lost Account", + className: "text-left", + render: (item) => , + }, + { + name: "Group Index", + className: "text-right", + render: (item) => ( + + ) + } + > + + #{item.friendGroupIndex} + + + ), + }, + { + name: "Initiator", + className: "text-left", + render: (item) => , + }, + { + name: "Init Block", + className: "text-right", + render: (item) => , + }, + { + name: "Last Approval Block", + className: "text-right", + render: (item) => ( + + ), + }, + { + name: "Threshold / Approvals", + className: "text-right", + render: (item) => ( + 0 && ( + + ) + } + > + + {item.fgGroup && ( + + {item.fgGroup?.friendsNeeded || 0} /{" "} + + )} + {item.approvalsCount} + + + ), + }, + { + name: "Action", + className: "text-left", + render: (item) => + isSameAddress(item.initiator, address) ? ( + + ) : ( + + isSameAddress(a, address), + )} + /> + ), + }, + ]; + + return { desktopColumns, mobileColumns }; + }, [address, onAction]); +} diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js new file mode 100644 index 0000000000..ce41178692 --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js @@ -0,0 +1,82 @@ +import { useContextApi } from "next-common/context/api"; +import { useCallback, useEffect, useState } from "react"; +import { flattenRecoveryData } from "next-common/components/data/recovery/hooks/useQueryAllRecoveryData"; + +export default function useHelpOthersFriendGroups(address) { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); + + useEffect(() => { + if (!api || !address) { + return; + } + + if (!api?.query.recovery?.friendGroups) { + setLoading(false); + setData([]); + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.friendGroups + .entries() + .then((entries) => { + if (cancelled) return; + + const result = entries.map(([storageKey, value]) => { + const account = storageKey.args?.[0]?.toString(); + const jsonValue = value.toJSON(); + const friendGroupVec = Array.isArray(jsonValue?.[0]) + ? jsonValue[0] + : []; + + return { + account, + friendGroups: friendGroupVec.map((group, index) => ({ + index, + friends: group.friends || [], + friendsNeeded: parseInt(group.friendsNeeded) || 0, + inheritor: group.inheritor || "", + inheritancePriority: parseInt(group.inheritancePriority) || 0, + inheritanceDelay: parseInt(group.inheritanceDelay) || 0, + cancelDelay: parseInt(group.cancelDelay) || 0, + })), + }; + }); + + // Filter to entries where current address is a friend + const filtered = result.filter((entry) => + entry.friendGroups.some((g) => + g.friends.some((f) => f?.toLowerCase() === address?.toLowerCase()), + ), + ); + + // Flatten for table display + const flattened = flattenRecoveryData(filtered); + + if (!cancelled) { + setData(flattened); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query help others friend groups", error); + if (!cancelled) { + setData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, address, fetchCount]); + + return { data, loading, fetch }; +} diff --git a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx new file mode 100644 index 0000000000..c196e507bf --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx @@ -0,0 +1,81 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import { + desktopColumns, + mobileColumns, +} from "next-common/components/data/recovery/table/columns"; +import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; +import { isNil } from "lodash-es"; +import useHelpOthersFriendGroups from "./hooks/useHelpOthersFriendGroups"; + +export default function InFriendGroupsSection({ address }) { + const { data, loading: isLoading } = useHelpOthersFriendGroups(address); + const [navCollapsed] = useNavCollapsed(); + const [dataList, setDataList] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + + const { page, component: pageComponent } = usePaginationComponent( + totalCount, + defaultPageSize, + ); + + const total = data?.length || 0; + + useEffect(() => { + setLoading(true); + }, [page]); + + useEffect(() => { + if (isLoading || isNil(data)) { + return; + } + + setTotalCount(total); + const startIndex = (page - 1) * defaultPageSize; + const endIndex = startIndex + defaultPageSize; + setDataList(data?.slice(startIndex, endIndex)); + setLoading(false); + }, [data, isLoading, page, total]); + + return ( +
+
+ + In Friend Groups + +
+
+ + + + + + {total > 0 && pageComponent} + +
+
+ ); +} diff --git a/packages/next-common/components/recovery/helpOthers/index.jsx b/packages/next-common/components/recovery/helpOthers/index.jsx index 8a6fe431cd..a1c74a1378 100644 --- a/packages/next-common/components/recovery/helpOthers/index.jsx +++ b/packages/next-common/components/recovery/helpOthers/index.jsx @@ -1,15 +1,21 @@ "use client"; +import useRealAddress from "next-common/utils/hooks/useRealAddress"; import RecoverySubTabs from "next-common/components/recovery/subTabs"; +import InFriendGroupsSection from "./inFriendGroupsSection"; +import HelpOthersAttemptsSection from "./helpOthersAttemptsSection"; +import { RelayChainApiProvider } from "next-common/context/relayChain"; export default function HelpOthersContent() { + const address = useRealAddress(); + return ( -
- -
- Help Others placeholder content. View recovery requests from others - where you are a friend. + +
+ + +
-
+ ); } diff --git a/packages/next-common/components/recovery/inheritors/index.jsx b/packages/next-common/components/recovery/inheritors/index.jsx index 269bb78321..20b488f2f4 100644 --- a/packages/next-common/components/recovery/inheritors/index.jsx +++ b/packages/next-common/components/recovery/inheritors/index.jsx @@ -1,15 +1,18 @@ "use client"; import RecoverySubTabs from "next-common/components/recovery/subTabs"; +import { RelayChainApiProvider } from "next-common/context/relayChain"; export default function InheritorsContent() { return ( -
- -
- Inheritants placeholder content. View recovery configurations where you - are designated as an inheritor. + +
+ +
+ Inheritants placeholder content. View recovery configurations where + you are designated as an inheritor. +
-
+ ); } diff --git a/packages/next-common/components/recovery/myRecovery/index.jsx b/packages/next-common/components/recovery/myRecovery/index.jsx index a93c6c0840..e05f47f585 100644 --- a/packages/next-common/components/recovery/myRecovery/index.jsx +++ b/packages/next-common/components/recovery/myRecovery/index.jsx @@ -4,15 +4,18 @@ import useRealAddress from "next-common/utils/hooks/useRealAddress"; import FriendGroupsSection from "./friendGroupsSection"; import RecoveryAttemptsSection from "./recoveryAttemptsSection"; import RecoverySubTabs from "next-common/components/recovery/subTabs"; +import { RelayChainApiProvider } from "next-common/context/relayChain"; export default function MyRecoveryContent() { const address = useRealAddress(); return ( -
- - - -
+ +
+ + + +
+
); } From 97182ca7f30aba4feb1d82369a9f41596bc74213 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 15:56:15 +0800 Subject: [PATCH 21/50] Add inherited page, #6919 --- .../helpOthers/finishAttemptDialog.jsx | 27 ++++++ .../hooks/useHelpOthersAttemptsColumns.jsx | 67 +++++++++++++-- .../components/recovery/helpOthers/index.jsx | 2 +- .../inheritors/hooks/useInheritedAccounts.js | 73 +++++++++++++++++ .../hooks/useInheritorFriendGroups.js | 82 +++++++++++++++++++ .../components/recovery/inheritors/index.jsx | 11 ++- .../inheritors/inheritedAccountsSection.jsx | 81 ++++++++++++++++++ .../inheritorFriendGroupsSection.jsx | 81 ++++++++++++++++++ 8 files changed, 413 insertions(+), 11 deletions(-) create mode 100644 packages/next-common/components/recovery/helpOthers/finishAttemptDialog.jsx create mode 100644 packages/next-common/components/recovery/inheritors/hooks/useInheritedAccounts.js create mode 100644 packages/next-common/components/recovery/inheritors/hooks/useInheritorFriendGroups.js create mode 100644 packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx create mode 100644 packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx diff --git a/packages/next-common/components/recovery/helpOthers/finishAttemptDialog.jsx b/packages/next-common/components/recovery/helpOthers/finishAttemptDialog.jsx new file mode 100644 index 0000000000..f7180c7c71 --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/finishAttemptDialog.jsx @@ -0,0 +1,27 @@ +"use client"; + +import { useCallback } from "react"; +import SimpleTxPopup from "next-common/components/simpleTxPopup"; +import { useContextApi } from "next-common/context/api"; + +export default function FinishAttemptDialog({ + onClose, + lostAccount, + friendGroupIndex, + onInBlock = () => {}, +}) { + const api = useContextApi(); + + const getTxFunc = useCallback(async () => { + return api.tx.recovery.finishAttempt(lostAccount, friendGroupIndex); + }, [api, lostAccount, friendGroupIndex]); + + return ( + + ); +} diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx index a5a90775d5..c48ace514b 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx @@ -8,6 +8,7 @@ import { isSameAddress } from "next-common/utils"; import BlockNumberWithTooltip from "../../myRecovery/blockNumberWithTooltip"; import CancelAttemptDialog from "../cancelAttemptDialog"; import ApproveAttemptDialog from "../approveAttemptDialog"; +import FinishAttemptDialog from "../finishAttemptDialog"; function CancelButton({ lostAccount, friendGroupIndex, onCancel }) { const [showDialog, setShowDialog] = useState(false); @@ -72,6 +73,30 @@ function ApproveButton({ ); } +function FinishButton({ lostAccount, friendGroupIndex, onFinish }) { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + {showDialog && ( + setShowDialog(false)} + lostAccount={lostAccount} + friendGroupIndex={friendGroupIndex} + onInBlock={onFinish} + /> + )} + + + ); +} + export default function useHelpOthersAttemptsColumns(address, onAction) { return useMemo(() => { const desktopColumns = [ @@ -153,8 +178,22 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { { name: "Action", className: "w-[100px] text-right", - render: (item) => - isSameAddress(item.initiator, address) ? ( + render: (item) => { + const threshold = + item.fgGroup?.friendsNeeded || item.friendsNeeded || 0; + const canFinish = (item.approvalsCount || 0) >= threshold; + + if (canFinish) { + return ( + + ); + } + + return isSameAddress(item.initiator, address) ? ( - ), + ); + }, }, ]; @@ -244,8 +284,22 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { { name: "Action", className: "text-left", - render: (item) => - isSameAddress(item.initiator, address) ? ( + render: (item) => { + const threshold = + item.fgGroup?.friendsNeeded || item.friendsNeeded || 0; + const canFinish = (item.approvalsCount || 0) >= threshold; + + if (canFinish) { + return ( + + ); + } + + return isSameAddress(item.initiator, address) ? ( - ), + ); + }, }, ]; diff --git a/packages/next-common/components/recovery/helpOthers/index.jsx b/packages/next-common/components/recovery/helpOthers/index.jsx index a1c74a1378..1344591e42 100644 --- a/packages/next-common/components/recovery/helpOthers/index.jsx +++ b/packages/next-common/components/recovery/helpOthers/index.jsx @@ -13,8 +13,8 @@ export default function HelpOthersContent() {
- +
); diff --git a/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccounts.js b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccounts.js new file mode 100644 index 0000000000..f77bdde495 --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccounts.js @@ -0,0 +1,73 @@ +import { useContextApi } from "next-common/context/api"; +import { useCallback, useEffect, useState } from "react"; + +export default function useInheritedAccounts(address) { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); + + useEffect(() => { + if (!api || !address) { + return; + } + + if (!api?.query.recovery?.inheritor) { + setLoading(false); + setData([]); + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.inheritor + .entries() + .then((entries) => { + if (cancelled) return; + + const result = entries + .map(([storageKey, value]) => { + const account = storageKey.args?.[0]?.toString(); + const jsonValue = value.toJSON(); + + const inheritancePriority = parseInt(jsonValue?.[0]) || 0; + const inheritor = jsonValue?.[1] || ""; + const consideration = jsonValue?.[2] || {}; + const depositor = consideration.depositor || ""; + const ticket = parseInt(consideration.ticket) || 0; + + return { + account, + inheritancePriority, + inheritor, + depositor, + ticket, + }; + }) + .filter( + (item) => item.inheritor?.toLowerCase() === address?.toLowerCase(), + ); + + if (!cancelled) { + setData(result); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query inherited accounts", error); + if (!cancelled) { + setData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, address, fetchCount]); + + return { data, loading, fetch }; +} diff --git a/packages/next-common/components/recovery/inheritors/hooks/useInheritorFriendGroups.js b/packages/next-common/components/recovery/inheritors/hooks/useInheritorFriendGroups.js new file mode 100644 index 0000000000..f88907fbe3 --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/hooks/useInheritorFriendGroups.js @@ -0,0 +1,82 @@ +import { useContextApi } from "next-common/context/api"; +import { useCallback, useEffect, useState } from "react"; +import { flattenRecoveryData } from "next-common/components/data/recovery/hooks/useQueryAllRecoveryData"; + +export default function useInheritorFriendGroups(address) { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); + + useEffect(() => { + if (!api || !address) { + return; + } + + if (!api?.query.recovery?.friendGroups) { + setLoading(false); + setData([]); + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.friendGroups + .entries() + .then((entries) => { + if (cancelled) return; + + const result = entries.map(([storageKey, value]) => { + const account = storageKey.args?.[0]?.toString(); + const jsonValue = value.toJSON(); + const friendGroupVec = Array.isArray(jsonValue?.[0]) + ? jsonValue[0] + : []; + + return { + account, + friendGroups: friendGroupVec.map((group, index) => ({ + index, + friends: group.friends || [], + friendsNeeded: parseInt(group.friendsNeeded) || 0, + inheritor: group.inheritor || "", + inheritancePriority: parseInt(group.inheritancePriority) || 0, + inheritanceDelay: parseInt(group.inheritanceDelay) || 0, + cancelDelay: parseInt(group.cancelDelay) || 0, + })), + }; + }); + + // Filter to entries where current address is an inheritor + const filtered = result.filter((entry) => + entry.friendGroups.some( + (g) => g.inheritor?.toLowerCase() === address?.toLowerCase(), + ), + ); + + // Flatten for table display + const flattened = flattenRecoveryData(filtered); + + if (!cancelled) { + setData(flattened); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query inheritor friend groups", error); + if (!cancelled) { + setData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, address, fetchCount]); + + return { data, loading, fetch }; +} diff --git a/packages/next-common/components/recovery/inheritors/index.jsx b/packages/next-common/components/recovery/inheritors/index.jsx index 20b488f2f4..ad749472f9 100644 --- a/packages/next-common/components/recovery/inheritors/index.jsx +++ b/packages/next-common/components/recovery/inheritors/index.jsx @@ -1,17 +1,20 @@ "use client"; +import useRealAddress from "next-common/utils/hooks/useRealAddress"; import RecoverySubTabs from "next-common/components/recovery/subTabs"; import { RelayChainApiProvider } from "next-common/context/relayChain"; +import InheritedAccountsSection from "./inheritedAccountsSection"; +import InheritorFriendGroupsSection from "./inheritorFriendGroupsSection"; export default function InheritorsContent() { + const address = useRealAddress(); + return (
-
- Inheritants placeholder content. View recovery configurations where - you are designated as an inheritor. -
+ +
); diff --git a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx new file mode 100644 index 0000000000..8f85e36b2f --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx @@ -0,0 +1,81 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import { + desktopColumns, + mobileColumns, +} from "next-common/components/data/recovery/table/inheritorsColumns"; +import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; +import { isNil } from "lodash-es"; +import useInheritedAccounts from "./hooks/useInheritedAccounts"; + +export default function InheritedAccountsSection({ address }) { + const { data, loading: isLoading } = useInheritedAccounts(address); + const [navCollapsed] = useNavCollapsed(); + const [dataList, setDataList] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + + const { page, component: pageComponent } = usePaginationComponent( + totalCount, + defaultPageSize, + ); + + const total = data?.length || 0; + + useEffect(() => { + setLoading(true); + }, [page]); + + useEffect(() => { + if (isLoading || isNil(data)) { + return; + } + + setTotalCount(total); + const startIndex = (page - 1) * defaultPageSize; + const endIndex = startIndex + defaultPageSize; + setDataList(data?.slice(startIndex, endIndex)); + setLoading(false); + }, [data, isLoading, page, total]); + + return ( +
+
+ + Inherited Lost Accounts + +
+
+ + + + + + {total > 0 && pageComponent} + +
+
+ ); +} diff --git a/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx new file mode 100644 index 0000000000..f5cc45fda5 --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx @@ -0,0 +1,81 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import { + desktopColumns, + mobileColumns, +} from "next-common/components/data/recovery/table/columns"; +import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; +import { isNil } from "lodash-es"; +import useInheritorFriendGroups from "./hooks/useInheritorFriendGroups"; + +export default function InheritorFriendGroupsSection({ address }) { + const { data, loading: isLoading } = useInheritorFriendGroups(address); + const [navCollapsed] = useNavCollapsed(); + const [dataList, setDataList] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + + const { page, component: pageComponent } = usePaginationComponent( + totalCount, + defaultPageSize, + ); + + const total = data?.length || 0; + + useEffect(() => { + setLoading(true); + }, [page]); + + useEffect(() => { + if (isLoading || isNil(data)) { + return; + } + + setTotalCount(total); + const startIndex = (page - 1) * defaultPageSize; + const endIndex = startIndex + defaultPageSize; + setDataList(data?.slice(startIndex, endIndex)); + setLoading(false); + }, [data, isLoading, page, total]); + + return ( +
+
+ + In Friend Groups + +
+
+ + + + + + {total > 0 && pageComponent} + +
+
+ ); +} From 97dc2783a900497429a1687e480be02f8549df09 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 16:43:08 +0800 Subject: [PATCH 22/50] fix: sort address, #6919 --- .../recovery/inheritors/inheritedAccountsSection.jsx | 2 +- .../recovery/myRecovery/addFriendGroupDialog.jsx | 7 ++++++- .../recovery/myRecovery/editFriendGroupDialog.jsx | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx index 8f85e36b2f..4cd764d583 100644 --- a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx +++ b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx @@ -49,7 +49,7 @@ export default function InheritedAccountsSection({ address }) {
- Inherited Lost Accounts + Inherited Accounts
diff --git a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx index 3751d21308..ca59668317 100644 --- a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx @@ -14,6 +14,7 @@ import AdvanceSettings from "next-common/components/summary/newProposalQuickStar import EstimatedGas from "next-common/components/estimatedGas"; import { useContextApi } from "next-common/context/api"; import { isSameAddress } from "next-common/utils"; +import { sortAddresses } from "@polkadot/util-crypto"; import { PriorityField, FriendsField, @@ -95,12 +96,16 @@ function AddFriendGroupForm({ onInBlock = () => {} }) { throw new Error("Please enter a valid cancel delay"); } + const sortedFriends = sortAddresses(validFriends).filter( + (addr, i, arr) => i === 0 || addr !== arr[i - 1], + ); + const raw = await api.query.recovery.friendGroups(address); const json = raw.toJSON(); const currentGroups = Array.isArray(json?.[0]) ? json[0] : []; const newGroup = { - friends: validFriends, + friends: sortedFriends, friendsNeeded: thresholdNum, inheritor, inheritancePriority: priorityNum, diff --git a/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx index 5f083d07a8..e6a0743edc 100644 --- a/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx @@ -14,6 +14,7 @@ import AdvanceSettings from "next-common/components/summary/newProposalQuickStar import EstimatedGas from "next-common/components/estimatedGas"; import { useContextApi } from "next-common/context/api"; import { isSameAddress } from "next-common/utils"; +import { sortAddresses } from "@polkadot/util-crypto"; import { PriorityField, FriendsField, @@ -103,6 +104,10 @@ function EditFriendGroupForm({ onInBlock = () => {} }) { throw new Error("Please enter a valid cancel delay"); } + const sortedFriends = sortAddresses(validFriends).filter( + (addr, i, arr) => i === 0 || addr !== arr[i - 1], + ); + const raw = await api.query.recovery.friendGroups(address); const json = raw.toJSON(); const currentGroups = Array.isArray(json?.[0]) ? json[0] : []; @@ -110,7 +115,7 @@ function EditFriendGroupForm({ onInBlock = () => {} }) { const updatedGroups = currentGroups.map((g, idx) => { if (idx === group.index) { return { - friends: validFriends, + friends: sortedFriends, friendsNeeded: thresholdNum, inheritor, inheritancePriority: priorityNum, From a2f12396ef4150d828b26c272dfdde4410aa6551 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 16:49:41 +0800 Subject: [PATCH 23/50] fix, #6919 --- .../components/recovery/myRecovery/addFriendGroupDialog.jsx | 5 +++-- .../components/recovery/myRecovery/editFriendGroupDialog.jsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx index ca59668317..1284793727 100644 --- a/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/addFriendGroupDialog.jsx @@ -57,7 +57,7 @@ function AddFriendGroupForm({ onInBlock = () => {} }) { setFriends(next); }; - const validFriendsCount = friends.filter(Boolean).length; + const validFriendsCount = new Set(friends.filter(Boolean)).size; const getTxFunc = useCallback(async () => { const validFriends = friends.filter(Boolean); @@ -73,7 +73,8 @@ function AddFriendGroupForm({ onInBlock = () => {} }) { if (!thresholdNum || thresholdNum < 1) { throw new Error("Please enter a valid threshold"); } - if (thresholdNum > validFriends.length) { + const uniqueFriendsCount = new Set(validFriends).size; + if (thresholdNum > uniqueFriendsCount) { throw new Error("Threshold cannot exceed the number of friends"); } diff --git a/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx b/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx index e6a0743edc..e3e0ffd0b3 100644 --- a/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx +++ b/packages/next-common/components/recovery/myRecovery/editFriendGroupDialog.jsx @@ -65,7 +65,7 @@ function EditFriendGroupForm({ onInBlock = () => {} }) { setFriends(next); }; - const validFriendsCount = friends.filter(Boolean).length; + const validFriendsCount = new Set(friends.filter(Boolean)).size; const getTxFunc = useCallback(async () => { const validFriends = friends.filter(Boolean); @@ -81,7 +81,8 @@ function EditFriendGroupForm({ onInBlock = () => {} }) { if (!thresholdNum || thresholdNum < 1) { throw new Error("Please enter a valid threshold"); } - if (thresholdNum > validFriends.length) { + const uniqueFriendsCount = new Set(validFriends).size; + if (thresholdNum > uniqueFriendsCount) { throw new Error("Threshold cannot exceed the number of friends"); } From 9befa22bcbbc2df0658039eaf45f3233efe54b9f Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 17:48:25 +0800 Subject: [PATCH 24/50] Supper revoke inheritor and start attempt, #6919 --- .../helpOthers/helpOthersAttemptsSection.jsx | 17 +- .../useHelpOthersFriendGroupsColumns.jsx | 187 ++++++++++++++++++ .../helpOthers/inFriendGroupsSection.jsx | 15 +- .../components/recovery/helpOthers/index.jsx | 27 ++- .../helpOthers/initiateAttemptDialog.jsx | 27 +++ .../myRecovery/hooks/useMyInheritor.js | 73 +++++++ .../hooks/useMyRecoveryInheritorColumns.jsx | 113 +++++++++++ .../components/recovery/myRecovery/index.jsx | 2 + .../recovery/myRecovery/inheritorSection.jsx | 80 ++++++++ .../myRecovery/revokeInheritorDialog.jsx | 25 +++ 10 files changed, 548 insertions(+), 18 deletions(-) create mode 100644 packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx create mode 100644 packages/next-common/components/recovery/helpOthers/initiateAttemptDialog.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/hooks/useMyInheritor.js create mode 100644 packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/inheritorSection.jsx create mode 100644 packages/next-common/components/recovery/myRecovery/revokeInheritorDialog.jsx diff --git a/packages/next-common/components/recovery/helpOthers/helpOthersAttemptsSection.jsx b/packages/next-common/components/recovery/helpOthers/helpOthersAttemptsSection.jsx index 84736169af..ca18de6556 100644 --- a/packages/next-common/components/recovery/helpOthers/helpOthersAttemptsSection.jsx +++ b/packages/next-common/components/recovery/helpOthers/helpOthersAttemptsSection.jsx @@ -9,7 +9,6 @@ import { defaultPageSize } from "next-common/utils/constants"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; -import useHelpOthersAttempts from "./hooks/useHelpOthersAttempts"; import useHelpOthersAttemptsColumns from "./hooks/useHelpOthersAttemptsColumns"; function enhanceAttemptWithFriendGroup(attempt, friendGroups = []) { @@ -18,16 +17,16 @@ function enhanceAttemptWithFriendGroup(attempt, friendGroups = []) { return { ...attempt, fgGroup }; } -export default function HelpOthersAttemptsSection({ address }) { - const { - data: rawData, - loading: isLoading, - friendGroupsData, - fetch, - } = useHelpOthersAttempts(address); +export default function HelpOthersAttemptsSection({ + address, + data: rawData, + loading: isLoading, + friendGroupsData, + onRefresh, +}) { const { desktopColumns, mobileColumns } = useHelpOthersAttemptsColumns( address, - fetch, + onRefresh, ); const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx new file mode 100644 index 0000000000..34cf8fe0ea --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx @@ -0,0 +1,187 @@ +"use client"; + +import { useMemo, useState } from "react"; +import AddressUser from "next-common/components/user/addressUser"; +import Tooltip from "next-common/components/tooltip"; +import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import DelayBlock from "next-common/components/recovery/delayBlock"; +import { isNil } from "lodash-es"; +import InitiateAttemptDialog from "../initiateAttemptDialog"; + +function FriendsCount({ friends = [] }) { + if (isNil(friends)) { + return null; + } + + return ( + } + > + + {friends?.length || 0} + + + ); +} + +function RecoverButton({ lostAccount, friendGroupIndex, onRecover }) { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + {showDialog && ( + setShowDialog(false)} + lostAccount={lostAccount} + friendGroupIndex={friendGroupIndex} + onInBlock={onRecover} + /> + )} + + + ); +} + +export default function useHelpOthersFriendGroupsColumns(onRecover) { + return useMemo(() => { + const desktopColumns = [ + { + name: "Account", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Group Index", + className: "w-[120px] text-left", + render: (item) => ( + #{item.index} + ), + }, + { + name: "Priority", + className: "w-[100px] text-left", + render: (item) => ( + + {item.inheritancePriority} + + ), + }, + { + name: "Friends", + className: "w-[100px] text-left", + render: (item) => , + }, + { + name: "Threshold", + className: "w-[120px] text-left", + render: (item) => ( + + {item.friendsNeeded} + + ), + }, + { + name: "Inheritor", + className: "min-w-[160px] text-left", + render: (item) => ( + + ), + }, + { + name: "Inheritance Delay", + className: "w-[180px] text-left", + render: (item) => , + }, + { + name: "Cancel Delay", + className: "w-[160px] text-left", + render: (item) => , + }, + { + name: "Action", + className: "w-[100px] text-right", + render: (item) => ( + + ), + }, + ]; + + const mobileColumns = [ + { + name: "Account", + className: "text-left", + render: (item) => , + }, + { + name: "Group Index", + className: "text-right", + render: (item) => ( + #{item.index} + ), + }, + { + name: "Priority", + className: "text-right", + render: (item) => ( + + {item.inheritancePriority} + + ), + }, + { + name: "Friends", + className: "text-left", + render: (item) => , + }, + { + name: "Threshold", + className: "text-right", + render: (item) => ( + + {item.friendsNeeded} + + ), + }, + { + name: "Inheritor", + className: "text-right", + render: (item) => , + }, + { + name: "Inheritance Delay", + className: "text-right", + render: (item) => , + }, + { + name: "Cancel Delay", + className: "text-right", + render: (item) => , + }, + { + name: "Action", + className: "text-left", + render: (item) => ( + + ), + }, + ]; + + return { desktopColumns, mobileColumns }; + }, [onRecover]); +} diff --git a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx index c196e507bf..2b962267c6 100644 --- a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx +++ b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx @@ -4,19 +4,20 @@ import { useEffect, useState } from "react"; import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; import { MapDataList } from "next-common/components/dataList"; import ScrollerX from "next-common/components/styled/containers/scrollerX"; -import { - desktopColumns, - mobileColumns, -} from "next-common/components/data/recovery/table/columns"; import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; import { defaultPageSize } from "next-common/utils/constants"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; -import useHelpOthersFriendGroups from "./hooks/useHelpOthersFriendGroups"; +import useHelpOthersFriendGroupsColumns from "./hooks/useHelpOthersFriendGroupsColumns"; -export default function InFriendGroupsSection({ address }) { - const { data, loading: isLoading } = useHelpOthersFriendGroups(address); +export default function InFriendGroupsSection({ + data, + loading: isLoading, + onRefresh, +}) { + const { desktopColumns, mobileColumns } = + useHelpOthersFriendGroupsColumns(onRefresh); const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); const [totalCount, setTotalCount] = useState(0); diff --git a/packages/next-common/components/recovery/helpOthers/index.jsx b/packages/next-common/components/recovery/helpOthers/index.jsx index 1344591e42..08a9163f82 100644 --- a/packages/next-common/components/recovery/helpOthers/index.jsx +++ b/packages/next-common/components/recovery/helpOthers/index.jsx @@ -1,20 +1,43 @@ "use client"; +import { useCallback } from "react"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; import RecoverySubTabs from "next-common/components/recovery/subTabs"; import InFriendGroupsSection from "./inFriendGroupsSection"; import HelpOthersAttemptsSection from "./helpOthersAttemptsSection"; +import useHelpOthersFriendGroups from "./hooks/useHelpOthersFriendGroups"; +import useHelpOthersAttempts from "./hooks/useHelpOthersAttempts"; import { RelayChainApiProvider } from "next-common/context/relayChain"; export default function HelpOthersContent() { const address = useRealAddress(); + const friendGroups = useHelpOthersFriendGroups(address); + const attempts = useHelpOthersAttempts(address); + + const fetchFriendGroups = friendGroups.fetch; + const fetchAttempts = attempts.fetch; + + const refreshAll = useCallback(() => { + fetchFriendGroups(); + fetchAttempts(); + }, [fetchFriendGroups, fetchAttempts]); return (
- - + +
); diff --git a/packages/next-common/components/recovery/helpOthers/initiateAttemptDialog.jsx b/packages/next-common/components/recovery/helpOthers/initiateAttemptDialog.jsx new file mode 100644 index 0000000000..0eb014c0dd --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/initiateAttemptDialog.jsx @@ -0,0 +1,27 @@ +"use client"; + +import { useCallback } from "react"; +import SimpleTxPopup from "next-common/components/simpleTxPopup"; +import { useContextApi } from "next-common/context/api"; + +export default function InitiateAttemptDialog({ + onClose, + lostAccount, + friendGroupIndex, + onInBlock = () => {}, +}) { + const api = useContextApi(); + + const getTxFunc = useCallback(async () => { + return api.tx.recovery.initiateAttempt(lostAccount, friendGroupIndex); + }, [api, lostAccount, friendGroupIndex]); + + return ( + + ); +} diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyInheritor.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyInheritor.js new file mode 100644 index 0000000000..3f813f6bde --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyInheritor.js @@ -0,0 +1,73 @@ +import { useContextApi } from "next-common/context/api"; +import { useCallback, useEffect, useState } from "react"; + +export default function useMyInheritor(address) { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); + + useEffect(() => { + if (!api || !address) { + return; + } + + if (!api?.query.recovery?.inheritor) { + setLoading(false); + setData([]); + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.inheritor + .entries() + .then((entries) => { + if (cancelled) return; + + const result = entries + .map(([storageKey, value]) => { + const account = storageKey.args?.[0]?.toString(); + const jsonValue = value.toJSON(); + + const inheritancePriority = parseInt(jsonValue?.[0]) || 0; + const inheritor = jsonValue?.[1] || ""; + const consideration = jsonValue?.[2] || {}; + const depositor = consideration.depositor || ""; + const ticket = parseInt(consideration.ticket) || 0; + + return { + account, + inheritancePriority, + inheritor, + depositor, + ticket, + }; + }) + .filter( + (item) => item.account?.toLowerCase() === address?.toLowerCase(), + ); + + if (!cancelled) { + setData(result); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query my inheritor", error); + if (!cancelled) { + setData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, address, fetchCount]); + + return { data, loading, fetch }; +} diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx new file mode 100644 index 0000000000..b0a94e4edf --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx @@ -0,0 +1,113 @@ +"use client"; + +import { useMemo, useState } from "react"; +import AddressUser from "next-common/components/user/addressUser"; +import ValueDisplay from "next-common/components/valueDisplay"; +import { toPrecision } from "next-common/utils"; +import { useChainSettings } from "next-common/context/chain"; +import { isNil } from "lodash-es"; +import RevokeInheritorDialog from "../revokeInheritorDialog"; + +function TicketCell({ ticket }) { + const { decimals, symbol } = useChainSettings(); + if (isNil(ticket)) { + return null; + } + return ; +} + +function RevokeButton({ onRevoke }) { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + {showDialog && ( + setShowDialog(false)} + onInBlock={onRevoke} + /> + )} + + + ); +} + +export default function useMyRecoveryInheritorColumns(onRevoke) { + return useMemo(() => { + const desktopColumns = [ + { + name: "Inheritor", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Priority", + className: "w-[120px] text-left", + render: (item) => ( + + {item.inheritancePriority} + + ), + }, + { + name: "Depositor", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Deposit", + className: "w-[180px] text-right", + render: (item) => , + }, + { + name: "Action", + className: "w-[100px] text-right", + render: () => , + }, + ]; + + const mobileColumns = [ + { + name: "Inheritor", + className: "text-left", + render: (item) => , + }, + { + name: "Priority", + className: "text-right", + render: (item) => ( + + {item.inheritancePriority} + + ), + }, + { + name: "Depositor", + className: "text-left", + render: (item) => , + }, + { + name: "Deposit", + className: "text-right", + render: (item) => , + }, + { + name: "Action", + className: "text-left", + render: () => , + }, + ]; + + return { desktopColumns, mobileColumns }; + }, [onRevoke]); +} diff --git a/packages/next-common/components/recovery/myRecovery/index.jsx b/packages/next-common/components/recovery/myRecovery/index.jsx index e05f47f585..2d17b2911a 100644 --- a/packages/next-common/components/recovery/myRecovery/index.jsx +++ b/packages/next-common/components/recovery/myRecovery/index.jsx @@ -2,6 +2,7 @@ import useRealAddress from "next-common/utils/hooks/useRealAddress"; import FriendGroupsSection from "./friendGroupsSection"; +import InheritorSection from "./inheritorSection"; import RecoveryAttemptsSection from "./recoveryAttemptsSection"; import RecoverySubTabs from "next-common/components/recovery/subTabs"; import { RelayChainApiProvider } from "next-common/context/relayChain"; @@ -14,6 +15,7 @@ export default function MyRecoveryContent() {
+
diff --git a/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx b/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx new file mode 100644 index 0000000000..5625f9e134 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx @@ -0,0 +1,80 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; +import { isNil } from "lodash-es"; +import useMyInheritor from "./hooks/useMyInheritor"; +import useMyRecoveryInheritorColumns from "./hooks/useMyRecoveryInheritorColumns"; + +export default function InheritorSection({ address }) { + const { data, loading: isLoading, fetch } = useMyInheritor(address); + const { desktopColumns, mobileColumns } = + useMyRecoveryInheritorColumns(fetch); + const [navCollapsed] = useNavCollapsed(); + const [dataList, setDataList] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + + const { page, component: pageComponent } = usePaginationComponent( + totalCount, + defaultPageSize, + ); + + const total = data?.length || 0; + + useEffect(() => { + setLoading(true); + }, [page]); + + useEffect(() => { + if (isLoading || isNil(data)) { + return; + } + + setTotalCount(total); + const startIndex = (page - 1) * defaultPageSize; + const endIndex = startIndex + defaultPageSize; + setDataList(data?.slice(startIndex, endIndex)); + setLoading(false); + }, [data, isLoading, page, total]); + + return ( +
+
+ + Inheritor + +
+
+ + + + + + {total > 0 && pageComponent} + +
+
+ ); +} diff --git a/packages/next-common/components/recovery/myRecovery/revokeInheritorDialog.jsx b/packages/next-common/components/recovery/myRecovery/revokeInheritorDialog.jsx new file mode 100644 index 0000000000..ce505769ce --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/revokeInheritorDialog.jsx @@ -0,0 +1,25 @@ +"use client"; + +import { useCallback } from "react"; +import SimpleTxPopup from "next-common/components/simpleTxPopup"; +import { useContextApi } from "next-common/context/api"; + +export default function RevokeInheritorDialog({ + onClose, + onInBlock = () => {}, +}) { + const api = useContextApi(); + + const getTxFunc = useCallback(async () => { + return api.tx.recovery.revokeInheritor(); + }, [api]); + + return ( + + ); +} From 86e6f82391314714eca90ec8b701ffbfac7bdccb Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 18:13:24 +0800 Subject: [PATCH 25/50] Improve button state management, #6919 --- .../hooks/useHelpOthersAttemptsColumns.jsx | 34 +++++++++- .../useHelpOthersFriendGroupsColumns.jsx | 63 ++++++++++++++----- .../helpOthers/inFriendGroupsSection.jsx | 7 ++- .../components/recovery/helpOthers/index.jsx | 1 + 4 files changed, 85 insertions(+), 20 deletions(-) diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx index c48ace514b..6babcc9415 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx @@ -5,6 +5,9 @@ import Tooltip from "next-common/components/tooltip"; import { AddressesTooltip } from "next-common/components/multisigs/fields"; import AddressUser from "next-common/components/user/addressUser"; import { isSameAddress } from "next-common/utils"; +import { useRelayChainApi } from "next-common/context/relayChain"; +import useCall from "next-common/utils/hooks/useCall"; +import { isNil } from "lodash-es"; import BlockNumberWithTooltip from "../../myRecovery/blockNumberWithTooltip"; import CancelAttemptDialog from "../cancelAttemptDialog"; import ApproveAttemptDialog from "../approveAttemptDialog"; @@ -73,8 +76,33 @@ function ApproveButton({ ); } -function FinishButton({ lostAccount, friendGroupIndex, onFinish }) { +function FinishButton({ + lostAccount, + friendGroupIndex, + initBlock, + inheritanceDelay, + onFinish, +}) { const [showDialog, setShowDialog] = useState(false); + const api = useRelayChainApi(); + const { value: currentNumber } = useCall(api?.query?.system?.number, []); + const currentBlock = currentNumber?.toNumber(); + + const finishBlock = initBlock + (inheritanceDelay || 0); + const canFinish = !isNil(currentBlock) && currentBlock >= finishBlock; + + if (!canFinish) { + const tooltip = isNil(currentBlock) + ? "Loading block number..." + : `Finish available at block #${finishBlock}`; + return ( + + + Finish + + + ); + } return ( <> @@ -188,6 +216,8 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { ); @@ -294,6 +324,8 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { ); diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx index 34cf8fe0ea..9adca29406 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx @@ -24,9 +24,19 @@ function FriendsCount({ friends = [] }) { ); } -function RecoverButton({ lostAccount, friendGroupIndex, onRecover }) { +function RecoverButton({ lostAccount, friendGroupIndex, onRecover, disabled }) { const [showDialog, setShowDialog] = useState(false); + if (disabled) { + return ( + + + Recover + + + ); + } + return ( <> {showDialog && ( @@ -48,7 +58,10 @@ function RecoverButton({ lostAccount, friendGroupIndex, onRecover }) { ); } -export default function useHelpOthersFriendGroupsColumns(onRecover) { +export default function useHelpOthersFriendGroupsColumns( + onRecover, + attemptsData = [], +) { return useMemo(() => { const desktopColumns = [ { @@ -108,13 +121,21 @@ export default function useHelpOthersFriendGroupsColumns(onRecover) { { name: "Action", className: "w-[100px] text-right", - render: (item) => ( - - ), + render: (item) => { + const hasAttempt = attemptsData.some( + (a) => + a.lostAccount === item.account && + a.friendGroupIndex === item.index, + ); + return ( + + ); + }, }, ]; @@ -172,16 +193,24 @@ export default function useHelpOthersFriendGroupsColumns(onRecover) { { name: "Action", className: "text-left", - render: (item) => ( - - ), + render: (item) => { + const hasAttempt = attemptsData.some( + (a) => + a.lostAccount === item.account && + a.friendGroupIndex === item.index, + ); + return ( + + ); + }, }, ]; return { desktopColumns, mobileColumns }; - }, [onRecover]); + }, [onRecover, attemptsData]); } diff --git a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx index 2b962267c6..6a5aec7174 100644 --- a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx +++ b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx @@ -14,10 +14,13 @@ import useHelpOthersFriendGroupsColumns from "./hooks/useHelpOthersFriendGroupsC export default function InFriendGroupsSection({ data, loading: isLoading, + attemptsData = [], onRefresh, }) { - const { desktopColumns, mobileColumns } = - useHelpOthersFriendGroupsColumns(onRefresh); + const { desktopColumns, mobileColumns } = useHelpOthersFriendGroupsColumns( + onRefresh, + attemptsData, + ); const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); const [totalCount, setTotalCount] = useState(0); diff --git a/packages/next-common/components/recovery/helpOthers/index.jsx b/packages/next-common/components/recovery/helpOthers/index.jsx index 08a9163f82..9f09b550c7 100644 --- a/packages/next-common/components/recovery/helpOthers/index.jsx +++ b/packages/next-common/components/recovery/helpOthers/index.jsx @@ -36,6 +36,7 @@ export default function HelpOthersContent() {
From 80ada0cc74985d89f8b2bcdb65068054405448b2 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 18:21:47 +0800 Subject: [PATCH 26/50] Improve finish button, #6919 --- .../hooks/useHelpOthersAttemptsColumns.jsx | 22 ++++++----- .../hooks/useRelayChainBlockNumber.js | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 packages/next-common/hooks/useRelayChainBlockNumber.js diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx index 6babcc9415..759b3f6bf3 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx @@ -4,10 +4,10 @@ import { useMemo, useState } from "react"; import Tooltip from "next-common/components/tooltip"; import { AddressesTooltip } from "next-common/components/multisigs/fields"; import AddressUser from "next-common/components/user/addressUser"; -import { isSameAddress } from "next-common/utils"; -import { useRelayChainApi } from "next-common/context/relayChain"; -import useCall from "next-common/utils/hooks/useCall"; +import { isSameAddress, estimateBlocksTime } from "next-common/utils"; import { isNil } from "lodash-es"; +import { useChainSettings } from "next-common/context/chain"; +import useRelayChainBlockNumber from "next-common/hooks/useRelayChainBlockNumber"; import BlockNumberWithTooltip from "../../myRecovery/blockNumberWithTooltip"; import CancelAttemptDialog from "../cancelAttemptDialog"; import ApproveAttemptDialog from "../approveAttemptDialog"; @@ -84,17 +84,21 @@ function FinishButton({ onFinish, }) { const [showDialog, setShowDialog] = useState(false); - const api = useRelayChainApi(); - const { value: currentNumber } = useCall(api?.query?.system?.number, []); - const currentBlock = currentNumber?.toNumber(); + const currentBlock = useRelayChainBlockNumber(); + const { blockTime } = useChainSettings(); const finishBlock = initBlock + (inheritanceDelay || 0); const canFinish = !isNil(currentBlock) && currentBlock >= finishBlock; if (!canFinish) { - const tooltip = isNil(currentBlock) - ? "Loading block number..." - : `Finish available at block #${finishBlock}`; + let tooltip; + if (isNil(currentBlock)) { + tooltip = "Loading block number..."; + } else { + const blocksRemaining = Math.max(0, finishBlock - currentBlock); + const estimatedTime = estimateBlocksTime(blocksRemaining, blockTime); + tooltip = `Finish available at block #${finishBlock} (in ${estimatedTime})`; + } return ( diff --git a/packages/next-common/hooks/useRelayChainBlockNumber.js b/packages/next-common/hooks/useRelayChainBlockNumber.js new file mode 100644 index 0000000000..fb5293898a --- /dev/null +++ b/packages/next-common/hooks/useRelayChainBlockNumber.js @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react"; +import { useRelayChainApi } from "next-common/context/relayChain"; + +export default function useRelayChainBlockNumber() { + const api = useRelayChainApi(); + const [blockNumber, setBlockNumber] = useState(null); + + useEffect(() => { + if (!api) { + return; + } + + let unsubscribe; + let isMounted = true; + + api.rpc.chain + .subscribeNewHeads((header) => { + if (isMounted) { + setBlockNumber(header.number.toNumber()); + } + }) + .then((unsub) => { + if (isMounted) { + unsubscribe = unsub; + } else { + unsub(); + } + }); + + return () => { + isMounted = false; + if (unsubscribe) { + unsubscribe(); + } + }; + }, [api]); + + return blockNumber; +} From edc8c3ea3c7908bb91b4121179158b1b505dafd0 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 30 Jun 2026 18:26:48 +0800 Subject: [PATCH 27/50] Update, # 6919 --- .../components/recovery/myRecovery/inheritorSection.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx b/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx index 5625f9e134..d71c9a1fc8 100644 --- a/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx @@ -48,7 +48,7 @@ export default function InheritorSection({ address }) {
- Inheritor + Account Inheritor
From 8c4fcf474f624b201815479621f120e2bb50917f Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 09:42:09 +0800 Subject: [PATCH 28/50] Improve cancel button, #6919 --- .../hooks/useHelpOthersAttemptsColumns.jsx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx index 759b3f6bf3..ff0a35800a 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx @@ -13,8 +13,37 @@ import CancelAttemptDialog from "../cancelAttemptDialog"; import ApproveAttemptDialog from "../approveAttemptDialog"; import FinishAttemptDialog from "../finishAttemptDialog"; -function CancelButton({ lostAccount, friendGroupIndex, onCancel }) { +function CancelButton({ + lostAccount, + friendGroupIndex, + lastApprovalBlock, + cancelDelay, + onCancel, +}) { const [showDialog, setShowDialog] = useState(false); + const currentBlock = useRelayChainBlockNumber(); + const { blockTime } = useChainSettings(); + + const cancelableAt = lastApprovalBlock + (cancelDelay || 0); + const canCancel = !isNil(currentBlock) && currentBlock >= cancelableAt; + + if (!canCancel) { + let tooltip; + if (isNil(currentBlock)) { + tooltip = "Loading block number..."; + } else { + const blocksRemaining = Math.max(0, cancelableAt - currentBlock); + const estimatedTime = estimateBlocksTime(blocksRemaining, blockTime); + tooltip = `Cancel available at block #${cancelableAt} (in ${estimatedTime})`; + } + return ( + + + Cancel + + + ); + } return ( <> @@ -231,6 +260,8 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { ) : ( @@ -339,6 +370,8 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { ) : ( From e42279c12f1e2fecaa7dc3d17310b92627aebf48 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 09:46:02 +0800 Subject: [PATCH 29/50] fix style, #6919 --- .../recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx index b0a94e4edf..20eedd069a 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx @@ -66,7 +66,7 @@ export default function useMyRecoveryInheritorColumns(onRevoke) { }, { name: "Deposit", - className: "w-[180px] text-right", + className: "w-[180px] text-left", render: (item) => , }, { From d5a479ed16f89817f6a1d102117d5048b215b7c0 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 09:58:30 +0800 Subject: [PATCH 30/50] Improve action button displaying, #6919 --- .../hooks/useHelpOthersAttemptsColumns.jsx | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx index ff0a35800a..5efc1b3c49 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx @@ -246,13 +246,24 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { if (canFinish) { return ( - +
+ + {isSameAddress(item.initiator, address) && ( + + )} +
); } @@ -356,13 +367,24 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { if (canFinish) { return ( - +
+ + {isSameAddress(item.initiator, address) && ( + + )} +
); } From 7cf64574f6cd31aab89e14d43c18f88f6f03ea5f Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 10:20:55 +0800 Subject: [PATCH 31/50] Add button tooltips, #6919 --- .../hooks/useHelpOthersAttemptsColumns.jsx | 56 ++++++++++--------- .../useHelpOthersFriendGroupsColumns.jsx | 16 +++--- .../myRecovery/friendGroupsSection.jsx | 49 +++++++++------- .../hooks/useMyRecoveryInheritorColumns.jsx | 17 +++--- .../recovery/myRecovery/slashButton.jsx | 17 +++--- 5 files changed, 88 insertions(+), 67 deletions(-) diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx index 5efc1b3c49..0f34194bf3 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx @@ -55,13 +55,15 @@ function CancelButton({ onInBlock={onCancel} /> )} - + + + ); } @@ -94,13 +96,15 @@ function ApproveButton({ onInBlock={onApprove} /> )} - + + + ); } @@ -147,13 +151,15 @@ function FinishButton({ onInBlock={onFinish} /> )} - + + + ); } @@ -194,19 +200,19 @@ export default function useHelpOthersAttemptsColumns(address, onAction) { }, { name: "Initiator", - className: "min-w-[200px] text-left", + className: "min-w-[160px] text-left", render: (item) => ( - + ), }, { name: "Init Block", - className: "w-[180px] text-left", + className: "w-[140px] text-left", render: (item) => , }, { name: "Last Approval Block", - className: "w-[200px] text-left", + className: "w-[180px] text-left", render: (item) => ( ), diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx index 9adca29406..22f1f00ded 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx @@ -47,13 +47,15 @@ function RecoverButton({ lostAccount, friendGroupIndex, onRecover, disabled }) { onInBlock={onRecover} /> )} - + + + ); } diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 9a60e7d356..37b780146e 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import Tooltip from "next-common/components/tooltip"; import Loading from "next-common/components/loading"; import AddressUser from "next-common/components/user/addressUser"; import DelayBlock from "next-common/components/recovery/delayBlock"; @@ -34,20 +35,24 @@ function FriendGroupCard({ group, onEdit, onRemove }) { Group #{group.index}
- - + + + + + +
@@ -160,13 +165,15 @@ export default function FriendGroupsSection({ address }) { Friend Groups - + + +
{loading ? (
diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx index 20eedd069a..f1392157c8 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx @@ -2,6 +2,7 @@ import { useMemo, useState } from "react"; import AddressUser from "next-common/components/user/addressUser"; +import Tooltip from "next-common/components/tooltip"; import ValueDisplay from "next-common/components/valueDisplay"; import { toPrecision } from "next-common/utils"; import { useChainSettings } from "next-common/context/chain"; @@ -27,13 +28,15 @@ function RevokeButton({ onRevoke }) { onInBlock={onRevoke} /> )} - + + + ); } diff --git a/packages/next-common/components/recovery/myRecovery/slashButton.jsx b/packages/next-common/components/recovery/myRecovery/slashButton.jsx index 88a923022c..5ebcff0ca9 100644 --- a/packages/next-common/components/recovery/myRecovery/slashButton.jsx +++ b/packages/next-common/components/recovery/myRecovery/slashButton.jsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import Tooltip from "next-common/components/tooltip"; import SlashAttemptDialog from "./slashAttemptDialog"; export default function SlashButton({ friendGroupIndex, onSlash }) { @@ -15,13 +16,15 @@ export default function SlashButton({ friendGroupIndex, onSlash }) { onInBlock={onSlash} /> )} - + + + ); } From afd29886c8071db5127cc757c8735c34f14aeab1 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 11:42:18 +0800 Subject: [PATCH 32/50] Implement control inherited account, #6919 --- .../recovery/inheritors/addressDisplay.jsx | 74 +++++++++++ .../controlInheritedAccountDialog.jsx | 97 ++++++++++++++ .../hooks/useInheritedAccountsColumns.jsx | 125 ++++++++++++++++++ .../inheritors/inheritedAccountsSection.jsx | 6 +- 4 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 packages/next-common/components/recovery/inheritors/addressDisplay.jsx create mode 100644 packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx create mode 100644 packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx diff --git a/packages/next-common/components/recovery/inheritors/addressDisplay.jsx b/packages/next-common/components/recovery/inheritors/addressDisplay.jsx new file mode 100644 index 0000000000..23fdbcae88 --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/addressDisplay.jsx @@ -0,0 +1,74 @@ +"use client"; + +import AddressAvatar from "next-common/components/user/addressAvatar"; +import useIdentityInfo from "next-common/hooks/useIdentityInfo"; +import { normalizeAddress } from "next-common/utils/address"; +import { tryConvertToEvmAddress } from "next-common/utils/mixedChainUtil"; +import { addressEllipsis } from "next-common/utils"; +import Identity from "next-common/components/Identity"; +import AddressInfoLoading from "next-common/components/addressInfo"; +import { GreyPanel } from "next-common/components/styled/containers/greyPanel"; +import styled from "styled-components"; + +const AvatarWrapper = styled.div` + display: flex; + align-items: center; + position: relative; +`; + +const NameWrapper = styled.div` + color: var(--textPrimary); + flex-grow: 1; + > :first-child { + font-size: 14px; + font-weight: 500; + } + > :last-child { + margin-top: 4px; + font-size: 12px; + color: var(--textTertiary); + } +`; + +const Wrapper = styled(GreyPanel)` + padding: 12px 16px; + gap: 16px; +`; + +export default function AddressDisplay({ address }) { + const { identity, hasIdentity, isLoading } = useIdentityInfo(address); + const normalized = normalizeAddress(address); + const displayAddress = tryConvertToEvmAddress(normalized); + const addressHint = addressEllipsis(displayAddress); + + if (isLoading) { + return ( + + + + ); + } + + return ( + +
+ + + + + {hasIdentity ? ( + <> + +
{normalized}
+ + ) : ( + <> +
{addressHint}
+
{normalized}
+ + )} +
+
+
+ ); +} diff --git a/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx b/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx new file mode 100644 index 0000000000..30e1733a14 --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx @@ -0,0 +1,97 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { useContextApi } from "next-common/context/api"; +import SignerPopupWrapper from "next-common/components/popupWithSigner/signerPopupWrapper"; +import PopupLabel from "next-common/components/popup/label"; +import Extrinsic from "next-common/components/extrinsic"; +import SignerWithBalance from "next-common/components/signerPopup/signerWithBalance"; +import TxSubmissionButton from "next-common/components/common/tx/txSubmissionButton"; +import AdvanceSettings from "next-common/components/summary/newProposalQuickStart/common/advanceSettings"; +import EstimatedGas from "next-common/components/estimatedGas"; +import { usePopupParams } from "next-common/components/popupWithSigner/context"; +import { ExtrinsicLoading } from "next-common/components/popup/fields/extrinsicField"; +import AddressDisplay from "./addressDisplay"; +import Popup from "next-common/components/popup/wrapper/Popup"; + +function ControlInheritedAccountInnerPopup({ recovered, onInBlock }) { + const { onClose } = usePopupParams(); + const api = useContextApi(); + const [call, setCall] = useState(null); + const disabled = !api || !call; + + const getTxFunc = useCallback(async () => { + if (!api || !call) { + return null; + } + return api.tx.recovery.controlInheritedAccount(recovered, call); + }, [api, recovered, call]); + + const setProposal = useCallback( + ({ isValid, data: tx } = {}) => { + if (!api || !isValid || !tx) { + setCall(null); + return; + } + setCall(tx); + }, + [api], + ); + + let extrinsicComponent = null; + + if (!api) { + extrinsicComponent = ; + } else { + extrinsicComponent = ( +
+
+ + +
+
+ ); + } + + return ( + + +
+ + +
+ {extrinsicComponent} + + + + +
+ ); +} + +export default function ControlInheritedAccountDialog({ + onClose, + recovered, + onInBlock, +}) { + return ( + + + + ); +} diff --git a/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx new file mode 100644 index 0000000000..93460c63e8 --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx @@ -0,0 +1,125 @@ +"use client"; + +import { useMemo, useState } from "react"; +import AddressUser from "next-common/components/user/addressUser"; +import ValueDisplay from "next-common/components/valueDisplay"; +import { toPrecision } from "next-common/utils"; +import { useChainSettings } from "next-common/context/chain"; +import { isNil } from "lodash-es"; +import ControlInheritedAccountDialog from "../controlInheritedAccountDialog"; + +function TicketCell({ ticket }) { + const { decimals, symbol } = useChainSettings(); + if (isNil(ticket)) { + return null; + } + return ; +} + +function ControlButton({ item }) { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + {showDialog && ( + setShowDialog(false)} + /> + )} + + + ); +} + +export default function useInheritedAccountsColumns() { + return useMemo(() => { + const desktopColumns = [ + { + name: "Lost Account", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Inheritor", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Priority", + className: "w-[120px] text-left", + render: (item) => ( + + {item.inheritancePriority} + + ), + }, + { + name: "Depositor", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Deposit", + className: "w-[180px] text-left", + render: (item) => , + }, + { + name: "Action", + className: "w-[100px] text-right", + render: (item) => , + }, + ]; + + const mobileColumns = [ + { + name: "Lost Account", + className: "text-left", + render: (item) => , + }, + { + name: "Inheritor", + className: "text-left", + render: (item) => , + }, + { + name: "Priority", + className: "text-right", + render: (item) => ( + + {item.inheritancePriority} + + ), + }, + { + name: "Depositor", + className: "text-left", + render: (item) => , + }, + { + name: "Deposit", + className: "text-right", + render: (item) => , + }, + { + name: "Action", + className: "text-left", + render: (item) => , + }, + ]; + + return { desktopColumns, mobileColumns }; + }, []); +} diff --git a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx index 4cd764d583..4344e452a9 100644 --- a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx +++ b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx @@ -4,19 +4,17 @@ import { useEffect, useState } from "react"; import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; import { MapDataList } from "next-common/components/dataList"; import ScrollerX from "next-common/components/styled/containers/scrollerX"; -import { - desktopColumns, - mobileColumns, -} from "next-common/components/data/recovery/table/inheritorsColumns"; import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; import { defaultPageSize } from "next-common/utils/constants"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; import useInheritedAccounts from "./hooks/useInheritedAccounts"; +import useInheritedAccountsColumns from "./hooks/useInheritedAccountsColumns"; export default function InheritedAccountsSection({ address }) { const { data, loading: isLoading } = useInheritedAccounts(address); + const { desktopColumns, mobileColumns } = useInheritedAccountsColumns(); const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); const [totalCount, setTotalCount] = useState(0); From e589d6ba203fc3355beda233481db246d032495e Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 12:12:13 +0800 Subject: [PATCH 33/50] Update, #6919 --- .../recovery/inheritors/controlInheritedAccountDialog.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx b/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx index 30e1733a14..e4699e54aa 100644 --- a/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx +++ b/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx @@ -48,8 +48,8 @@ function ControlInheritedAccountInnerPopup({ recovered, onInBlock }) {
From cb849e5cb0b4cce044db464852ac184459633387 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 13:13:51 +0800 Subject: [PATCH 34/50] refactor, #6919 --- .../{recovery/inheritors => popup/fields}/addressDisplay.jsx | 0 .../recovery/inheritors/controlInheritedAccountDialog.jsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/next-common/components/{recovery/inheritors => popup/fields}/addressDisplay.jsx (100%) diff --git a/packages/next-common/components/recovery/inheritors/addressDisplay.jsx b/packages/next-common/components/popup/fields/addressDisplay.jsx similarity index 100% rename from packages/next-common/components/recovery/inheritors/addressDisplay.jsx rename to packages/next-common/components/popup/fields/addressDisplay.jsx diff --git a/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx b/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx index e4699e54aa..7472ee02fe 100644 --- a/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx +++ b/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx @@ -11,7 +11,7 @@ import AdvanceSettings from "next-common/components/summary/newProposalQuickStar import EstimatedGas from "next-common/components/estimatedGas"; import { usePopupParams } from "next-common/components/popupWithSigner/context"; import { ExtrinsicLoading } from "next-common/components/popup/fields/extrinsicField"; -import AddressDisplay from "./addressDisplay"; +import AddressDisplay from "next-common/components/popup/fields/addressDisplay"; import Popup from "next-common/components/popup/wrapper/Popup"; function ControlInheritedAccountInnerPopup({ recovered, onInBlock }) { From 9e5dcb3317e341bd32823ce436eaa5104aebd4d4 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 13:52:29 +0800 Subject: [PATCH 35/50] Update text, #6919 --- packages/next/pages/account/help-recover.jsx | 4 +++- packages/next/pages/account/inherited.jsx | 4 ++-- packages/next/pages/account/my-recovery.jsx | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/next/pages/account/help-recover.jsx b/packages/next/pages/account/help-recover.jsx index 306d97b205..ae311dff61 100644 --- a/packages/next/pages/account/help-recover.jsx +++ b/packages/next/pages/account/help-recover.jsx @@ -5,7 +5,9 @@ import { withCommonProps } from "next-common/lib"; export default function AccountHelpRecoverPage() { return ( - +
diff --git a/packages/next/pages/account/inherited.jsx b/packages/next/pages/account/inherited.jsx index 275e5b0640..19e862e92b 100644 --- a/packages/next/pages/account/inherited.jsx +++ b/packages/next/pages/account/inherited.jsx @@ -3,9 +3,9 @@ import AccountLayout from "next-common/components/layout/AccountLayout"; import AccountSubTabs from "next-common/components/overview/account/subTabs"; import { withCommonProps } from "next-common/lib"; -export default function AccountInheritantsPage() { +export default function AccountInheritedPage() { return ( - +
diff --git a/packages/next/pages/account/my-recovery.jsx b/packages/next/pages/account/my-recovery.jsx index 78d51142d6..9b6c951364 100644 --- a/packages/next/pages/account/my-recovery.jsx +++ b/packages/next/pages/account/my-recovery.jsx @@ -5,7 +5,7 @@ import { withCommonProps } from "next-common/lib"; export default function AccountMyRecoveryPage() { return ( - +
From 357ec36319f33ccc3ed4b010978ac3022f71cf0b Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 14:20:54 +0800 Subject: [PATCH 36/50] refactor query attempt hooks, #6919 --- ...veryData.js => useQueryAllFriendGroups.js} | 2 +- .../hooks/useQueryAllRecoveryAttempts.js | 111 +----------- .../components/data/recovery/index.jsx | 6 +- .../components/data/recovery/table/index.jsx | 2 +- .../common/hooks/useRecoveryAttemptsData.js | 147 ++++++++++++++++ .../helpOthers/hooks/useHelpOthersAttempts.js | 159 +++--------------- .../hooks/useHelpOthersFriendGroups.js | 2 +- .../hooks/useInheritorFriendGroups.js | 2 +- .../myRecovery/hooks/useMyRecoveryAttempts.js | 114 +------------ 9 files changed, 188 insertions(+), 357 deletions(-) rename packages/next-common/components/data/recovery/hooks/{useQueryAllRecoveryData.js => useQueryAllFriendGroups.js} (97%) create mode 100644 packages/next-common/components/recovery/common/hooks/useRecoveryAttemptsData.js diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js b/packages/next-common/components/data/recovery/hooks/useQueryAllFriendGroups.js similarity index 97% rename from packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js rename to packages/next-common/components/data/recovery/hooks/useQueryAllFriendGroups.js index df56c3d169..de6a28e1b3 100644 --- a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllFriendGroups.js @@ -24,7 +24,7 @@ export function flattenRecoveryData(data) { return rows; } -export default function useQueryAllRecoveryData() { +export default function useQueryAllFriendGroups() { const api = useContextApi(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js index 650f4ee07b..721bf57c75 100644 --- a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js @@ -1,111 +1,6 @@ -import { useContextApi } from "next-common/context/api"; -import { useEffect, useState } from "react"; - -function bitfieldToIndices(bitfield) { - const indices = []; - let bitIndex = 0; - for (const byte of bitfield) { - if (typeof byte === "number") { - for (let b = 0; b < 8; b++) { - if (byte & (1 << b)) { - indices.push(bitIndex + b); - } - } - } - bitIndex += 8; - } - return indices; -} +import useRecoveryAttemptsData from "next-common/components/recovery/common/hooks/useRecoveryAttemptsData"; export default function useQueryAllRecoveryAttempts() { - const api = useContextApi(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - if (!api) { - return; - } - - if (!api?.query.recovery?.attempt) { - setLoading(false); - setData([]); - return; - } - - let cancelled = false; - setLoading(true); - - api.query.recovery.attempt - .entries() - .then(async (entries) => { - if (cancelled) return; - - // Build a map of (lostAccount) -> friendGroups to resolve bitfield indices to addresses - const lostAccounts = [ - ...new Set(entries.map(([key]) => key.args?.[0]?.toString())), - ]; - const friendGroupsMap = {}; - - await Promise.all( - lostAccounts.map(async (account) => { - try { - const value = await api.query.recovery.friendGroups(account); - const json = value.toJSON(); - friendGroupsMap[account] = json?.[0] || []; - } catch { - friendGroupsMap[account] = []; - } - }), - ); - - const result = entries.map(([storageKey, value]) => { - const lostAccount = storageKey.args?.[0]?.toString(); - const friendGroupIndex = storageKey.args?.[1]?.toNumber(); - - const json = value.toJSON(); - const attempt = json?.[0] || {}; - - const approvalsBitfield = attempt.approvals || []; - const approvedIndices = bitfieldToIndices(approvalsBitfield); - const approvalsCount = approvedIndices.length; - - // Resolve approved indices to actual friend addresses - const friendGroup = - (friendGroupsMap[lostAccount] || [])[friendGroupIndex] || {}; - const friends = friendGroup.friends || []; - const approvedAddresses = approvedIndices - .filter((i) => i < friends.length) - .map((i) => friends[i]); - - return { - lostAccount, - friendGroupIndex, - initiator: attempt.initiator || "", - initBlock: parseInt(attempt.initBlock) || 0, - lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, - approvalsCount, - approvedAddresses, - }; - }); - - if (!cancelled) { - setData(result); - setLoading(false); - } - }) - .catch((error) => { - console.error("Failed to query recovery attempts", error); - if (!cancelled) { - setData([]); - setLoading(false); - } - }); - - return () => { - cancelled = true; - }; - }, [api]); - - return { data, loading }; + const { attempts, loading } = useRecoveryAttemptsData(); + return { data: attempts, loading }; } diff --git a/packages/next-common/components/data/recovery/index.jsx b/packages/next-common/components/data/recovery/index.jsx index 774e118b73..c657492214 100644 --- a/packages/next-common/components/data/recovery/index.jsx +++ b/packages/next-common/components/data/recovery/index.jsx @@ -7,9 +7,9 @@ import PageHeader from "../common/pageHeader"; import RecoveryFriendGroupsTable from "./table"; import RecoveryAttemptsTable from "./table/attempts"; import RecoveryInheritorsTable from "./table/inheritors"; -import useQueryAllRecoveryData, { +import useQueryAllFriendGroups, { flattenRecoveryData, -} from "./hooks/useQueryAllRecoveryData"; +} from "./hooks/useQueryAllFriendGroups"; import useQueryAllRecoveryAttempts from "./hooks/useQueryAllRecoveryAttempts"; import useQueryAllRecoveryInheritors from "./hooks/useQueryAllRecoveryInheritors"; import { searchAddress } from "./table"; @@ -21,7 +21,7 @@ export default function RecoveryExplorer() { const search = router.query.search || ""; const { data: friendGroupsData, loading: friendGroupsLoading } = - useQueryAllRecoveryData(); + useQueryAllFriendGroups(); const { data: attemptsData, loading: attemptsLoading } = useQueryAllRecoveryAttempts(); const { data: inheritorsData, loading: inheritorsLoading } = diff --git a/packages/next-common/components/data/recovery/table/index.jsx b/packages/next-common/components/data/recovery/table/index.jsx index d2e4981591..cb0a4e63e8 100644 --- a/packages/next-common/components/data/recovery/table/index.jsx +++ b/packages/next-common/components/data/recovery/table/index.jsx @@ -11,7 +11,7 @@ import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; import { addRouterQuery } from "next-common/utils/router"; -import { flattenRecoveryData } from "../../recovery/hooks/useQueryAllRecoveryData"; +import { flattenRecoveryData } from "../../recovery/hooks/useQueryAllFriendGroups"; export function searchAddress(list, keyword) { if (!keyword) { diff --git a/packages/next-common/components/recovery/common/hooks/useRecoveryAttemptsData.js b/packages/next-common/components/recovery/common/hooks/useRecoveryAttemptsData.js new file mode 100644 index 0000000000..97b4c48d7b --- /dev/null +++ b/packages/next-common/components/recovery/common/hooks/useRecoveryAttemptsData.js @@ -0,0 +1,147 @@ +import { useContextApi } from "next-common/context/api"; +import { useCallback, useEffect, useState } from "react"; + +function bitfieldToIndices(bitfield) { + const indices = []; + let bitIndex = 0; + for (const byte of bitfield) { + if (typeof byte === "number") { + for (let b = 0; b < 8; b++) { + if (byte & (1 << b)) { + indices.push(bitIndex + b); + } + } + } + bitIndex += 8; + } + return indices; +} + +export function buildFriendGroupsData(friendGroupsMap) { + return Object.entries(friendGroupsMap).map(([account, groups]) => ({ + account, + friendGroups: groups.map((group, index) => ({ + index, + friends: group.friends || [], + friendsNeeded: parseInt(group.friendsNeeded) || 0, + inheritor: group.inheritor || "", + inheritancePriority: parseInt(group.inheritancePriority) || 0, + inheritanceDelay: parseInt(group.inheritanceDelay) || 0, + cancelDelay: parseInt(group.cancelDelay) || 0, + })), + })); +} + +function processAttempts(entries, friendGroupsMap) { + return entries.map(([storageKey, value]) => { + const lostAccount = storageKey.args?.[0]?.toString(); + const friendGroupIndex = storageKey.args?.[1]?.toNumber(); + + const json = value.toJSON(); + const attempt = json?.[0] || {}; + + const approvalsBitfield = attempt.approvals || []; + const approvedIndices = bitfieldToIndices(approvalsBitfield); + const approvalsCount = approvedIndices.length; + + const friendGroup = + (friendGroupsMap[lostAccount] || [])[friendGroupIndex] || {}; + const friends = friendGroup.friends || []; + const approvedAddresses = approvedIndices + .filter((i) => i < friends.length) + .map((i) => friends[i]); + + return { + lostAccount, + friendGroupIndex, + initiator: attempt.initiator || "", + initBlock: parseInt(attempt.initBlock) || 0, + lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, + approvalsCount, + approvedAddresses, + friendsNeeded: parseInt(friendGroup.friendsNeeded) || 0, + }; + }); +} + +/** + * Base hook that fetches all recovery attempts and their friend groups. + * + * @returns {{ attempts: Array, loading: boolean, fetch: () => void, friendGroupsMap: object }} + * - attempts: all processed recovery attempt objects + * - loading: whether data is being fetched + * - fetch: trigger a re-fetch + * - friendGroupsMap: raw map of lostAccount -> friendGroups for building friendGroupsData + */ +export default function useRecoveryAttemptsData() { + const api = useContextApi(); + const [attempts, setAttempts] = useState([]); + const [loading, setLoading] = useState(true); + const [friendGroupsMap, setFriendGroupsMap] = useState({}); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); + + useEffect(() => { + if (!api) { + return; + } + + if (!api?.query.recovery?.attempt) { + setLoading(false); + setAttempts([]); + setFriendGroupsMap({}); + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.attempt + .entries() + .then(async (entries) => { + if (cancelled) return; + + const lostAccounts = [ + ...new Set(entries.map(([key]) => key.args?.[0]?.toString())), + ]; + const fgMap = {}; + + await Promise.all( + lostAccounts.map(async (account) => { + try { + const value = await api.query.recovery.friendGroups(account); + const json = value.toJSON(); + fgMap[account] = json?.[0] || []; + } catch { + fgMap[account] = []; + } + }), + ); + + if (cancelled) return; + + const result = processAttempts(entries, fgMap); + + if (!cancelled) { + setAttempts(result); + setFriendGroupsMap(fgMap); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query recovery attempts", error); + if (!cancelled) { + setAttempts([]); + setFriendGroupsMap({}); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, fetchCount]); + + return { attempts, loading, fetch, friendGroupsMap }; +} diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js index aebf9bd63c..cc843c3b87 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js @@ -1,140 +1,29 @@ -import { useContextApi } from "next-common/context/api"; -import { useCallback, useEffect, useState } from "react"; +import { useMemo } from "react"; +import useRecoveryAttemptsData, { + buildFriendGroupsData, +} from "next-common/components/recovery/common/hooks/useRecoveryAttemptsData"; export default function useHelpOthersAttempts(address) { - const api = useContextApi(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [friendGroupsData, setFriendGroupsData] = useState([]); - const [fetchCount, setFetchCount] = useState(0); - - const fetch = useCallback(() => setFetchCount((c) => c + 1), []); - - useEffect(() => { - if (!api || !address) { - return; - } - - if (!api?.query.recovery?.attempt) { - setLoading(false); - setData([]); - return; - } - - let cancelled = false; - setLoading(true); - - api.query.recovery.attempt - .entries() - .then(async (entries) => { - if (cancelled) return; - - // Collect unique lostAccounts to fetch their friend groups - const lostAccounts = [ - ...new Set(entries.map(([key]) => key.args?.[0]?.toString())), - ]; - const friendGroupsMap = {}; - - await Promise.all( - lostAccounts.map(async (account) => { - try { - const value = await api.query.recovery.friendGroups(account); - const json = value.toJSON(); - friendGroupsMap[account] = json?.[0] || []; - } catch { - friendGroupsMap[account] = []; - } - }), - ); - - // Build raw friend groups data for enhanceAttemptWithFriendGroup - const rawFriendGroups = Object.entries(friendGroupsMap).map( - ([account, groups]) => ({ - account, - friendGroups: groups.map((group, index) => ({ - index, - friends: group.friends || [], - friendsNeeded: parseInt(group.friendsNeeded) || 0, - inheritor: group.inheritor || "", - inheritancePriority: parseInt(group.inheritancePriority) || 0, - inheritanceDelay: parseInt(group.inheritanceDelay) || 0, - cancelDelay: parseInt(group.cancelDelay) || 0, - })), - }), - ); - - // Filter entries where current address is a friend of the lost account - const filtered = entries.filter(([storageKey]) => { - const lostAccount = storageKey.args?.[0]?.toString(); - const friendGroups = friendGroupsMap[lostAccount] || []; - return friendGroups.some((group) => - (group.friends || []).some( - (f) => f?.toLowerCase() === address?.toLowerCase(), - ), - ); - }); - - if (cancelled) return; - - const result = filtered.map(([storageKey, value]) => { - const lostAccount = storageKey.args?.[0]?.toString(); - const friendGroupIndex = storageKey.args?.[1]?.toNumber(); - const json = value.toJSON(); - const attempt = json?.[0] || {}; - - const approvalsBitfield = attempt.approvals || []; - // Simple bitfield to indices - const approvedIndices = []; - let bitIndex = 0; - for (const byte of approvalsBitfield) { - if (typeof byte === "number") { - for (let b = 0; b < 8; b++) { - if (byte & (1 << b)) approvedIndices.push(bitIndex + b); - } - } - bitIndex += 8; - } - - const approvalsCount = approvedIndices.length; - - const friendGroup = - (friendGroupsMap[lostAccount] || [])[friendGroupIndex] || {}; - const friends = friendGroup.friends || []; - const approvedAddresses = approvedIndices - .filter((i) => i < friends.length) - .map((i) => friends[i]); - - return { - lostAccount, - friendGroupIndex, - initiator: attempt.initiator || "", - initBlock: parseInt(attempt.initBlock) || 0, - lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, - approvalsCount, - approvedAddresses, - friendsNeeded: friendGroup.friendsNeeded || 0, - }; - }); - - if (!cancelled) { - setData(result); - setFriendGroupsData(rawFriendGroups); - setLoading(false); - } - }) - .catch((error) => { - console.error("Failed to query help others attempts", error); - if (!cancelled) { - setData([]); - setFriendGroupsData([]); - setLoading(false); - } - }); - - return () => { - cancelled = true; - }; - }, [api, address, fetchCount]); + const { attempts, loading, fetch, friendGroupsMap } = + useRecoveryAttemptsData(); + + const data = useMemo(() => { + if (!address) return []; + + return attempts.filter((attempt) => { + const friendGroups = friendGroupsMap[attempt.lostAccount] || []; + return friendGroups.some((group) => + (group.friends || []).some( + (f) => f?.toLowerCase() === address?.toLowerCase(), + ), + ); + }); + }, [attempts, friendGroupsMap, address]); + + const friendGroupsData = useMemo( + () => buildFriendGroupsData(friendGroupsMap), + [friendGroupsMap], + ); return { data, loading, friendGroupsData, fetch }; } diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js index ce41178692..403bceb179 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js @@ -1,6 +1,6 @@ import { useContextApi } from "next-common/context/api"; import { useCallback, useEffect, useState } from "react"; -import { flattenRecoveryData } from "next-common/components/data/recovery/hooks/useQueryAllRecoveryData"; +import { flattenRecoveryData } from "next-common/components/data/recovery/hooks/useQueryAllFriendGroups"; export default function useHelpOthersFriendGroups(address) { const api = useContextApi(); diff --git a/packages/next-common/components/recovery/inheritors/hooks/useInheritorFriendGroups.js b/packages/next-common/components/recovery/inheritors/hooks/useInheritorFriendGroups.js index f88907fbe3..21050e6f2e 100644 --- a/packages/next-common/components/recovery/inheritors/hooks/useInheritorFriendGroups.js +++ b/packages/next-common/components/recovery/inheritors/hooks/useInheritorFriendGroups.js @@ -1,6 +1,6 @@ import { useContextApi } from "next-common/context/api"; import { useCallback, useEffect, useState } from "react"; -import { flattenRecoveryData } from "next-common/components/data/recovery/hooks/useQueryAllRecoveryData"; +import { flattenRecoveryData } from "next-common/components/data/recovery/hooks/useQueryAllFriendGroups"; export default function useInheritorFriendGroups(address) { const api = useContextApi(); diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js index 4b4b2e6905..d93417637b 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js @@ -1,113 +1,13 @@ -import { useContextApi } from "next-common/context/api"; -import { useCallback, useEffect, useState } from "react"; - -function bitfieldToIndices(bitfield) { - const indices = []; - let bitIndex = 0; - for (const byte of bitfield) { - if (typeof byte === "number") { - for (let b = 0; b < 8; b++) { - if (byte & (1 << b)) indices.push(bitIndex + b); - } - } - bitIndex += 8; - } - return indices; -} +import { useMemo } from "react"; +import useRecoveryAttemptsData from "next-common/components/recovery/common/hooks/useRecoveryAttemptsData"; export default function useMyRecoveryAttempts(address) { - const api = useContextApi(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [fetchCount, setFetchCount] = useState(0); - - const fetch = useCallback(() => setFetchCount((c) => c + 1), []); - - useEffect(() => { - if (!api || !address) { - return; - } - - if (!api?.query.recovery?.attempt) { - return; - } - - let cancelled = false; - setLoading(true); - - api.query.recovery.attempt - .entries() - .then(async (entries) => { - if (cancelled) return; - - // Filter entries where lostAccount matches current address - const myEntries = entries.filter( - ([key]) => key.args?.[0]?.toString() === address, - ); - - // Collect unique lostAccounts to fetch their friend groups - const lostAccounts = [ - ...new Set(myEntries.map(([key]) => key.args?.[0]?.toString())), - ]; - const friendGroupsMap = {}; - - await Promise.all( - lostAccounts.map(async (account) => { - try { - const value = await api.query.recovery.friendGroups(account); - const json = value.toJSON(); - friendGroupsMap[account] = json?.[0] || []; - } catch { - friendGroupsMap[account] = []; - } - }), - ); - - const result = myEntries.map(([storageKey, value]) => { - const friendGroupIndex = storageKey.args?.[1]?.toNumber(); - const json = value.toJSON(); - const attempt = json?.[0] || {}; - - const approvalsBitfield = attempt.approvals || []; - const approvedIndices = bitfieldToIndices(approvalsBitfield); - const approvalsCount = approvedIndices.length; - - const friendGroup = - (friendGroupsMap[address] || [])[friendGroupIndex] || {}; - const friends = friendGroup.friends || []; - const approvedAddresses = approvedIndices - .filter((i) => i < friends.length) - .map((i) => friends[i]); - - return { - lostAccount: address, - friendGroupIndex, - initiator: attempt.initiator || "", - initBlock: parseInt(attempt.initBlock) || 0, - lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, - approvalsCount, - approvedAddresses, - friendsNeeded: friendGroup.friendsNeeded || 0, - }; - }); - - if (!cancelled) { - setData(result); - setLoading(false); - } - }) - .catch((error) => { - console.error("Failed to query recovery attempts", error); - if (!cancelled) { - setData([]); - setLoading(false); - } - }); + const { attempts, loading, fetch } = useRecoveryAttemptsData(); - return () => { - cancelled = true; - }; - }, [api, address, fetchCount]); + const data = useMemo(() => { + if (!address) return []; + return attempts.filter((a) => a.lostAccount === address); + }, [attempts, address]); return { data, loading, fetch }; } From a44b5d93a5a0c849cd10d9179e334a44d84e7aa6 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 14:32:44 +0800 Subject: [PATCH 37/50] refactor recovery hooks, #6919 --- .../recovery/hooks/useQueryAllFriendGroups.js | 9 +- .../hooks/useQueryAllRecoveryAttempts.js | 147 +++++++++++++++++- .../hooks/useQueryAllRecoveryInheritors.js | 9 +- .../common/hooks/useRecoveryAttemptsData.js | 147 ------------------ .../helpOthers/hooks/useHelpOthersAttempts.js | 12 +- .../hooks/useHelpOthersFriendGroups.js | 88 ++--------- .../inheritors/hooks/useInheritedAccounts.js | 78 ++-------- .../myRecovery/hooks/useMyFriendGroups.js | 58 +------ .../myRecovery/hooks/useMyInheritor.js | 78 ++-------- .../myRecovery/hooks/useMyRecoveryAttempts.js | 4 +- 10 files changed, 208 insertions(+), 422 deletions(-) delete mode 100644 packages/next-common/components/recovery/common/hooks/useRecoveryAttemptsData.js diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllFriendGroups.js b/packages/next-common/components/data/recovery/hooks/useQueryAllFriendGroups.js index de6a28e1b3..659b48daaf 100644 --- a/packages/next-common/components/data/recovery/hooks/useQueryAllFriendGroups.js +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllFriendGroups.js @@ -1,5 +1,5 @@ import { useContextApi } from "next-common/context/api"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; export function flattenRecoveryData(data) { if (!data || data.length === 0) { @@ -28,6 +28,9 @@ export default function useQueryAllFriendGroups() { const api = useContextApi(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); useEffect(() => { if (!api) { @@ -83,7 +86,7 @@ export default function useQueryAllFriendGroups() { return () => { cancelled = true; }; - }, [api]); + }, [api, fetchCount]); - return { data, loading }; + return { data, loading, fetch }; } diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js index 721bf57c75..389c9a0ff2 100644 --- a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js @@ -1,6 +1,147 @@ -import useRecoveryAttemptsData from "next-common/components/recovery/common/hooks/useRecoveryAttemptsData"; +import { useContextApi } from "next-common/context/api"; +import { useCallback, useEffect, useState } from "react"; +function bitfieldToIndices(bitfield) { + const indices = []; + let bitIndex = 0; + for (const byte of bitfield) { + if (typeof byte === "number") { + for (let b = 0; b < 8; b++) { + if (byte & (1 << b)) { + indices.push(bitIndex + b); + } + } + } + bitIndex += 8; + } + return indices; +} + +export function buildFriendGroupsData(friendGroupsMap) { + return Object.entries(friendGroupsMap).map(([account, groups]) => ({ + account, + friendGroups: groups.map((group, index) => ({ + index, + friends: group.friends || [], + friendsNeeded: parseInt(group.friendsNeeded) || 0, + inheritor: group.inheritor || "", + inheritancePriority: parseInt(group.inheritancePriority) || 0, + inheritanceDelay: parseInt(group.inheritanceDelay) || 0, + cancelDelay: parseInt(group.cancelDelay) || 0, + })), + })); +} + +function processAttempts(entries, friendGroupsMap) { + return entries.map(([storageKey, value]) => { + const lostAccount = storageKey.args?.[0]?.toString(); + const friendGroupIndex = storageKey.args?.[1]?.toNumber(); + + const json = value.toJSON(); + const attempt = json?.[0] || {}; + + const approvalsBitfield = attempt.approvals || []; + const approvedIndices = bitfieldToIndices(approvalsBitfield); + const approvalsCount = approvedIndices.length; + + const friendGroup = + (friendGroupsMap[lostAccount] || [])[friendGroupIndex] || {}; + const friends = friendGroup.friends || []; + const approvedAddresses = approvedIndices + .filter((i) => i < friends.length) + .map((i) => friends[i]); + + return { + lostAccount, + friendGroupIndex, + initiator: attempt.initiator || "", + initBlock: parseInt(attempt.initBlock) || 0, + lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, + approvalsCount, + approvedAddresses, + friendsNeeded: parseInt(friendGroup.friendsNeeded) || 0, + }; + }); +} + +/** + * Fetch all recovery attempts and their friend groups. + * + * @returns {{ data: Array, loading: boolean, fetch: () => void, friendGroupsMap: object }} + * - data: all processed recovery attempt objects + * - loading: whether data is being fetched + * - fetch: trigger a re-fetch + * - friendGroupsMap: raw map of lostAccount -> friendGroups for building friendGroupsData + */ export default function useQueryAllRecoveryAttempts() { - const { attempts, loading } = useRecoveryAttemptsData(); - return { data: attempts, loading }; + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [friendGroupsMap, setFriendGroupsMap] = useState({}); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); + + useEffect(() => { + if (!api) { + return; + } + + if (!api?.query.recovery?.attempt) { + setLoading(false); + setData([]); + setFriendGroupsMap({}); + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.attempt + .entries() + .then(async (entries) => { + if (cancelled) return; + + const lostAccounts = [ + ...new Set(entries.map(([key]) => key.args?.[0]?.toString())), + ]; + const fgMap = {}; + + await Promise.all( + lostAccounts.map(async (account) => { + try { + const value = await api.query.recovery.friendGroups(account); + const json = value.toJSON(); + fgMap[account] = json?.[0] || []; + } catch { + fgMap[account] = []; + } + }), + ); + + if (cancelled) return; + + const result = processAttempts(entries, fgMap); + + if (!cancelled) { + setData(result); + setFriendGroupsMap(fgMap); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query recovery attempts", error); + if (!cancelled) { + setData([]); + setFriendGroupsMap({}); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api, fetchCount]); + + return { data, loading, fetch, friendGroupsMap }; } diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryInheritors.js b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryInheritors.js index f29d905207..ae09a20a4b 100644 --- a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryInheritors.js +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryInheritors.js @@ -1,10 +1,13 @@ import { useContextApi } from "next-common/context/api"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; export default function useQueryAllRecoveryInheritors() { const api = useContextApi(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); + const [fetchCount, setFetchCount] = useState(0); + + const fetch = useCallback(() => setFetchCount((c) => c + 1), []); useEffect(() => { if (!api) { @@ -58,7 +61,7 @@ export default function useQueryAllRecoveryInheritors() { return () => { cancelled = true; }; - }, [api]); + }, [api, fetchCount]); - return { data, loading }; + return { data, loading, fetch }; } diff --git a/packages/next-common/components/recovery/common/hooks/useRecoveryAttemptsData.js b/packages/next-common/components/recovery/common/hooks/useRecoveryAttemptsData.js deleted file mode 100644 index 97b4c48d7b..0000000000 --- a/packages/next-common/components/recovery/common/hooks/useRecoveryAttemptsData.js +++ /dev/null @@ -1,147 +0,0 @@ -import { useContextApi } from "next-common/context/api"; -import { useCallback, useEffect, useState } from "react"; - -function bitfieldToIndices(bitfield) { - const indices = []; - let bitIndex = 0; - for (const byte of bitfield) { - if (typeof byte === "number") { - for (let b = 0; b < 8; b++) { - if (byte & (1 << b)) { - indices.push(bitIndex + b); - } - } - } - bitIndex += 8; - } - return indices; -} - -export function buildFriendGroupsData(friendGroupsMap) { - return Object.entries(friendGroupsMap).map(([account, groups]) => ({ - account, - friendGroups: groups.map((group, index) => ({ - index, - friends: group.friends || [], - friendsNeeded: parseInt(group.friendsNeeded) || 0, - inheritor: group.inheritor || "", - inheritancePriority: parseInt(group.inheritancePriority) || 0, - inheritanceDelay: parseInt(group.inheritanceDelay) || 0, - cancelDelay: parseInt(group.cancelDelay) || 0, - })), - })); -} - -function processAttempts(entries, friendGroupsMap) { - return entries.map(([storageKey, value]) => { - const lostAccount = storageKey.args?.[0]?.toString(); - const friendGroupIndex = storageKey.args?.[1]?.toNumber(); - - const json = value.toJSON(); - const attempt = json?.[0] || {}; - - const approvalsBitfield = attempt.approvals || []; - const approvedIndices = bitfieldToIndices(approvalsBitfield); - const approvalsCount = approvedIndices.length; - - const friendGroup = - (friendGroupsMap[lostAccount] || [])[friendGroupIndex] || {}; - const friends = friendGroup.friends || []; - const approvedAddresses = approvedIndices - .filter((i) => i < friends.length) - .map((i) => friends[i]); - - return { - lostAccount, - friendGroupIndex, - initiator: attempt.initiator || "", - initBlock: parseInt(attempt.initBlock) || 0, - lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, - approvalsCount, - approvedAddresses, - friendsNeeded: parseInt(friendGroup.friendsNeeded) || 0, - }; - }); -} - -/** - * Base hook that fetches all recovery attempts and their friend groups. - * - * @returns {{ attempts: Array, loading: boolean, fetch: () => void, friendGroupsMap: object }} - * - attempts: all processed recovery attempt objects - * - loading: whether data is being fetched - * - fetch: trigger a re-fetch - * - friendGroupsMap: raw map of lostAccount -> friendGroups for building friendGroupsData - */ -export default function useRecoveryAttemptsData() { - const api = useContextApi(); - const [attempts, setAttempts] = useState([]); - const [loading, setLoading] = useState(true); - const [friendGroupsMap, setFriendGroupsMap] = useState({}); - const [fetchCount, setFetchCount] = useState(0); - - const fetch = useCallback(() => setFetchCount((c) => c + 1), []); - - useEffect(() => { - if (!api) { - return; - } - - if (!api?.query.recovery?.attempt) { - setLoading(false); - setAttempts([]); - setFriendGroupsMap({}); - return; - } - - let cancelled = false; - setLoading(true); - - api.query.recovery.attempt - .entries() - .then(async (entries) => { - if (cancelled) return; - - const lostAccounts = [ - ...new Set(entries.map(([key]) => key.args?.[0]?.toString())), - ]; - const fgMap = {}; - - await Promise.all( - lostAccounts.map(async (account) => { - try { - const value = await api.query.recovery.friendGroups(account); - const json = value.toJSON(); - fgMap[account] = json?.[0] || []; - } catch { - fgMap[account] = []; - } - }), - ); - - if (cancelled) return; - - const result = processAttempts(entries, fgMap); - - if (!cancelled) { - setAttempts(result); - setFriendGroupsMap(fgMap); - setLoading(false); - } - }) - .catch((error) => { - console.error("Failed to query recovery attempts", error); - if (!cancelled) { - setAttempts([]); - setFriendGroupsMap({}); - setLoading(false); - } - }); - - return () => { - cancelled = true; - }; - }, [api, fetchCount]); - - return { attempts, loading, fetch, friendGroupsMap }; -} diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js index cc843c3b87..048f37c91b 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttempts.js @@ -1,11 +1,15 @@ import { useMemo } from "react"; -import useRecoveryAttemptsData, { +import useQueryAllRecoveryAttempts, { buildFriendGroupsData, -} from "next-common/components/recovery/common/hooks/useRecoveryAttemptsData"; +} from "next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts"; export default function useHelpOthersAttempts(address) { - const { attempts, loading, fetch, friendGroupsMap } = - useRecoveryAttemptsData(); + const { + data: attempts, + loading, + fetch, + friendGroupsMap, + } = useQueryAllRecoveryAttempts(); const data = useMemo(() => { if (!address) return []; diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js index 403bceb179..ffdc1b6464 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroups.js @@ -1,82 +1,22 @@ -import { useContextApi } from "next-common/context/api"; -import { useCallback, useEffect, useState } from "react"; -import { flattenRecoveryData } from "next-common/components/data/recovery/hooks/useQueryAllFriendGroups"; +import { useMemo } from "react"; +import useQueryAllFriendGroups, { + flattenRecoveryData, +} from "next-common/components/data/recovery/hooks/useQueryAllFriendGroups"; export default function useHelpOthersFriendGroups(address) { - const api = useContextApi(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [fetchCount, setFetchCount] = useState(0); + const { data: allData, loading, fetch } = useQueryAllFriendGroups(); - const fetch = useCallback(() => setFetchCount((c) => c + 1), []); + const data = useMemo(() => { + if (!address) return []; - useEffect(() => { - if (!api || !address) { - return; - } + const filtered = allData.filter((entry) => + entry.friendGroups.some((g) => + g.friends.some((f) => f?.toLowerCase() === address?.toLowerCase()), + ), + ); - if (!api?.query.recovery?.friendGroups) { - setLoading(false); - setData([]); - return; - } - - let cancelled = false; - setLoading(true); - - api.query.recovery.friendGroups - .entries() - .then((entries) => { - if (cancelled) return; - - const result = entries.map(([storageKey, value]) => { - const account = storageKey.args?.[0]?.toString(); - const jsonValue = value.toJSON(); - const friendGroupVec = Array.isArray(jsonValue?.[0]) - ? jsonValue[0] - : []; - - return { - account, - friendGroups: friendGroupVec.map((group, index) => ({ - index, - friends: group.friends || [], - friendsNeeded: parseInt(group.friendsNeeded) || 0, - inheritor: group.inheritor || "", - inheritancePriority: parseInt(group.inheritancePriority) || 0, - inheritanceDelay: parseInt(group.inheritanceDelay) || 0, - cancelDelay: parseInt(group.cancelDelay) || 0, - })), - }; - }); - - // Filter to entries where current address is a friend - const filtered = result.filter((entry) => - entry.friendGroups.some((g) => - g.friends.some((f) => f?.toLowerCase() === address?.toLowerCase()), - ), - ); - - // Flatten for table display - const flattened = flattenRecoveryData(filtered); - - if (!cancelled) { - setData(flattened); - setLoading(false); - } - }) - .catch((error) => { - console.error("Failed to query help others friend groups", error); - if (!cancelled) { - setData([]); - setLoading(false); - } - }); - - return () => { - cancelled = true; - }; - }, [api, address, fetchCount]); + return flattenRecoveryData(filtered); + }, [allData, address]); return { data, loading, fetch }; } diff --git a/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccounts.js b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccounts.js index f77bdde495..1c220635bb 100644 --- a/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccounts.js +++ b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccounts.js @@ -1,73 +1,15 @@ -import { useContextApi } from "next-common/context/api"; -import { useCallback, useEffect, useState } from "react"; +import { useMemo } from "react"; +import useQueryAllRecoveryInheritors from "next-common/components/data/recovery/hooks/useQueryAllRecoveryInheritors"; export default function useInheritedAccounts(address) { - const api = useContextApi(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [fetchCount, setFetchCount] = useState(0); - - const fetch = useCallback(() => setFetchCount((c) => c + 1), []); - - useEffect(() => { - if (!api || !address) { - return; - } - - if (!api?.query.recovery?.inheritor) { - setLoading(false); - setData([]); - return; - } - - let cancelled = false; - setLoading(true); - - api.query.recovery.inheritor - .entries() - .then((entries) => { - if (cancelled) return; - - const result = entries - .map(([storageKey, value]) => { - const account = storageKey.args?.[0]?.toString(); - const jsonValue = value.toJSON(); - - const inheritancePriority = parseInt(jsonValue?.[0]) || 0; - const inheritor = jsonValue?.[1] || ""; - const consideration = jsonValue?.[2] || {}; - const depositor = consideration.depositor || ""; - const ticket = parseInt(consideration.ticket) || 0; - - return { - account, - inheritancePriority, - inheritor, - depositor, - ticket, - }; - }) - .filter( - (item) => item.inheritor?.toLowerCase() === address?.toLowerCase(), - ); - - if (!cancelled) { - setData(result); - setLoading(false); - } - }) - .catch((error) => { - console.error("Failed to query inherited accounts", error); - if (!cancelled) { - setData([]); - setLoading(false); - } - }); - - return () => { - cancelled = true; - }; - }, [api, address, fetchCount]); + const { data: allData, loading, fetch } = useQueryAllRecoveryInheritors(); + + const data = useMemo(() => { + if (!address) return []; + return allData.filter( + (item) => item.inheritor?.toLowerCase() === address?.toLowerCase(), + ); + }, [allData, address]); return { data, loading, fetch }; } diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js index 26106f086b..422429d3dd 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyFriendGroups.js @@ -1,56 +1,14 @@ -import { useContextApi } from "next-common/context/api"; -import { useCallback, useEffect, useState } from "react"; +import { useMemo } from "react"; +import useQueryAllFriendGroups from "next-common/components/data/recovery/hooks/useQueryAllFriendGroups"; export default function useMyFriendGroups(address) { - const api = useContextApi(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [fetchCount, setFetchCount] = useState(0); + const { data: allData, loading, fetch } = useQueryAllFriendGroups(); - const fetch = useCallback(() => setFetchCount((c) => c + 1), []); - - useEffect(() => { - if (!api || !address) { - return; - } - - if (!api?.query.recovery?.friendGroups) { - return; - } - - let cancelled = false; - setLoading(true); - - api.query.recovery - .friendGroups(address) - .then((value) => { - if (cancelled) return; - const json = value.toJSON(); - const groups = Array.isArray(json?.[0]) ? json[0] : []; - const mapped = groups.map((group, index) => ({ - index, - friends: group.friends || [], - friendsNeeded: parseInt(group.friendsNeeded) || 0, - inheritor: group.inheritor || "", - inheritancePriority: parseInt(group.inheritancePriority) || 0, - inheritanceDelay: parseInt(group.inheritanceDelay) || 0, - cancelDelay: parseInt(group.cancelDelay) || 0, - })); - setData(mapped); - setLoading(false); - }) - .catch((error) => { - console.error("Failed to query friend groups", error); - if (!cancelled) { - setData([]); - setLoading(false); - } - }); - - return () => { - cancelled = true; - }; - }, [api, address, fetchCount]); + const data = useMemo(() => { + if (!address) return []; + const entry = allData.find((e) => e.account === address); + return entry?.friendGroups || []; + }, [allData, address]); return { data, loading, fetch }; } diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyInheritor.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyInheritor.js index 3f813f6bde..8b403f5373 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyInheritor.js +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyInheritor.js @@ -1,73 +1,15 @@ -import { useContextApi } from "next-common/context/api"; -import { useCallback, useEffect, useState } from "react"; +import { useMemo } from "react"; +import useQueryAllRecoveryInheritors from "next-common/components/data/recovery/hooks/useQueryAllRecoveryInheritors"; export default function useMyInheritor(address) { - const api = useContextApi(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [fetchCount, setFetchCount] = useState(0); - - const fetch = useCallback(() => setFetchCount((c) => c + 1), []); - - useEffect(() => { - if (!api || !address) { - return; - } - - if (!api?.query.recovery?.inheritor) { - setLoading(false); - setData([]); - return; - } - - let cancelled = false; - setLoading(true); - - api.query.recovery.inheritor - .entries() - .then((entries) => { - if (cancelled) return; - - const result = entries - .map(([storageKey, value]) => { - const account = storageKey.args?.[0]?.toString(); - const jsonValue = value.toJSON(); - - const inheritancePriority = parseInt(jsonValue?.[0]) || 0; - const inheritor = jsonValue?.[1] || ""; - const consideration = jsonValue?.[2] || {}; - const depositor = consideration.depositor || ""; - const ticket = parseInt(consideration.ticket) || 0; - - return { - account, - inheritancePriority, - inheritor, - depositor, - ticket, - }; - }) - .filter( - (item) => item.account?.toLowerCase() === address?.toLowerCase(), - ); - - if (!cancelled) { - setData(result); - setLoading(false); - } - }) - .catch((error) => { - console.error("Failed to query my inheritor", error); - if (!cancelled) { - setData([]); - setLoading(false); - } - }); - - return () => { - cancelled = true; - }; - }, [api, address, fetchCount]); + const { data: allData, loading, fetch } = useQueryAllRecoveryInheritors(); + + const data = useMemo(() => { + if (!address) return []; + return allData.filter( + (item) => item.account?.toLowerCase() === address?.toLowerCase(), + ); + }, [allData, address]); return { data, loading, fetch }; } diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js index d93417637b..340ff7b3f2 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts.js @@ -1,8 +1,8 @@ import { useMemo } from "react"; -import useRecoveryAttemptsData from "next-common/components/recovery/common/hooks/useRecoveryAttemptsData"; +import useQueryAllRecoveryAttempts from "next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts"; export default function useMyRecoveryAttempts(address) { - const { attempts, loading, fetch } = useRecoveryAttemptsData(); + const { data: attempts, loading, fetch } = useQueryAllRecoveryAttempts(); const data = useMemo(() => { if (!address) return []; From 28649ac2ffae8566dfe158e69925bcecd7bfa042 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 14:35:58 +0800 Subject: [PATCH 38/50] Update text, #6919 --- .../helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx index 22f1f00ded..2384a3052e 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx @@ -29,7 +29,7 @@ function RecoverButton({ lostAccount, friendGroupIndex, onRecover, disabled }) { if (disabled) { return ( - + Recover From 44dd27eea59d45e522c499f25cadb923789c2629 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 14:59:00 +0800 Subject: [PATCH 39/50] refactor recovery list columns, #6919 --- .../data/recovery/table/attemptsColumns.jsx | 185 +--------- .../data/recovery/table/columns.jsx | 144 +------- .../data/recovery/table/inheritorsColumns.jsx | 88 +---- .../components/recovery/common/columns.jsx | 236 +++++++++++++ .../hooks/useHelpOthersAttemptsColumns.jsx | 321 +++++------------- .../useHelpOthersFriendGroupsColumns.jsx | 205 +++-------- .../hooks/useInheritedAccountsColumns.jsx | 96 +----- .../hooks/useMyRecoveryInheritorColumns.jsx | 82 +---- .../hooks/useRecoveryAttemptsTableColumns.jsx | 149 +------- 9 files changed, 462 insertions(+), 1044 deletions(-) create mode 100644 packages/next-common/components/recovery/common/columns.jsx diff --git a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx index cffb51d62b..ccb619dcd7 100644 --- a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx +++ b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx @@ -1,178 +1,19 @@ -import AddressUser from "next-common/components/user/addressUser"; -import Tooltip from "next-common/components/tooltip"; -import { AddressesTooltip } from "next-common/components/multisigs/fields"; -import { useRelayChainApi } from "next-common/context/relayChain"; -import { useChainSettings } from "next-common/context/chain"; -import { estimateBlocksTime } from "next-common/utils"; -import useCall from "next-common/utils/hooks/useCall"; -import { isNil } from "lodash-es"; - -function BlockNumberWithTooltip({ height }) { - const api = useRelayChainApi(); - const { blockTime } = useChainSettings(); - const { value: currentNumber } = useCall(api?.query?.system?.number, []); - const currentHeight = currentNumber?.toNumber(); - - if (isNil(height) || isNil(currentHeight)) { - return ( - - #{height?.toLocaleString() || 0} - - ); - } - - const diff = Math.max(0, currentHeight - height); - const estimatedTime = diff > 0 ? estimateBlocksTime(diff, blockTime) : null; - - return ( - - - #{height?.toLocaleString() || 0} - - - ); -} +import { attemptColumns } from "next-common/components/recovery/common/columns"; export const desktopColumns = [ - { - name: "Lost Account", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Group Index", - className: "w-[120px] text-left", - render: (item) => ( - - ) - } - > - - #{item.friendGroupIndex} - - - ), - }, - { - name: "Initiator", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Init Block", - className: "w-[180px] text-left", - render: (item) => , - }, - { - name: "Last Approval Block", - className: "w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Threshold / Approvals", - className: "w-[160px] text-right", - render: (item) => ( - 0 && ( - - ) - } - > - - {item.fgGroup && ( - - {item.fgGroup?.friendsNeeded || 0} /{" "} - - )} - {item.approvalsCount} - - - ), - }, + attemptColumns.lostAccount("min-w-[200px] text-left"), + attemptColumns.groupIndex("w-[120px] text-left"), + attemptColumns.initiator("min-w-[200px] text-left"), + attemptColumns.initBlock("w-[180px] text-left"), + attemptColumns.lastApprovalBlock("w-[200px] text-left"), + attemptColumns.thresholdApprovals("w-[160px] text-right"), ]; export const mobileColumns = [ - { - name: "Lost Account", - className: "text-left", - render: (item) => , - }, - { - name: "Group Index", - className: "text-right", - render: (item) => ( - - ) - } - > - - #{item.friendGroupIndex} - - - ), - }, - { - name: "Initiator", - className: "text-left", - render: (item) => , - }, - { - name: "Init Block", - className: "text-right", - render: (item) => , - }, - { - name: "Last Approval Block", - className: "text-right", - render: (item) => ( - - ), - }, - { - name: "Threshold / Approvals", - className: "text-right", - render: (item) => ( - 0 && ( - - ) - } - > - - {item.fgGroup && ( - - {item.fgGroup?.friendsNeeded || 0} /{" "} - - )} - {item.approvalsCount} - - - ), - }, + attemptColumns.lostAccount("text-left"), + attemptColumns.groupIndex("text-right"), + attemptColumns.initiator("text-left"), + attemptColumns.initBlock("text-right"), + attemptColumns.lastApprovalBlock("text-right"), + attemptColumns.thresholdApprovals("text-right"), ]; diff --git a/packages/next-common/components/data/recovery/table/columns.jsx b/packages/next-common/components/data/recovery/table/columns.jsx index 7eea69229e..d3d7f076b6 100644 --- a/packages/next-common/components/data/recovery/table/columns.jsx +++ b/packages/next-common/components/data/recovery/table/columns.jsx @@ -1,133 +1,23 @@ -import AddressUser from "next-common/components/user/addressUser"; -import Tooltip from "next-common/components/tooltip"; -import { AddressesTooltip } from "next-common/components/multisigs/fields"; -import DelayBlock from "next-common/components/recovery/delayBlock"; -import { isNil } from "lodash-es"; - -function FriendsCount({ friends = [] }) { - if (isNil(friends)) { - return null; - } - - return ( - } - > - - {friends?.length || 0} - - - ); -} +import { friendGroupColumns } from "next-common/components/recovery/common/columns"; export const desktopColumns = [ - { - name: "Account", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Group Index", - className: "w-[120px] text-left", - render: (item) => ( - #{item.index} - ), - }, - { - name: "Priority", - className: "w-[100px] text-left", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Friends", - className: "w-[100px] text-left", - render: (item) => , - }, - { - name: "Threshold", - className: "w-[120px] text-left", - render: (item) => ( - - {item.friendsNeeded} - - ), - }, - { - name: "Inheritor", - className: "min-w-[160px] text-left", - render: (item) => ( - - ), - }, - { - name: "Inheritance Delay", - className: "w-[180px] text-left", - render: (item) => , - }, - { - name: "Cancel Delay", - className: "w-[160px] text-right", - render: (item) => , - }, + friendGroupColumns.account("min-w-[200px] text-left"), + friendGroupColumns.groupIndex("w-[120px] text-left"), + friendGroupColumns.priority("w-[100px] text-left"), + friendGroupColumns.friends("w-[100px] text-left"), + friendGroupColumns.threshold("w-[120px] text-left"), + friendGroupColumns.inheritor("min-w-[160px] text-left"), + friendGroupColumns.inheritanceDelay("w-[180px] text-left"), + friendGroupColumns.cancelDelay("w-[160px] text-right"), ]; export const mobileColumns = [ - { - name: "Account", - className: "text-left", - render: (item) => , - }, - { - name: "Group Index", - className: "text-right", - render: (item) => ( - #{item.index} - ), - }, - - { - name: "Priority", - className: "text-right", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Friends", - className: "text-left", - render: (item) => , - }, - - { - name: "Threshold", - className: "text-right", - render: (item) => ( - - {item.friendsNeeded} - - ), - }, - { - name: "Inheritor", - className: "text-right", - render: (item) => , - }, - { - name: "Inheritance Delay", - className: "text-right", - render: (item) => , - }, - { - name: "Cancel Delay", - className: "text-right", - render: (item) => , - }, + friendGroupColumns.account("text-left"), + friendGroupColumns.groupIndex("text-right"), + friendGroupColumns.priority("text-right"), + friendGroupColumns.friends("text-left"), + friendGroupColumns.threshold("text-right"), + friendGroupColumns.inheritor("text-right"), + friendGroupColumns.inheritanceDelay("text-right"), + friendGroupColumns.cancelDelay("text-right"), ]; diff --git a/packages/next-common/components/data/recovery/table/inheritorsColumns.jsx b/packages/next-common/components/data/recovery/table/inheritorsColumns.jsx index 2bdcca48e7..9d07da3e16 100644 --- a/packages/next-common/components/data/recovery/table/inheritorsColumns.jsx +++ b/packages/next-common/components/data/recovery/table/inheritorsColumns.jsx @@ -1,83 +1,17 @@ -import AddressUser from "next-common/components/user/addressUser"; -import ValueDisplay from "next-common/components/valueDisplay"; -import { toPrecision } from "next-common/utils"; -import { useChainSettings } from "next-common/context/chain"; -import { isNil } from "lodash-es"; - -function TicketCell({ ticket }) { - const { decimals, symbol } = useChainSettings(); - if (isNil(ticket)) { - return null; - } - return ; -} +import { inheritorColumns } from "next-common/components/recovery/common/columns"; export const desktopColumns = [ - { - name: "Lost Account", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Inheritor", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Priority", - className: "w-[120px] text-left", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Depositor", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Deposit", - className: "w-[180px] text-right", - render: (item) => , - }, + inheritorColumns.lostAccount("min-w-[200px] text-left"), + inheritorColumns.inheritor("min-w-[200px] text-left"), + inheritorColumns.priority("w-[120px] text-left"), + inheritorColumns.depositor("min-w-[200px] text-left"), + inheritorColumns.deposit("w-[180px] text-right"), ]; export const mobileColumns = [ - { - name: "Lost Account", - className: "text-left", - render: (item) => , - }, - { - name: "Inheritor", - className: "text-left", - render: (item) => , - }, - { - name: "Priority", - className: "text-right", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Depositor", - className: "text-left", - render: (item) => , - }, - { - name: "Deposit", - className: "text-right", - render: (item) => , - }, + inheritorColumns.lostAccount("text-left"), + inheritorColumns.inheritor("text-left"), + inheritorColumns.priority("text-right"), + inheritorColumns.depositor("text-left"), + inheritorColumns.deposit("text-right"), ]; diff --git a/packages/next-common/components/recovery/common/columns.jsx b/packages/next-common/components/recovery/common/columns.jsx new file mode 100644 index 0000000000..645300d306 --- /dev/null +++ b/packages/next-common/components/recovery/common/columns.jsx @@ -0,0 +1,236 @@ +"use client"; + +import AddressUser from "next-common/components/user/addressUser"; +import Tooltip from "next-common/components/tooltip"; +import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import BlockNumberWithTooltip from "next-common/components/recovery/myRecovery/blockNumberWithTooltip"; +import DelayBlock from "next-common/components/recovery/delayBlock"; +import ValueDisplay from "next-common/components/valueDisplay"; +import { toPrecision } from "next-common/utils"; +import { useChainSettings } from "next-common/context/chain"; +import { isNil } from "lodash-es"; + +// +// Shared components +// + +export function FriendsCount({ friends = [] }) { + if (isNil(friends)) { + return null; + } + + return ( + } + > + + {friends?.length || 0} + + + ); +} + +function TicketCellInner({ ticket }) { + const { decimals, symbol } = useChainSettings(); + if (isNil(ticket)) { + return null; + } + return ; +} + +// +// Attempt table columns +// + +export const attemptColumns = { + lostAccount: (className) => ({ + name: "Lost Account", + className, + render: (item) => ( + + ), + }), + + groupIndex: (className) => ({ + name: "Group Index", + className, + render: (item) => ( + + ) + } + > + + #{item.friendGroupIndex} + + + ), + }), + + initiator: (className) => ({ + name: "Initiator", + className, + render: (item) => ( + + ), + }), + + initBlock: (className) => ({ + name: "Init Block", + className, + render: (item) => , + }), + + lastApprovalBlock: (className) => ({ + name: "Last Approval Block", + className, + render: (item) => ( + + ), + }), + + thresholdApprovals: (className) => ({ + name: "Threshold / Approvals", + className, + render: (item) => ( + 0 && ( + + ) + } + > + + {item.fgGroup && ( + + {item.fgGroup?.friendsNeeded || 0} /{" "} + + )} + {item.approvalsCount} + + + ), + }), +}; + +// +// Friend group table columns +// + +export const friendGroupColumns = { + account: (className) => ({ + name: "Account", + className, + render: (item) => ( + + ), + }), + + groupIndex: (className) => ({ + name: "Group Index", + className, + render: (item) => ( + #{item.index} + ), + }), + + priority: (className) => ({ + name: "Priority", + className, + render: (item) => ( + + {item.inheritancePriority} + + ), + }), + + friends: (className) => ({ + name: "Friends", + className, + render: (item) => , + }), + + threshold: (className) => ({ + name: "Threshold", + className, + render: (item) => ( + + {item.friendsNeeded} + + ), + }), + + inheritor: (className) => ({ + name: "Inheritor", + className, + render: (item) => ( + + ), + }), + + inheritanceDelay: (className) => ({ + name: "Inheritance Delay", + className, + render: (item) => , + }), + + cancelDelay: (className) => ({ + name: "Cancel Delay", + className, + render: (item) => , + }), +}; + +// +// Inheritor table columns +// + +export const inheritorColumns = { + lostAccount: (className) => ({ + name: "Lost Account", + className, + render: (item) => ( + + ), + }), + + inheritor: (className) => ({ + name: "Inheritor", + className, + render: (item) => ( + + ), + }), + + priority: (className) => ({ + name: "Priority", + className, + render: (item) => ( + + {item.inheritancePriority} + + ), + }), + + depositor: (className) => ({ + name: "Depositor", + className, + render: (item) => ( + + ), + }), + + deposit: (className) => ({ + name: "Deposit", + className, + render: (item) => , + }), +}; diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx index 0f34194bf3..15919e4348 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersAttemptsColumns.jsx @@ -2,13 +2,11 @@ import { useMemo, useState } from "react"; import Tooltip from "next-common/components/tooltip"; -import { AddressesTooltip } from "next-common/components/multisigs/fields"; -import AddressUser from "next-common/components/user/addressUser"; +import { attemptColumns } from "next-common/components/recovery/common/columns"; import { isSameAddress, estimateBlocksTime } from "next-common/utils"; import { isNil } from "lodash-es"; import { useChainSettings } from "next-common/context/chain"; import useRelayChainBlockNumber from "next-common/hooks/useRelayChainBlockNumber"; -import BlockNumberWithTooltip from "../../myRecovery/blockNumberWithTooltip"; import CancelAttemptDialog from "../cancelAttemptDialog"; import ApproveAttemptDialog from "../approveAttemptDialog"; import FinishAttemptDialog from "../finishAttemptDialog"; @@ -164,256 +162,105 @@ function FinishButton({ ); } +function HelpOthersAttemptActions({ + lostAccount, + friendGroupIndex, + initBlock, + lastApprovalBlock, + initiator, + approvalsCount, + approvedAddresses, + fgGroup, + friendsNeeded, + address, + onAction, +}) { + const threshold = fgGroup?.friendsNeeded || friendsNeeded || 0; + const canFinish = (approvalsCount || 0) >= threshold; + + if (canFinish) { + return ( +
+ + {isSameAddress(initiator, address) && ( + + )} +
+ ); + } + + return isSameAddress(initiator, address) ? ( + + ) : ( + + isSameAddress(a, address), + )} + /> + ); +} + export default function useHelpOthersAttemptsColumns(address, onAction) { return useMemo(() => { const desktopColumns = [ + attemptColumns.lostAccount("min-w-[200px] text-left"), + attemptColumns.groupIndex("w-[120px] text-left"), + attemptColumns.initiator("min-w-[160px] text-left"), + attemptColumns.initBlock("w-[140px] text-left"), + attemptColumns.lastApprovalBlock("w-[180px] text-left"), + attemptColumns.thresholdApprovals("w-[160px] text-left"), { - name: "Lost Account", - className: "min-w-[200px] text-left", + name: "Action", + className: "w-[100px] text-right", render: (item) => ( - ), }, - { - name: "Group Index", - className: "w-[120px] text-left", - render: (item) => ( - - ) - } - > - - #{item.friendGroupIndex} - - - ), - }, - { - name: "Initiator", - className: "min-w-[160px] text-left", - render: (item) => ( - - ), - }, - { - name: "Init Block", - className: "w-[140px] text-left", - render: (item) => , - }, - { - name: "Last Approval Block", - className: "w-[180px] text-left", - render: (item) => ( - - ), - }, - { - name: "Threshold / Approvals", - className: "w-[160px] text-left", - render: (item) => ( - 0 && ( - - ) - } - > - - {item.fgGroup && ( - - {item.fgGroup?.friendsNeeded || 0} /{" "} - - )} - {item.approvalsCount} - - - ), - }, - { - name: "Action", - className: "w-[100px] text-right", - render: (item) => { - const threshold = - item.fgGroup?.friendsNeeded || item.friendsNeeded || 0; - const canFinish = (item.approvalsCount || 0) >= threshold; - - if (canFinish) { - return ( -
- - {isSameAddress(item.initiator, address) && ( - - )} -
- ); - } - - return isSameAddress(item.initiator, address) ? ( - - ) : ( - - isSameAddress(a, address), - )} - /> - ); - }, - }, ]; const mobileColumns = [ + attemptColumns.lostAccount("text-left"), + attemptColumns.groupIndex("text-right"), + attemptColumns.initiator("text-left"), + attemptColumns.initBlock("text-right"), + attemptColumns.lastApprovalBlock("text-right"), + attemptColumns.thresholdApprovals("text-right"), { - name: "Lost Account", - className: "text-left", - render: (item) => , - }, - { - name: "Group Index", - className: "text-right", - render: (item) => ( - - ) - } - > - - #{item.friendGroupIndex} - - - ), - }, - { - name: "Initiator", + name: "Action", className: "text-left", - render: (item) => , - }, - { - name: "Init Block", - className: "text-right", - render: (item) => , - }, - { - name: "Last Approval Block", - className: "text-right", - render: (item) => ( - - ), - }, - { - name: "Threshold / Approvals", - className: "text-right", render: (item) => ( - 0 && ( - - ) - } - > - - {item.fgGroup && ( - - {item.fgGroup?.friendsNeeded || 0} /{" "} - - )} - {item.approvalsCount} - - + ), }, - { - name: "Action", - className: "text-left", - render: (item) => { - const threshold = - item.fgGroup?.friendsNeeded || item.friendsNeeded || 0; - const canFinish = (item.approvalsCount || 0) >= threshold; - - if (canFinish) { - return ( -
- - {isSameAddress(item.initiator, address) && ( - - )} -
- ); - } - - return isSameAddress(item.initiator, address) ? ( - - ) : ( - - isSameAddress(a, address), - )} - /> - ); - }, - }, ]; return { desktopColumns, mobileColumns }; diff --git a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx index 2384a3052e..8e52437bd1 100644 --- a/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx +++ b/packages/next-common/components/recovery/helpOthers/hooks/useHelpOthersFriendGroupsColumns.jsx @@ -1,29 +1,10 @@ "use client"; import { useMemo, useState } from "react"; -import AddressUser from "next-common/components/user/addressUser"; import Tooltip from "next-common/components/tooltip"; -import { AddressesTooltip } from "next-common/components/multisigs/fields"; -import DelayBlock from "next-common/components/recovery/delayBlock"; -import { isNil } from "lodash-es"; +import { friendGroupColumns } from "next-common/components/recovery/common/columns"; import InitiateAttemptDialog from "../initiateAttemptDialog"; -function FriendsCount({ friends = [] }) { - if (isNil(friends)) { - return null; - } - - return ( - } - > - - {friends?.length || 0} - - - ); -} - function RecoverButton({ lostAccount, friendGroupIndex, onRecover, disabled }) { const [showDialog, setShowDialog] = useState(false); @@ -60,157 +41,75 @@ function RecoverButton({ lostAccount, friendGroupIndex, onRecover, disabled }) { ); } +function HelpOthersFriendGroupActions({ + account, + index, + attemptsData, + onRecover, +}) { + const hasAttempt = attemptsData.some( + (a) => a.lostAccount === account && a.friendGroupIndex === index, + ); + + return ( + + ); +} + export default function useHelpOthersFriendGroupsColumns( onRecover, attemptsData = [], ) { return useMemo(() => { const desktopColumns = [ - { - name: "Account", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Group Index", - className: "w-[120px] text-left", - render: (item) => ( - #{item.index} - ), - }, - { - name: "Priority", - className: "w-[100px] text-left", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Friends", - className: "w-[100px] text-left", - render: (item) => , - }, - { - name: "Threshold", - className: "w-[120px] text-left", - render: (item) => ( - - {item.friendsNeeded} - - ), - }, - { - name: "Inheritor", - className: "min-w-[160px] text-left", - render: (item) => ( - - ), - }, - { - name: "Inheritance Delay", - className: "w-[180px] text-left", - render: (item) => , - }, - { - name: "Cancel Delay", - className: "w-[160px] text-left", - render: (item) => , - }, + friendGroupColumns.account("min-w-[200px] text-left"), + friendGroupColumns.groupIndex("w-[120px] text-left"), + friendGroupColumns.priority("w-[100px] text-left"), + friendGroupColumns.friends("w-[100px] text-left"), + friendGroupColumns.threshold("w-[120px] text-left"), + friendGroupColumns.inheritor("min-w-[160px] text-left"), + friendGroupColumns.inheritanceDelay("w-[180px] text-left"), + friendGroupColumns.cancelDelay("w-[160px] text-left"), { name: "Action", className: "w-[100px] text-right", - render: (item) => { - const hasAttempt = attemptsData.some( - (a) => - a.lostAccount === item.account && - a.friendGroupIndex === item.index, - ); - return ( - - ); - }, + render: (item) => ( + + ), }, ]; const mobileColumns = [ + friendGroupColumns.account("text-left"), + friendGroupColumns.groupIndex("text-right"), + friendGroupColumns.priority("text-right"), + friendGroupColumns.friends("text-left"), + friendGroupColumns.threshold("text-right"), + friendGroupColumns.inheritor("text-right"), + friendGroupColumns.inheritanceDelay("text-right"), + friendGroupColumns.cancelDelay("text-right"), { - name: "Account", - className: "text-left", - render: (item) => , - }, - { - name: "Group Index", - className: "text-right", - render: (item) => ( - #{item.index} - ), - }, - { - name: "Priority", - className: "text-right", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Friends", + name: "Action", className: "text-left", - render: (item) => , - }, - { - name: "Threshold", - className: "text-right", render: (item) => ( - - {item.friendsNeeded} - + ), }, - { - name: "Inheritor", - className: "text-right", - render: (item) => , - }, - { - name: "Inheritance Delay", - className: "text-right", - render: (item) => , - }, - { - name: "Cancel Delay", - className: "text-right", - render: (item) => , - }, - { - name: "Action", - className: "text-left", - render: (item) => { - const hasAttempt = attemptsData.some( - (a) => - a.lostAccount === item.account && - a.friendGroupIndex === item.index, - ); - return ( - - ); - }, - }, ]; return { desktopColumns, mobileColumns }; diff --git a/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx index 93460c63e8..dd8cf504c4 100644 --- a/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx +++ b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx @@ -1,21 +1,9 @@ "use client"; import { useMemo, useState } from "react"; -import AddressUser from "next-common/components/user/addressUser"; -import ValueDisplay from "next-common/components/valueDisplay"; -import { toPrecision } from "next-common/utils"; -import { useChainSettings } from "next-common/context/chain"; -import { isNil } from "lodash-es"; +import { inheritorColumns } from "next-common/components/recovery/common/columns"; import ControlInheritedAccountDialog from "../controlInheritedAccountDialog"; -function TicketCell({ ticket }) { - const { decimals, symbol } = useChainSettings(); - if (isNil(ticket)) { - return null; - } - return ; -} - function ControlButton({ item }) { const [showDialog, setShowDialog] = useState(false); @@ -38,85 +26,35 @@ function ControlButton({ item }) { ); } +function InheritedAccountActions({ item }) { + return ; +} + export default function useInheritedAccountsColumns() { return useMemo(() => { const desktopColumns = [ - { - name: "Lost Account", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Inheritor", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Priority", - className: "w-[120px] text-left", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Depositor", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Deposit", - className: "w-[180px] text-left", - render: (item) => , - }, + inheritorColumns.lostAccount("min-w-[200px] text-left"), + inheritorColumns.inheritor("min-w-[200px] text-left"), + inheritorColumns.priority("w-[120px] text-left"), + inheritorColumns.depositor("min-w-[200px] text-left"), + inheritorColumns.deposit("w-[180px] text-left"), { name: "Action", className: "w-[100px] text-right", - render: (item) => , + render: (item) => , }, ]; const mobileColumns = [ - { - name: "Lost Account", - className: "text-left", - render: (item) => , - }, - { - name: "Inheritor", - className: "text-left", - render: (item) => , - }, - { - name: "Priority", - className: "text-right", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Depositor", - className: "text-left", - render: (item) => , - }, - { - name: "Deposit", - className: "text-right", - render: (item) => , - }, + inheritorColumns.lostAccount("text-left"), + inheritorColumns.inheritor("text-left"), + inheritorColumns.priority("text-right"), + inheritorColumns.depositor("text-left"), + inheritorColumns.deposit("text-right"), { name: "Action", className: "text-left", - render: (item) => , + render: (item) => , }, ]; diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx index f1392157c8..90586f5185 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx +++ b/packages/next-common/components/recovery/myRecovery/hooks/useMyRecoveryInheritorColumns.jsx @@ -1,22 +1,10 @@ "use client"; import { useMemo, useState } from "react"; -import AddressUser from "next-common/components/user/addressUser"; import Tooltip from "next-common/components/tooltip"; -import ValueDisplay from "next-common/components/valueDisplay"; -import { toPrecision } from "next-common/utils"; -import { useChainSettings } from "next-common/context/chain"; -import { isNil } from "lodash-es"; +import { inheritorColumns } from "next-common/components/recovery/common/columns"; import RevokeInheritorDialog from "../revokeInheritorDialog"; -function TicketCell({ ticket }) { - const { decimals, symbol } = useChainSettings(); - if (isNil(ticket)) { - return null; - } - return ; -} - function RevokeButton({ onRevoke }) { const [showDialog, setShowDialog] = useState(false); @@ -41,73 +29,33 @@ function RevokeButton({ onRevoke }) { ); } +function MyInheritorActions({ onRevoke }) { + return ; +} + export default function useMyRecoveryInheritorColumns(onRevoke) { return useMemo(() => { const desktopColumns = [ - { - name: "Inheritor", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Priority", - className: "w-[120px] text-left", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Depositor", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Deposit", - className: "w-[180px] text-left", - render: (item) => , - }, + inheritorColumns.inheritor("min-w-[200px] text-left"), + inheritorColumns.priority("w-[120px] text-left"), + inheritorColumns.depositor("min-w-[200px] text-left"), + inheritorColumns.deposit("w-[180px] text-left"), { name: "Action", className: "w-[100px] text-right", - render: () => , + render: () => , }, ]; const mobileColumns = [ - { - name: "Inheritor", - className: "text-left", - render: (item) => , - }, - { - name: "Priority", - className: "text-right", - render: (item) => ( - - {item.inheritancePriority} - - ), - }, - { - name: "Depositor", - className: "text-left", - render: (item) => , - }, - { - name: "Deposit", - className: "text-right", - render: (item) => , - }, + inheritorColumns.inheritor("text-left"), + inheritorColumns.priority("text-right"), + inheritorColumns.depositor("text-left"), + inheritorColumns.deposit("text-right"), { name: "Action", className: "text-left", - render: () => , + render: () => , }, ]; diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx b/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx index 83199b9aa5..785aebe759 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx +++ b/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx @@ -1,84 +1,26 @@ "use client"; import { useMemo } from "react"; -import Tooltip from "next-common/components/tooltip"; -import { AddressesTooltip } from "next-common/components/multisigs/fields"; -import AddressUser from "next-common/components/user/addressUser"; -import BlockNumberWithTooltip from "../blockNumberWithTooltip"; +import { attemptColumns } from "next-common/components/recovery/common/columns"; import SlashButton from "../slashButton"; +function AttemptActions({ friendGroupIndex, onSlash }) { + return ; +} + export default function useRecoveryAttemptsTableColumns(onSlash) { return useMemo(() => { const desktopColumns = [ - { - name: "Group Index", - className: "w-[120px] text-left", - render: (item) => ( - - ) - } - > - - #{item.friendGroupIndex} - - - ), - }, - { - name: "Initiator", - className: "min-w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Init Block", - className: "w-[180px] text-left", - render: (item) => , - }, - { - name: "Last Approval Block", - className: "w-[200px] text-left", - render: (item) => ( - - ), - }, - { - name: "Threshold / Approvals", - className: "w-[160px] text-left", - render: (item) => ( - 0 && ( - - ) - } - > - - {item.fgGroup && ( - - {item.fgGroup?.friendsNeeded || 0} /{" "} - - )} - {item.approvalsCount} - - - ), - }, + attemptColumns.groupIndex("w-[120px] text-left"), + attemptColumns.initiator("min-w-[200px] text-left"), + attemptColumns.initBlock("w-[180px] text-left"), + attemptColumns.lastApprovalBlock("w-[200px] text-left"), + attemptColumns.thresholdApprovals("w-[160px] text-left"), { name: "Action", className: "w-[100px] text-right", render: (item) => ( - @@ -87,73 +29,16 @@ export default function useRecoveryAttemptsTableColumns(onSlash) { ]; const mobileColumns = [ - { - name: "Group Index", - className: "text-right", - render: (item) => ( - - ) - } - > - - #{item.friendGroupIndex} - - - ), - }, - { - name: "Initiator", - className: "text-left", - render: (item) => , - }, - { - name: "Init Block", - className: "text-right", - render: (item) => , - }, - { - name: "Last Approval Block", - className: "text-right", - render: (item) => ( - - ), - }, - { - name: "Threshold / Approvals", - className: "text-right", - render: (item) => ( - 0 && ( - - ) - } - > - - {item.fgGroup && ( - - {item.fgGroup?.friendsNeeded || 0} /{" "} - - )} - {item.approvalsCount} - - - ), - }, + attemptColumns.groupIndex("text-right"), + attemptColumns.initiator("text-left"), + attemptColumns.initBlock("text-right"), + attemptColumns.lastApprovalBlock("text-right"), + attemptColumns.thresholdApprovals("text-right"), { name: "Action", className: "text-left", render: (item) => ( - From b569b00ff68d8f19c5bd3ab2b01a0105a00814d5 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 15:31:37 +0800 Subject: [PATCH 40/50] Add recovery prompts, #6919 --- .../overview/accountInfo/accountInfoPanel.js | 10 +++- .../useOngoingRecoveryAttemptsPrompt.jsx | 60 +++++++++++++++++++ .../components/useRecoveryInheritorPrompt.jsx | 60 +++++++++++++++++++ packages/next-common/utils/constants.js | 6 ++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 packages/next-common/components/overview/accountInfo/components/useOngoingRecoveryAttemptsPrompt.jsx create mode 100644 packages/next-common/components/overview/accountInfo/components/useRecoveryInheritorPrompt.jsx diff --git a/packages/next-common/components/overview/accountInfo/accountInfoPanel.js b/packages/next-common/components/overview/accountInfo/accountInfoPanel.js index 0a96997004..041693ef09 100644 --- a/packages/next-common/components/overview/accountInfo/accountInfoPanel.js +++ b/packages/next-common/components/overview/accountInfo/accountInfoPanel.js @@ -13,6 +13,8 @@ import { NeutralPanel } from "next-common/components/styled/containers/neutralPa import { tryConvertToEvmAddress } from "next-common/utils/mixedChainUtil"; import AccountPanelScrollPrompt from "./components/accountPanelScrollPrompt"; import ExtensionUpdatePrompt from "./components/extensionUpdatePrompt"; +import OngoingRecoveryAttemptsPrompt from "./components/useOngoingRecoveryAttemptsPrompt"; +import RecoveryInheritorPrompt from "./components/useRecoveryInheritorPrompt"; import { useAccountTransferPopup } from "./hook/useAccountTransferPopup"; import dynamic from "next/dynamic"; import { useState } from "react"; @@ -304,7 +306,7 @@ export function AccountHead({ width }) { } export default function AccountInfoPanel() { - const { hasIdentityVerification } = useChainSettings(); + const { hasIdentityVerification, modules } = useChainSettings(); const width = useWindowWidthContext(); if (isNil(width)) { @@ -321,6 +323,12 @@ export default function AccountInfoPanel() { {hasIdentityVerification && } + {modules?.recovery && ( + + + + + )} ); diff --git a/packages/next-common/components/overview/accountInfo/components/useOngoingRecoveryAttemptsPrompt.jsx b/packages/next-common/components/overview/accountInfo/components/useOngoingRecoveryAttemptsPrompt.jsx new file mode 100644 index 0000000000..06477b823c --- /dev/null +++ b/packages/next-common/components/overview/accountInfo/components/useOngoingRecoveryAttemptsPrompt.jsx @@ -0,0 +1,60 @@ +import { + PromptTypes, + ScrollPromptItemWrapper, +} from "next-common/components/scrollPrompt"; +import { CACHE_KEY } from "next-common/utils/constants"; +import { useCookieValue } from "next-common/utils/hooks/useCookieValue"; +import Link from "next-common/components/link"; +import { useMemo } from "react"; +import useRealAddress from "next-common/utils/hooks/useRealAddress"; +import useMyRecoveryAttempts from "next-common/components/recovery/myRecovery/hooks/useMyRecoveryAttempts"; +import { isEmpty } from "lodash-es"; + +function useOngoingRecoveryAttemptsPrompt() { + const address = useRealAddress(); + const { data, loading } = useMyRecoveryAttempts(address); + const [visible, setVisible] = useCookieValue( + CACHE_KEY.ongoingRecoveryAttemptsPrompt, + true, + ); + const hasAttempts = !loading && data?.length > 0; + + return useMemo(() => { + if (!hasAttempts || !visible) { + return {}; + } + + return { + key: CACHE_KEY.ongoingRecoveryAttemptsPrompt, + message: ( +
+ You have ongoing recovery attempts. See details{" "} + + here + +
+ ), + type: PromptTypes.WARNING, + close: () => setVisible(false, { expires: 15 }), + }; + }, [hasAttempts, visible, setVisible]); +} + +export default function OngoingRecoveryAttemptsPrompt({ onClose }) { + const prompt = useOngoingRecoveryAttemptsPrompt(); + if (isEmpty(prompt)) { + return null; + } + + return ( + { + onClose?.(); + prompt?.close(); + }, + }} + /> + ); +} diff --git a/packages/next-common/components/overview/accountInfo/components/useRecoveryInheritorPrompt.jsx b/packages/next-common/components/overview/accountInfo/components/useRecoveryInheritorPrompt.jsx new file mode 100644 index 0000000000..74c5f84607 --- /dev/null +++ b/packages/next-common/components/overview/accountInfo/components/useRecoveryInheritorPrompt.jsx @@ -0,0 +1,60 @@ +import { + PromptTypes, + ScrollPromptItemWrapper, +} from "next-common/components/scrollPrompt"; +import { CACHE_KEY } from "next-common/utils/constants"; +import { useCookieValue } from "next-common/utils/hooks/useCookieValue"; +import Link from "next-common/components/link"; +import { useMemo } from "react"; +import useRealAddress from "next-common/utils/hooks/useRealAddress"; +import useMyInheritor from "next-common/components/recovery/myRecovery/hooks/useMyInheritor"; +import { isEmpty } from "lodash-es"; + +function useRecoveryInheritorPrompt() { + const address = useRealAddress(); + const { data, loading } = useMyInheritor(address); + const [visible, setVisible] = useCookieValue( + CACHE_KEY.recoveryInheritorPrompt, + true, + ); + const hasInheritor = !loading && data?.length > 0; + + return useMemo(() => { + if (!hasInheritor || !visible) { + return {}; + } + + return { + key: CACHE_KEY.recoveryInheritorPrompt, + message: ( +
+ You have inheritor settings. See details{" "} + + here + +
+ ), + type: PromptTypes.WARNING, + close: () => setVisible(false, { expires: 15 }), + }; + }, [hasInheritor, visible, setVisible]); +} + +export default function RecoveryInheritorPrompt({ onClose }) { + const prompt = useRecoveryInheritorPrompt(); + if (isEmpty(prompt)) { + return null; + } + + return ( + { + onClose?.(); + prompt?.close(); + }, + }} + /> + ); +} diff --git a/packages/next-common/utils/constants.js b/packages/next-common/utils/constants.js index cb82197c4a..4fb9353fce 100644 --- a/packages/next-common/utils/constants.js +++ b/packages/next-common/utils/constants.js @@ -147,6 +147,9 @@ export const CACHE_KEY = { nominatorClaimRewardPrompt: "nominator-claim-reward-prompt", poolWithdrawUnbondedPrompt: "pool-withdraw-unbonded-prompt", poolClaimRewardPrompt: "pool-claim-reward-prompt", + + ongoingRecoveryAttemptsPrompt: "ongoing-recovery-attempts-prompt", + recoveryInheritorPrompt: "recovery-inheritor-prompt", }; export const ADDRESS_CACHE_KEYS = [ @@ -168,6 +171,9 @@ export const ADDRESS_CACHE_KEYS = [ CACHE_KEY.nominatorClaimRewardPrompt, CACHE_KEY.poolWithdrawUnbondedPrompt, CACHE_KEY.poolClaimRewardPrompt, + + CACHE_KEY.ongoingRecoveryAttemptsPrompt, + CACHE_KEY.recoveryInheritorPrompt, ]; export const CHAIN = process.env.NEXT_PUBLIC_CHAIN; From b669a3828fe37808c4dccd2fa3eff159752e8527 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 16:06:45 +0800 Subject: [PATCH 41/50] Add cancel action for lost account, #6919 --- .../recovery/myRecovery/attemptsTable.jsx | 7 ++-- .../recovery/myRecovery/cancelButton.jsx | 35 +++++++++++++++++++ .../hooks/useRecoveryAttemptsTableColumns.jsx | 26 ++++++++++---- .../myRecovery/recoveryAttemptsSection.jsx | 1 + 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 packages/next-common/components/recovery/myRecovery/cancelButton.jsx diff --git a/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx b/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx index 68d9c53e94..d53ae67170 100644 --- a/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx +++ b/packages/next-common/components/recovery/myRecovery/attemptsTable.jsx @@ -22,14 +22,17 @@ export default function MyRecoveryAttemptsTable({ loading: isLoading, friendGroups, onSlash = () => {}, + onCancel = () => {}, }) { const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); const [totalCount, setTotalCount] = useState(0); const [loading, setLoading] = useState(true); - const { desktopColumns, mobileColumns } = - useRecoveryAttemptsTableColumns(onSlash); + const { desktopColumns, mobileColumns } = useRecoveryAttemptsTableColumns( + onSlash, + onCancel, + ); const { page, component: pageComponent } = usePaginationComponent( totalCount, diff --git a/packages/next-common/components/recovery/myRecovery/cancelButton.jsx b/packages/next-common/components/recovery/myRecovery/cancelButton.jsx new file mode 100644 index 0000000000..a50fea9403 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/cancelButton.jsx @@ -0,0 +1,35 @@ +"use client"; + +import { useState } from "react"; +import Tooltip from "next-common/components/tooltip"; +import CancelAttemptDialog from "next-common/components/recovery/helpOthers/cancelAttemptDialog"; + +export default function CancelButton({ + lostAccount, + friendGroupIndex, + onCancel, +}) { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + {showDialog && ( + setShowDialog(false)} + lostAccount={lostAccount} + friendGroupIndex={friendGroupIndex} + onInBlock={onCancel} + /> + )} + + + + + ); +} diff --git a/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx b/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx index 785aebe759..f13de773eb 100644 --- a/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx +++ b/packages/next-common/components/recovery/myRecovery/hooks/useRecoveryAttemptsTableColumns.jsx @@ -3,12 +3,22 @@ import { useMemo } from "react"; import { attemptColumns } from "next-common/components/recovery/common/columns"; import SlashButton from "../slashButton"; +import CancelButton from "../cancelButton"; -function AttemptActions({ friendGroupIndex, onSlash }) { - return ; +function AttemptActions({ lostAccount, friendGroupIndex, onSlash, onCancel }) { + return ( +
+ + +
+ ); } -export default function useRecoveryAttemptsTableColumns(onSlash) { +export default function useRecoveryAttemptsTableColumns(onSlash, onCancel) { return useMemo(() => { const desktopColumns = [ attemptColumns.groupIndex("w-[120px] text-left"), @@ -18,11 +28,13 @@ export default function useRecoveryAttemptsTableColumns(onSlash) { attemptColumns.thresholdApprovals("w-[160px] text-left"), { name: "Action", - className: "w-[100px] text-right", + className: "w-[160px] text-right", render: (item) => ( ), }, @@ -36,16 +48,18 @@ export default function useRecoveryAttemptsTableColumns(onSlash) { attemptColumns.thresholdApprovals("text-right"), { name: "Action", - className: "text-left", + className: "text-right", render: (item) => ( ), }, ]; return { desktopColumns, mobileColumns }; - }, [onSlash]); + }, [onSlash, onCancel]); } diff --git a/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx b/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx index ec3e4cea7d..5ae15df0e5 100644 --- a/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx @@ -29,6 +29,7 @@ export default function RecoveryAttemptsSection({ address }) { loading={attemptsLoading} friendGroups={friendGroupsFormatted} onSlash={fetch} + onCancel={fetch} />
From 7791a590d6826b709f61009000f3b5d574e44fb5 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 16:45:30 +0800 Subject: [PATCH 42/50] refactor, #6919 --- .../useOngoingRecoveryAttemptsPrompt.jsx | 4 ++-- .../components/useRecoveryInheritorPrompt.jsx | 16 ++++++++++++---- .../components/recovery/common/columns.jsx | 2 +- .../helpOthers/inFriendGroupsSection.jsx | 3 ++- .../inheritors/controlInheritedAccountDialog.jsx | 7 +++++-- .../inheritors/inheritorFriendGroupsSection.jsx | 3 ++- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/next-common/components/overview/accountInfo/components/useOngoingRecoveryAttemptsPrompt.jsx b/packages/next-common/components/overview/accountInfo/components/useOngoingRecoveryAttemptsPrompt.jsx index 06477b823c..9ee7687233 100644 --- a/packages/next-common/components/overview/accountInfo/components/useOngoingRecoveryAttemptsPrompt.jsx +++ b/packages/next-common/components/overview/accountInfo/components/useOngoingRecoveryAttemptsPrompt.jsx @@ -28,9 +28,9 @@ function useOngoingRecoveryAttemptsPrompt() { key: CACHE_KEY.ongoingRecoveryAttemptsPrompt, message: (
- You have ongoing recovery attempts. See details{" "} + You have ongoing recovery attempts.{" "} - here + See details
), diff --git a/packages/next-common/components/overview/accountInfo/components/useRecoveryInheritorPrompt.jsx b/packages/next-common/components/overview/accountInfo/components/useRecoveryInheritorPrompt.jsx index 74c5f84607..02d04a710e 100644 --- a/packages/next-common/components/overview/accountInfo/components/useRecoveryInheritorPrompt.jsx +++ b/packages/next-common/components/overview/accountInfo/components/useRecoveryInheritorPrompt.jsx @@ -5,6 +5,7 @@ import { import { CACHE_KEY } from "next-common/utils/constants"; import { useCookieValue } from "next-common/utils/hooks/useCookieValue"; import Link from "next-common/components/link"; +import AddressUser from "next-common/components/user/addressUser"; import { useMemo } from "react"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; import useMyInheritor from "next-common/components/recovery/myRecovery/hooks/useMyInheritor"; @@ -18,6 +19,7 @@ function useRecoveryInheritorPrompt() { true, ); const hasInheritor = !loading && data?.length > 0; + const inheritorAddress = data?.[0]?.inheritor; return useMemo(() => { if (!hasInheritor || !visible) { @@ -27,17 +29,23 @@ function useRecoveryInheritorPrompt() { return { key: CACHE_KEY.recoveryInheritorPrompt, message: ( -
- You have inheritor settings. See details{" "} +
+ Your account has been inherited by + + .{" "} - here + See details
), type: PromptTypes.WARNING, close: () => setVisible(false, { expires: 15 }), }; - }, [hasInheritor, visible, setVisible]); + }, [hasInheritor, visible, setVisible, inheritorAddress]); } export default function RecoveryInheritorPrompt({ onClose }) { diff --git a/packages/next-common/components/recovery/common/columns.jsx b/packages/next-common/components/recovery/common/columns.jsx index 645300d306..3832677827 100644 --- a/packages/next-common/components/recovery/common/columns.jsx +++ b/packages/next-common/components/recovery/common/columns.jsx @@ -127,7 +127,7 @@ export const attemptColumns = { export const friendGroupColumns = { account: (className) => ({ - name: "Account", + name: "Lost Account", className, render: (item) => ( diff --git a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx index 6a5aec7174..671d19c14d 100644 --- a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx +++ b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx @@ -53,7 +53,8 @@ export default function InFriendGroupsSection({
- In Friend Groups + Friend Groups + I'm a friend
diff --git a/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx b/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx index 7472ee02fe..553dfc15a6 100644 --- a/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx +++ b/packages/next-common/components/recovery/inheritors/controlInheritedAccountDialog.jsx @@ -46,7 +46,10 @@ function ControlInheritedAccountInnerPopup({ recovered, onInBlock }) { extrinsicComponent = (
- +
- +
{extrinsicComponent} diff --git a/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx index f5cc45fda5..9c3344a0a4 100644 --- a/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx +++ b/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx @@ -49,7 +49,8 @@ export default function InheritorFriendGroupsSection({ address }) {
- In Friend Groups + Friend Groups + I'm the inheritor
From d58f9b3bf1172b8835159c978d507bbc7d90e22e Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 16:49:55 +0800 Subject: [PATCH 43/50] Update, #6919 --- .../next-common/components/recovery/common/columns.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/next-common/components/recovery/common/columns.jsx b/packages/next-common/components/recovery/common/columns.jsx index 3832677827..99d7e82764 100644 --- a/packages/next-common/components/recovery/common/columns.jsx +++ b/packages/next-common/components/recovery/common/columns.jsx @@ -95,7 +95,7 @@ export const attemptColumns = { }), thresholdApprovals: (className) => ({ - name: "Threshold / Approvals", + name: "Approvals / Threshold", className, render: (item) => ( + {item.approvalsCount} {item.fgGroup && ( - {item.fgGroup?.friendsNeeded || 0} /{" "} + {" "} + / {item.fgGroup?.friendsNeeded || 0} )} - {item.approvalsCount} ), From 01fbf052246eb222cae46db6be207859dcb9838d Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 16:59:39 +0800 Subject: [PATCH 44/50] fix, #6919 --- packages/next-common/components/recovery/myRecovery/index.jsx | 4 ++-- packages/next-common/utils/consts/menu/common.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/index.jsx b/packages/next-common/components/recovery/myRecovery/index.jsx index 2d17b2911a..e0f1e7f96a 100644 --- a/packages/next-common/components/recovery/myRecovery/index.jsx +++ b/packages/next-common/components/recovery/myRecovery/index.jsx @@ -14,9 +14,9 @@ export default function MyRecoveryContent() {
- - + +
); diff --git a/packages/next-common/utils/consts/menu/common.js b/packages/next-common/utils/consts/menu/common.js index 592f0fe799..ae79a40cf2 100644 --- a/packages/next-common/utils/consts/menu/common.js +++ b/packages/next-common/utils/consts/menu/common.js @@ -28,7 +28,7 @@ export const accountMenu = { "/account/delegations", "/account/deposits", "/account/help-recover", - "/account/inheritants", + "/account/inherited", "/account/multisigs", "/account/my-recovery", "/account/proxies", From 2bc8cad894687641185ec240e0f9624c78bab548 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 18:21:23 +0800 Subject: [PATCH 45/50] refactor, #6919 --- .../components/recovery/helpOthers/context.js | 55 ++++++++++++++ .../components/recovery/helpOthers/index.jsx | 75 ++++++++++++------- .../components/recovery/inheritors/context.js | 62 +++++++++++++++ .../components/recovery/inheritors/index.jsx | 32 ++++++-- .../inheritors/inheritedAccountsSection.jsx | 14 ++-- .../inheritorFriendGroupsSection.jsx | 15 ++-- .../components/recovery/myRecovery/context.js | 75 +++++++++++++++++++ .../myRecovery/friendGroupsSection.jsx | 19 ++--- .../components/recovery/myRecovery/index.jsx | 34 +++++++-- .../recovery/myRecovery/inheritorSection.jsx | 16 ++-- .../myRecovery/recoveryAttemptsSection.jsx | 17 ++--- 11 files changed, 336 insertions(+), 78 deletions(-) create mode 100644 packages/next-common/components/recovery/helpOthers/context.js create mode 100644 packages/next-common/components/recovery/inheritors/context.js create mode 100644 packages/next-common/components/recovery/myRecovery/context.js diff --git a/packages/next-common/components/recovery/helpOthers/context.js b/packages/next-common/components/recovery/helpOthers/context.js new file mode 100644 index 0000000000..1ead26d931 --- /dev/null +++ b/packages/next-common/components/recovery/helpOthers/context.js @@ -0,0 +1,55 @@ +"use client"; + +import { createContext, useContext, useMemo } from "react"; +import useHelpOthersFriendGroups from "./hooks/useHelpOthersFriendGroups"; +import useHelpOthersAttempts from "./hooks/useHelpOthersAttempts"; + +const HelpOthersDataContext = createContext(null); + +export function HelpOthersDataProvider({ address, children }) { + const friendGroups = useHelpOthersFriendGroups(address); + const attempts = useHelpOthersAttempts(address); + + const loading = friendGroups.loading || attempts.loading; + + const value = useMemo( + () => ({ + address, + friendGroupsData: friendGroups.data, + friendGroupsLoading: friendGroups.loading, + fetchFriendGroups: friendGroups.fetch, + attemptsData: attempts.data, + attemptsLoading: attempts.loading, + attemptsFriendGroupsData: attempts.friendGroupsData, + fetchAttempts: attempts.fetch, + loading, + }), + [ + address, + friendGroups.data, + friendGroups.loading, + friendGroups.fetch, + attempts.data, + attempts.loading, + attempts.friendGroupsData, + attempts.fetch, + loading, + ], + ); + + return ( + + {children} + + ); +} + +export function useHelpOthersData() { + const context = useContext(HelpOthersDataContext); + if (!context) { + throw new Error( + "useHelpOthersData must be used within a HelpOthersDataProvider", + ); + } + return context; +} diff --git a/packages/next-common/components/recovery/helpOthers/index.jsx b/packages/next-common/components/recovery/helpOthers/index.jsx index 9f09b550c7..d12d3e7e76 100644 --- a/packages/next-common/components/recovery/helpOthers/index.jsx +++ b/packages/next-common/components/recovery/helpOthers/index.jsx @@ -5,41 +5,66 @@ import useRealAddress from "next-common/utils/hooks/useRealAddress"; import RecoverySubTabs from "next-common/components/recovery/subTabs"; import InFriendGroupsSection from "./inFriendGroupsSection"; import HelpOthersAttemptsSection from "./helpOthersAttemptsSection"; -import useHelpOthersFriendGroups from "./hooks/useHelpOthersFriendGroups"; -import useHelpOthersAttempts from "./hooks/useHelpOthersAttempts"; import { RelayChainApiProvider } from "next-common/context/relayChain"; +import { HelpOthersDataProvider, useHelpOthersData } from "./context"; +import Loading from "next-common/components/loading"; -export default function HelpOthersContent() { - const address = useRealAddress(); - const friendGroups = useHelpOthersFriendGroups(address); - const attempts = useHelpOthersAttempts(address); - - const fetchFriendGroups = friendGroups.fetch; - const fetchAttempts = attempts.fetch; +function HelpOthersSections() { + const { + address, + friendGroupsData, + friendGroupsLoading, + fetchFriendGroups, + attemptsData, + attemptsLoading, + attemptsFriendGroupsData, + fetchAttempts, + loading, + } = useHelpOthersData(); const refreshAll = useCallback(() => { fetchFriendGroups(); fetchAttempts(); }, [fetchFriendGroups, fetchAttempts]); + if (loading) { + return ( +
+ +
+ ); + } + + return ( + <> + + + + ); +} + +export default function HelpOthersContent() { + const address = useRealAddress(); + return ( -
- - - -
+ +
+ + +
+
); } diff --git a/packages/next-common/components/recovery/inheritors/context.js b/packages/next-common/components/recovery/inheritors/context.js new file mode 100644 index 0000000000..e79e77f4d0 --- /dev/null +++ b/packages/next-common/components/recovery/inheritors/context.js @@ -0,0 +1,62 @@ +"use client"; + +import { createContext, useContext, useMemo } from "react"; +import useInheritedAccounts from "./hooks/useInheritedAccounts"; +import useInheritorFriendGroups from "./hooks/useInheritorFriendGroups"; + +const InheritorsDataContext = createContext(null); + +export function InheritorsDataProvider({ address, children }) { + const { + data: inheritedAccounts, + loading: inheritedAccountsLoading, + fetch: fetchInheritedAccounts, + } = useInheritedAccounts(address); + + const { + data: inheritorFriendGroups, + loading: inheritorFriendGroupsLoading, + fetch: fetchInheritorFriendGroups, + } = useInheritorFriendGroups(address); + + const loading = inheritedAccountsLoading || inheritorFriendGroupsLoading; + + const value = useMemo( + () => ({ + address, + inheritedAccounts, + inheritedAccountsLoading, + fetchInheritedAccounts, + inheritorFriendGroups, + inheritorFriendGroupsLoading, + fetchInheritorFriendGroups, + loading, + }), + [ + address, + inheritedAccounts, + inheritedAccountsLoading, + fetchInheritedAccounts, + inheritorFriendGroups, + inheritorFriendGroupsLoading, + fetchInheritorFriendGroups, + loading, + ], + ); + + return ( + + {children} + + ); +} + +export function useInheritorsData() { + const context = useContext(InheritorsDataContext); + if (!context) { + throw new Error( + "useInheritorsData must be used within a InheritorsDataProvider", + ); + } + return context; +} diff --git a/packages/next-common/components/recovery/inheritors/index.jsx b/packages/next-common/components/recovery/inheritors/index.jsx index ad749472f9..b9fb57a0a6 100644 --- a/packages/next-common/components/recovery/inheritors/index.jsx +++ b/packages/next-common/components/recovery/inheritors/index.jsx @@ -3,19 +3,41 @@ import useRealAddress from "next-common/utils/hooks/useRealAddress"; import RecoverySubTabs from "next-common/components/recovery/subTabs"; import { RelayChainApiProvider } from "next-common/context/relayChain"; +import { InheritorsDataProvider, useInheritorsData } from "./context"; import InheritedAccountsSection from "./inheritedAccountsSection"; import InheritorFriendGroupsSection from "./inheritorFriendGroupsSection"; +import Loading from "next-common/components/loading"; + +function InheritorsSections() { + const { loading } = useInheritorsData(); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( + <> + + + + ); +} export default function InheritorsContent() { const address = useRealAddress(); return ( -
- - - -
+ +
+ + +
+
); } diff --git a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx index 4344e452a9..e72c4d74e6 100644 --- a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx +++ b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx @@ -9,11 +9,11 @@ import { defaultPageSize } from "next-common/utils/constants"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; -import useInheritedAccounts from "./hooks/useInheritedAccounts"; +import { useInheritorsData } from "./context"; import useInheritedAccountsColumns from "./hooks/useInheritedAccountsColumns"; -export default function InheritedAccountsSection({ address }) { - const { data, loading: isLoading } = useInheritedAccounts(address); +export default function InheritedAccountsSection() { + const { inheritedAccounts, inheritedAccountsLoading } = useInheritorsData(); const { desktopColumns, mobileColumns } = useInheritedAccountsColumns(); const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); @@ -25,23 +25,23 @@ export default function InheritedAccountsSection({ address }) { defaultPageSize, ); - const total = data?.length || 0; + const total = inheritedAccounts?.length || 0; useEffect(() => { setLoading(true); }, [page]); useEffect(() => { - if (isLoading || isNil(data)) { + if (inheritedAccountsLoading || isNil(inheritedAccounts)) { return; } setTotalCount(total); const startIndex = (page - 1) * defaultPageSize; const endIndex = startIndex + defaultPageSize; - setDataList(data?.slice(startIndex, endIndex)); + setDataList(inheritedAccounts?.slice(startIndex, endIndex)); setLoading(false); - }, [data, isLoading, page, total]); + }, [inheritedAccounts, inheritedAccountsLoading, page, total]); return (
diff --git a/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx index 9c3344a0a4..2ecc3d4ad2 100644 --- a/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx +++ b/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx @@ -13,10 +13,11 @@ import { defaultPageSize } from "next-common/utils/constants"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; -import useInheritorFriendGroups from "./hooks/useInheritorFriendGroups"; +import { useInheritorsData } from "./context"; -export default function InheritorFriendGroupsSection({ address }) { - const { data, loading: isLoading } = useInheritorFriendGroups(address); +export default function InheritorFriendGroupsSection() { + const { inheritorFriendGroups, inheritorFriendGroupsLoading } = + useInheritorsData(); const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); const [totalCount, setTotalCount] = useState(0); @@ -27,23 +28,23 @@ export default function InheritorFriendGroupsSection({ address }) { defaultPageSize, ); - const total = data?.length || 0; + const total = inheritorFriendGroups?.length || 0; useEffect(() => { setLoading(true); }, [page]); useEffect(() => { - if (isLoading || isNil(data)) { + if (inheritorFriendGroupsLoading || isNil(inheritorFriendGroups)) { return; } setTotalCount(total); const startIndex = (page - 1) * defaultPageSize; const endIndex = startIndex + defaultPageSize; - setDataList(data?.slice(startIndex, endIndex)); + setDataList(inheritorFriendGroups?.slice(startIndex, endIndex)); setLoading(false); - }, [data, isLoading, page, total]); + }, [inheritorFriendGroups, inheritorFriendGroupsLoading, page, total]); return (
diff --git a/packages/next-common/components/recovery/myRecovery/context.js b/packages/next-common/components/recovery/myRecovery/context.js new file mode 100644 index 0000000000..255a4d9513 --- /dev/null +++ b/packages/next-common/components/recovery/myRecovery/context.js @@ -0,0 +1,75 @@ +"use client"; + +import { createContext, useContext, useMemo } from "react"; +import useMyFriendGroups from "./hooks/useMyFriendGroups"; +import useMyInheritor from "./hooks/useMyInheritor"; +import useMyRecoveryAttempts from "./hooks/useMyRecoveryAttempts"; + +const RecoveryDataContext = createContext(null); + +export function RecoveryDataProvider({ address, children }) { + const { + data: friendGroups, + loading: friendGroupsLoading, + fetch: fetchFriendGroups, + } = useMyFriendGroups(address); + + const { + data: inheritor, + loading: inheritorLoading, + fetch: fetchInheritor, + } = useMyInheritor(address); + + const { + data: attempts, + loading: attemptsLoading, + fetch: fetchAttempts, + } = useMyRecoveryAttempts(address); + + const loading = friendGroupsLoading || inheritorLoading || attemptsLoading; + + const value = useMemo( + () => ({ + friendGroups, + friendGroupsLoading, + fetchFriendGroups, + inheritor, + inheritorLoading, + fetchInheritor, + attempts, + attemptsLoading, + fetchAttempts, + loading, + address, + }), + [ + friendGroups, + friendGroupsLoading, + fetchFriendGroups, + inheritor, + inheritorLoading, + fetchInheritor, + attempts, + attemptsLoading, + fetchAttempts, + loading, + address, + ], + ); + + return ( + + {children} + + ); +} + +export function useRecoveryData() { + const context = useContext(RecoveryDataContext); + if (!context) { + throw new Error( + "useRecoveryData must be used within a RecoveryDataProvider", + ); + } + return context; +} diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index 37b780146e..f9f06f0a93 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -6,7 +6,7 @@ import Tooltip from "next-common/components/tooltip"; import Loading from "next-common/components/loading"; import AddressUser from "next-common/components/user/addressUser"; import DelayBlock from "next-common/components/recovery/delayBlock"; -import useMyFriendGroups from "./hooks/useMyFriendGroups"; +import { useRecoveryData } from "./context"; import AddFriendGroupDialog from "./addFriendGroupDialog"; import EditFriendGroupDialog from "./editFriendGroupDialog"; import RemoveFriendGroupDialog from "./removeFriendGroupDialog"; @@ -124,20 +124,21 @@ function FriendGroupCard({ group, onEdit, onRemove }) { ); } -export default function FriendGroupsSection({ address }) { +export default function FriendGroupsSection() { + const { friendGroups, friendGroupsLoading, fetchFriendGroups, address } = + useRecoveryData(); const [showAddDialog, setShowAddDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); const [editingGroup, setEditingGroup] = useState(null); const [showRemoveDialog, setShowRemoveDialog] = useState(false); const [removingIndex, setRemovingIndex] = useState(null); - const { data, loading, fetch } = useMyFriendGroups(address); return (
{showAddDialog && ( setShowAddDialog(false)} - onInBlock={fetch} + onInBlock={fetchFriendGroups} /> )} {showEditDialog && editingGroup && ( @@ -147,7 +148,7 @@ export default function FriendGroupsSection({ address }) { setEditingGroup(null); }} group={editingGroup} - onInBlock={fetch} + onInBlock={fetchFriendGroups} /> )} {showRemoveDialog && ( @@ -158,7 +159,7 @@ export default function FriendGroupsSection({ address }) { }} index={removingIndex} address={address} - onInBlock={fetch} + onInBlock={fetchFriendGroups} /> )}
@@ -175,11 +176,11 @@ export default function FriendGroupsSection({ address }) {
- {loading ? ( + {friendGroupsLoading ? (
- ) : data.length === 0 ? ( + ) : friendGroups.length === 0 ? ( No friend groups found @@ -187,7 +188,7 @@ export default function FriendGroupsSection({ address }) { ) : (
- {data.map((group) => ( + {friendGroups.map((group) => ( + +
+ ); + } + + return ( + <> + + + + + ); +} export default function MyRecoveryContent() { const address = useRealAddress(); return ( -
- - - - -
+ +
+ + +
+
); } diff --git a/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx b/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx index d71c9a1fc8..ed4023813f 100644 --- a/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/inheritorSection.jsx @@ -9,13 +9,13 @@ import { defaultPageSize } from "next-common/utils/constants"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; -import useMyInheritor from "./hooks/useMyInheritor"; +import { useRecoveryData } from "./context"; import useMyRecoveryInheritorColumns from "./hooks/useMyRecoveryInheritorColumns"; -export default function InheritorSection({ address }) { - const { data, loading: isLoading, fetch } = useMyInheritor(address); +export default function InheritorSection() { + const { inheritor, inheritorLoading, fetchInheritor } = useRecoveryData(); const { desktopColumns, mobileColumns } = - useMyRecoveryInheritorColumns(fetch); + useMyRecoveryInheritorColumns(fetchInheritor); const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); const [totalCount, setTotalCount] = useState(0); @@ -26,23 +26,23 @@ export default function InheritorSection({ address }) { defaultPageSize, ); - const total = data?.length || 0; + const total = inheritor?.length || 0; useEffect(() => { setLoading(true); }, [page]); useEffect(() => { - if (isLoading || isNil(data)) { + if (inheritorLoading || isNil(inheritor)) { return; } setTotalCount(total); const startIndex = (page - 1) * defaultPageSize; const endIndex = startIndex + defaultPageSize; - setDataList(data?.slice(startIndex, endIndex)); + setDataList(inheritor?.slice(startIndex, endIndex)); setLoading(false); - }, [data, isLoading, page, total]); + }, [inheritor, inheritorLoading, page, total]); return (
diff --git a/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx b/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx index 5ae15df0e5..43748b9555 100644 --- a/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/recoveryAttemptsSection.jsx @@ -1,16 +1,11 @@ "use client"; -import useMyRecoveryAttempts from "./hooks/useMyRecoveryAttempts"; -import useMyFriendGroups from "./hooks/useMyFriendGroups"; import MyRecoveryAttemptsTable from "./attemptsTable"; +import { useRecoveryData } from "./context"; -export default function RecoveryAttemptsSection({ address }) { - const { - data: attempts, - loading: attemptsLoading, - fetch, - } = useMyRecoveryAttempts(address); - const { data: friendGroups } = useMyFriendGroups(address); +export default function RecoveryAttemptsSection() { + const { attempts, attemptsLoading, fetchAttempts, friendGroups, address } = + useRecoveryData(); const friendGroupsFormatted = address ? [{ account: address, friendGroups: friendGroups }] @@ -28,8 +23,8 @@ export default function RecoveryAttemptsSection({ address }) { data={attempts} loading={attemptsLoading} friendGroups={friendGroupsFormatted} - onSlash={fetch} - onCancel={fetch} + onSlash={fetchAttempts} + onCancel={fetchAttempts} />
From 44bf35fbbcb7c8337ab7840666be2f8df0959538 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 18:32:51 +0800 Subject: [PATCH 46/50] Sort my recovery tab sections, #6919 --- .../components/recovery/myRecovery/index.jsx | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/index.jsx b/packages/next-common/components/recovery/myRecovery/index.jsx index 9ed7e90559..40bd57ff49 100644 --- a/packages/next-common/components/recovery/myRecovery/index.jsx +++ b/packages/next-common/components/recovery/myRecovery/index.jsx @@ -10,7 +10,7 @@ import { RecoveryDataProvider, useRecoveryData } from "./context"; import Loading from "next-common/components/loading"; function MyRecoverySections() { - const { loading } = useRecoveryData(); + const { loading, attempts, inheritor, friendGroups } = useRecoveryData(); if (loading) { return ( @@ -20,11 +20,51 @@ function MyRecoverySections() { ); } + // 1) Sections with data always come before sections without data + // 2) With data priority: attempts(1), inheritor(2), friend groups(3) + // 3) Without data priority: friend groups(1), attempts(2), inheritor(3) + const hasData = (arr) => arr && arr.length > 0; + + const sectionConfigs = [ + { + key: "recoveryAttempts", + hasData: hasData(attempts), + Component: RecoveryAttemptsSection, + priorityWithData: 1, + priorityWithoutData: 2, + }, + { + key: "inheritors", + hasData: hasData(inheritor), + Component: InheritorSection, + priorityWithData: 2, + priorityWithoutData: 3, + }, + { + key: "friendGroups", + hasData: hasData(friendGroups), + Component: FriendGroupsSection, + priorityWithData: 3, + priorityWithoutData: 1, + }, + ]; + + const sorted = [...sectionConfigs].sort((a, b) => { + // Data-bearing sections first + if (a.hasData !== b.hasData) { + return a.hasData ? -1 : 1; + } + // Same group: sort by the corresponding priority + const pa = a.hasData ? a.priorityWithData : a.priorityWithoutData; + const pb = b.hasData ? b.priorityWithData : b.priorityWithoutData; + return pa - pb; + }); + return ( <> - - - + {sorted.map(({ key, Component }) => ( + + ))} ); } From fc0a18c9734de6c9c4917b9136828bcb79c82a42 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 1 Jul 2026 19:24:18 +0800 Subject: [PATCH 47/50] Sort sections, #6919 --- .../components/recovery/helpOthers/index.jsx | 51 ++++++++++++++----- .../components/recovery/inheritors/index.jsx | 30 +++++++++-- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/packages/next-common/components/recovery/helpOthers/index.jsx b/packages/next-common/components/recovery/helpOthers/index.jsx index d12d3e7e76..74b66bc129 100644 --- a/packages/next-common/components/recovery/helpOthers/index.jsx +++ b/packages/next-common/components/recovery/helpOthers/index.jsx @@ -35,21 +35,46 @@ function HelpOthersSections() { ); } + const hasData = (arr) => arr && arr.length > 0; + + const sectionConfigs = [ + { + key: "attempts", + hasData: hasData(attemptsData), + Component: HelpOthersAttemptsSection, + props: { + address, + data: attemptsData, + loading: attemptsLoading, + friendGroupsData: attemptsFriendGroupsData, + onRefresh: refreshAll, + }, + }, + { + key: "friendGroups", + hasData: hasData(friendGroupsData), + Component: InFriendGroupsSection, + props: { + data: friendGroupsData, + loading: friendGroupsLoading, + attemptsData, + onRefresh: refreshAll, + }, + }, + ]; + + const sorted = [...sectionConfigs].sort((a, b) => { + if (a.hasData !== b.hasData) { + return a.hasData ? -1 : 1; + } + return 0; // preserve original order within same group + }); + return ( <> - - + {sorted.map(({ key, Component, props }) => ( + + ))} ); } diff --git a/packages/next-common/components/recovery/inheritors/index.jsx b/packages/next-common/components/recovery/inheritors/index.jsx index b9fb57a0a6..938bf0c7b6 100644 --- a/packages/next-common/components/recovery/inheritors/index.jsx +++ b/packages/next-common/components/recovery/inheritors/index.jsx @@ -9,7 +9,8 @@ import InheritorFriendGroupsSection from "./inheritorFriendGroupsSection"; import Loading from "next-common/components/loading"; function InheritorsSections() { - const { loading } = useInheritorsData(); + const { loading, inheritedAccounts, inheritorFriendGroups } = + useInheritorsData(); if (loading) { return ( @@ -19,10 +20,33 @@ function InheritorsSections() { ); } + const hasData = (arr) => arr && arr.length > 0; + + const sectionConfigs = [ + { + key: "inheritedAccounts", + hasData: hasData(inheritedAccounts), + Component: InheritedAccountsSection, + }, + { + key: "inheritorFriendGroups", + hasData: hasData(inheritorFriendGroups), + Component: InheritorFriendGroupsSection, + }, + ]; + + const sorted = [...sectionConfigs].sort((a, b) => { + if (a.hasData !== b.hasData) { + return a.hasData ? -1 : 1; + } + return 0; + }); + return ( <> - - + {sorted.map(({ key, Component }) => ( + + ))} ); } From 7f47a5584ab1f60bcd4020e40ab0d650dd84f058 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 2 Jul 2026 10:43:01 +0800 Subject: [PATCH 48/50] Improve block time tooltip, #6919 --- .../myRecovery/blockNumberWithTooltip.jsx | 35 +++++++++------- .../hooks/common/useBlockTimestamp.js | 40 ++++++++++++------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/packages/next-common/components/recovery/myRecovery/blockNumberWithTooltip.jsx b/packages/next-common/components/recovery/myRecovery/blockNumberWithTooltip.jsx index 94bbf7a6f0..6627fceca2 100644 --- a/packages/next-common/components/recovery/myRecovery/blockNumberWithTooltip.jsx +++ b/packages/next-common/components/recovery/myRecovery/blockNumberWithTooltip.jsx @@ -2,30 +2,35 @@ import Tooltip from "next-common/components/tooltip"; import { useRelayChainApi } from "next-common/context/relayChain"; -import { useChainSettings } from "next-common/context/chain"; -import { estimateBlocksTime } from "next-common/utils"; -import useCall from "next-common/utils/hooks/useCall"; +import useBlockTimestamp from "next-common/hooks/common/useBlockTimestamp"; +import FieldLoading from "next-common/components/icons/fieldLoading"; +import { formatTimeAgo } from "next-common/utils/viewfuncs/formatTimeAgo"; +import formatTime from "next-common/utils/viewfuncs/formatDate"; +import useNow from "next-common/hooks/useNow"; import { isNil } from "lodash-es"; export default function BlockNumberWithTooltip({ height }) { const api = useRelayChainApi(); - const { blockTime } = useChainSettings(); - const { value: currentNumber } = useCall(api?.query?.system?.number, []); - const currentHeight = currentNumber?.toNumber(); + const { timestamp, isLoading } = useBlockTimestamp(height, api); + const now = useNow(30 * 1000); - if (isNil(height) || isNil(currentHeight)) { - return ( - - #{height?.toLocaleString() || 0} - - ); + if (isNil(height)) { + return #0; } - const diff = Math.max(0, currentHeight - height); - const estimatedTime = diff > 0 ? estimateBlocksTime(diff, blockTime) : null; + const tooltipContent = isLoading ? ( + + ) : timestamp ? ( +
+
{formatTime(timestamp)}
+
+ {formatTimeAgo(timestamp, { referenceTime: now })} +
+
+ ) : null; return ( - + #{height?.toLocaleString() || 0} diff --git a/packages/next-common/hooks/common/useBlockTimestamp.js b/packages/next-common/hooks/common/useBlockTimestamp.js index e78828ab2e..41c683c45f 100644 --- a/packages/next-common/hooks/common/useBlockTimestamp.js +++ b/packages/next-common/hooks/common/useBlockTimestamp.js @@ -15,23 +15,33 @@ export default function useBlockTimestamp(height, specifiedApi = null) { const [isEstimated, setIsEstimated] = useState(false); const [isLoading, setIsLoading] = useState(false); + const isBlockLoaded = chainHeight >= height; + + useEffect(() => { + if (isBlockLoaded && api) { + return; + } + + const now = new Date().getTime(); + setTimestamp( + BigNumber(oneBlockTime) + .multipliedBy(height - chainHeight) + .plus(now) + .toNumber(), + ); + setIsEstimated(true); + }, [api, height, chainHeight, isBlockLoaded, oneBlockTime]); + useEffect(() => { - if (chainHeight >= height && api) { - setIsLoading(true); - getBlockTimeByHeight(api, height) - .then((v) => setTimestamp(v)) - .finally(() => setIsLoading(false)); - } else { - const now = new Date().getTime(); - setTimestamp( - BigNumber(oneBlockTime) - .multipliedBy(height - chainHeight) - .plus(now) - .toNumber(), - ); - setIsEstimated(true); + if (!isBlockLoaded || !api) { + return; } - }, [api, height, chainHeight, oneBlockTime]); + + setIsLoading(true); + getBlockTimeByHeight(api, height) + .then((v) => setTimestamp(v)) + .finally(() => setIsLoading(false)); + }, [api, height, isBlockLoaded]); return { timestamp, From aca7b1efd39ec784e3772bd7892fa3d3c26823d4 Mon Sep 17 00:00:00 2001 From: chaojun Date: Thu, 2 Jul 2026 17:41:41 +0800 Subject: [PATCH 49/50] Improve, #6919 --- .../helpOthers/inFriendGroupsSection.jsx | 4 +++- .../hooks/useInheritedAccountsColumns.jsx | 17 ++++++++++------- .../inheritors/inheritedAccountsSection.jsx | 3 +++ .../inheritors/inheritorFriendGroupsSection.jsx | 4 +++- .../recovery/myRecovery/friendGroupsSection.jsx | 2 +- .../recovery/myRecovery/inheritorSection.jsx | 2 +- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx index 671d19c14d..2d4288735f 100644 --- a/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx +++ b/packages/next-common/components/recovery/helpOthers/inFriendGroupsSection.jsx @@ -54,7 +54,9 @@ export default function InFriendGroupsSection({
Friend Groups - I'm a friend + + I'm a friend +
diff --git a/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx index dd8cf504c4..abf10b4be5 100644 --- a/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx +++ b/packages/next-common/components/recovery/inheritors/hooks/useInheritedAccountsColumns.jsx @@ -1,6 +1,7 @@ "use client"; import { useMemo, useState } from "react"; +import Tooltip from "next-common/components/tooltip"; import { inheritorColumns } from "next-common/components/recovery/common/columns"; import ControlInheritedAccountDialog from "../controlInheritedAccountDialog"; @@ -15,13 +16,15 @@ function ControlButton({ item }) { onClose={() => setShowDialog(false)} /> )} - + + + ); } diff --git a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx index e72c4d74e6..2f92fc5799 100644 --- a/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx +++ b/packages/next-common/components/recovery/inheritors/inheritedAccountsSection.jsx @@ -48,6 +48,9 @@ export default function InheritedAccountsSection() {
Inherited Accounts + + I'm the inheritor +
diff --git a/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx b/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx index 2ecc3d4ad2..20c325de40 100644 --- a/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx +++ b/packages/next-common/components/recovery/inheritors/inheritorFriendGroupsSection.jsx @@ -51,7 +51,9 @@ export default function InheritorFriendGroupsSection() {
Friend Groups - I'm the inheritor + + I'm the inheritor +
diff --git a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx index f9f06f0a93..b691eecd9e 100644 --- a/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx +++ b/packages/next-common/components/recovery/myRecovery/friendGroupsSection.jsx @@ -164,7 +164,7 @@ export default function FriendGroupsSection() { )}
- Friend Groups + My Friend Groups