import("next-common/components/paraChainTeleportPopup").then(
@@ -313,6 +314,7 @@ export default function AccountInfoPanel() {
+
);
diff --git a/packages/next-common/components/overview/accountInfo/components/ScrollPromptContainer.js b/packages/next-common/components/overview/accountInfo/components/ScrollPromptContainer.js
new file mode 100644
index 0000000000..2b5addaf27
--- /dev/null
+++ b/packages/next-common/components/overview/accountInfo/components/ScrollPromptContainer.js
@@ -0,0 +1,113 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import { animate } from "framer-motion";
+import { useWindowWidthContext } from "next-common/context/windowSize";
+import { Fragment } from "react";
+
+const ITEM_HEIGHT = 40;
+const MOBILE_ITEM_HEIGHT = 60;
+const ITEM_GAP = 4;
+
+export default function ScrollPromptContainer({ components = [] }) {
+ const wrapperRef = useRef();
+ const width = useWindowWidthContext();
+ const isMobile = width < 768;
+ const [total, setTotal] = useState(0);
+ const pauseRef = useRef(false);
+
+ const pageSize = total > 2 ? 2 : 1;
+
+ const wrapperHeight = useMemo(() => {
+ if (!total) {
+ return 0;
+ }
+ if (isMobile) {
+ return pageSize * MOBILE_ITEM_HEIGHT + ITEM_GAP * (pageSize - 1);
+ }
+ return pageSize * ITEM_HEIGHT + ITEM_GAP * (pageSize - 1);
+ }, [isMobile, pageSize, total]);
+
+ const [random, setRandom] = useState(1);
+ useEffect(() => {
+ if (!wrapperRef.current) {
+ return;
+ }
+
+ const updateTotal = () => {
+ setTotal(Math.floor(wrapperRef.current.children.length / 3));
+ };
+
+ updateTotal();
+
+ const observer = new MutationObserver(updateTotal);
+ observer.observe(wrapperRef.current, { childList: true });
+
+ return () => observer.disconnect();
+ }, [random]);
+
+ const marginTop = useMemo(() => {
+ if (isMobile) {
+ return MOBILE_ITEM_HEIGHT + ITEM_GAP;
+ }
+ return ITEM_HEIGHT + ITEM_GAP;
+ }, [isMobile]);
+
+ const indexRef = useRef(0);
+
+ useEffect(() => {
+ indexRef.current = 0;
+ wrapperRef.current?.scrollTo({ top: 0 });
+
+ if (total < 2) {
+ return;
+ }
+
+ const interval = setInterval(() => {
+ if (pauseRef.current || !wrapperRef.current) {
+ return;
+ }
+
+ const nextIndex = indexRef.current + 1;
+ const from = indexRef.current * marginTop;
+ const to = nextIndex * marginTop;
+
+ animate(from, to, {
+ duration: 1,
+ onUpdate: (latest) => {
+ wrapperRef.current?.scrollTo({ top: latest });
+ },
+ onComplete: () => {
+ indexRef.current = nextIndex;
+ if (indexRef.current >= total) {
+ indexRef.current -= total;
+ wrapperRef.current?.scrollTo({ top: indexRef.current * marginTop });
+ }
+ },
+ });
+ }, 6500);
+ return () => clearInterval(interval);
+ }, [marginTop, total]);
+
+ return (
+ (pauseRef.current = true)}
+ onMouseLeave={() => (pauseRef.current = false)}
+ >
+ {[...components, ...components, ...components].map((Item, index) => {
+ return (
+
+ - setRandom(random + 1)}
+ />
+
+ );
+ })}
+
+ );
+}
diff --git a/packages/next-common/components/overview/accountInfo/components/accountPanelJudgementScrollPrompt.js b/packages/next-common/components/overview/accountInfo/components/accountPanelJudgementScrollPrompt.js
new file mode 100644
index 0000000000..a488efb4ac
--- /dev/null
+++ b/packages/next-common/components/overview/accountInfo/components/accountPanelJudgementScrollPrompt.js
@@ -0,0 +1,8 @@
+import ScrollPromptContainer from "./ScrollPromptContainer";
+import { JudgementPrompt } from "./judgementPrompt";
+
+const promptComponents = [JudgementPrompt];
+
+export default function AccountPanelJudgementScrollPrompt() {
+ return ;
+}
diff --git a/packages/next-common/components/overview/accountInfo/components/accountPanelScrollPrompt.js b/packages/next-common/components/overview/accountInfo/components/accountPanelScrollPrompt.js
index c3e81f2dc7..8410435296 100644
--- a/packages/next-common/components/overview/accountInfo/components/accountPanelScrollPrompt.js
+++ b/packages/next-common/components/overview/accountInfo/components/accountPanelScrollPrompt.js
@@ -3,10 +3,7 @@ import { AvatarPrompt } from "./useSetAvatarPrompt";
import { IdentityPrompt } from "./useSetIdentityPrompt";
import { MultisigPrompt } from "./useMultisigPrompt";
import AssetsManagePrompt from "./useAssetsManagePrompt";
-import { useEffect, useMemo, useRef, useState } from "react";
-import { animate } from "framer-motion";
-import { useWindowWidthContext } from "next-common/context/windowSize";
-import { Fragment } from "react";
+import ScrollPromptContainer from "./ScrollPromptContainer";
import AccountUnlockBalancePrompt from "./accountUnlockBalancePrompt";
import VestingUnlockablePrompt from "./vestingUnlockablePrompt";
import NominatorWithdrawUnbondedPrompt from "./nominatorWithdrawUnbondedPrompt";
@@ -14,10 +11,6 @@ import NominatorClaimRewardPrompt from "./nominatorClaimRewardPrompt";
import PoolWithdrawUnbondedPrompt from "./poolWithdrawUnbondedPrompt";
import PoolClaimRewardPrompt from "./poolClaimRewardPrompt";
-const ITEM_HEIGHT = 40;
-const MOBILE_ITEM_HEIGHT = 60;
-const ITEM_GAP = 4;
-
const promptComponents = [
DelegationPrompt,
AvatarPrompt,
@@ -33,110 +26,5 @@ const promptComponents = [
];
export default function AccountPanelScrollPrompt() {
- const wrapperRef = useRef();
- const width = useWindowWidthContext();
- const isMobile = width < 768;
- const [total, setTotal] = useState(0);
- const pauseRef = useRef(false);
-
- const pageSize = total > 2 ? 2 : 1;
-
- const wrapperHeight = useMemo(() => {
- if (!total) {
- return 0;
- }
- if (isMobile) {
- return pageSize * MOBILE_ITEM_HEIGHT + ITEM_GAP * (pageSize - 1);
- }
- return pageSize * ITEM_HEIGHT + ITEM_GAP * (pageSize - 1);
- }, [isMobile, pageSize, total]);
-
- const [random, setRandom] = useState(1);
- useEffect(() => {
- if (!wrapperRef.current) {
- return;
- }
-
- const updateTotal = () => {
- setTotal(Math.floor(wrapperRef.current.children.length / 3));
- };
-
- updateTotal();
-
- const observer = new MutationObserver(updateTotal);
- observer.observe(wrapperRef.current, { childList: true });
-
- return () => observer.disconnect();
- }, [random]);
-
- const marginTop = useMemo(() => {
- if (isMobile) {
- return MOBILE_ITEM_HEIGHT + ITEM_GAP;
- }
- return ITEM_HEIGHT + ITEM_GAP;
- }, [isMobile]);
-
- const indexRef = useRef(0);
-
- useEffect(() => {
- indexRef.current = 0;
- wrapperRef.current?.scrollTo({ top: 0 });
-
- if (total < 2) {
- return;
- }
-
- const interval = setInterval(() => {
- if (pauseRef.current || !wrapperRef.current) {
- return;
- }
-
- const nextIndex = indexRef.current + 1;
- const from = indexRef.current * marginTop;
- const to = nextIndex * marginTop;
-
- animate(from, to, {
- duration: 1,
- onUpdate: (latest) => {
- wrapperRef.current?.scrollTo({ top: latest });
- },
- onComplete: () => {
- indexRef.current = nextIndex;
- if (indexRef.current >= total) {
- indexRef.current -= total;
- wrapperRef.current?.scrollTo({ top: indexRef.current * marginTop });
- }
- },
- });
- }, 6500);
- return () => clearInterval(interval);
- }, [marginTop, total]);
-
- return (
- (pauseRef.current = true)}
- onMouseLeave={() => (pauseRef.current = false)}
- >
- {[...promptComponents, ...promptComponents, ...promptComponents].map(
- (Item, index) => {
- return (
-
- - {
- setRandom(random + 1);
- }}
- />
-
- );
- },
- )}
-
- );
+ return ;
}
diff --git a/packages/next-common/components/overview/accountInfo/components/judgementPrompt.js b/packages/next-common/components/overview/accountInfo/components/judgementPrompt.js
new file mode 100644
index 0000000000..e482961320
--- /dev/null
+++ b/packages/next-common/components/overview/accountInfo/components/judgementPrompt.js
@@ -0,0 +1,15 @@
+import RequestJudgementPromptItem from "./requestJudgementPromptItem";
+import NavigateToJudgementPagePromptItem from "./navigateToJudgementPagePromptItem";
+import { useChain } from "next-common/context/chain";
+import { isPeopleChain } from "next-common/utils/chain";
+
+export function JudgementPrompt({ onClose }) {
+ const chain = useChain();
+
+ return (
+ <>
+
+ {isPeopleChain(chain) && }
+ >
+ );
+}
diff --git a/packages/next-common/components/overview/accountInfo/components/navigateToJudgementPagePromptItem.js b/packages/next-common/components/overview/accountInfo/components/navigateToJudgementPagePromptItem.js
new file mode 100644
index 0000000000..668a9696ce
--- /dev/null
+++ b/packages/next-common/components/overview/accountInfo/components/navigateToJudgementPagePromptItem.js
@@ -0,0 +1,69 @@
+import Link from "next-common/components/link";
+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 { useMemo } from "react";
+import useMyJudgementRequest from "next-common/components/people/hooks/useMyJudgementRequest";
+
+function NavigateToJudgementPagePrompt() {
+ return (
+
+ Verify your social accounts for identity judgement.
+
+ Go to Verifications page
+
+ .
+
+ );
+}
+
+function useNavigateToJudgementPagePromptItem() {
+ const { value: myJudgementRequest } = useMyJudgementRequest();
+
+ const needAction =
+ myJudgementRequest &&
+ Object.values(myJudgementRequest.verifications).some(
+ (verified) => verified !== true,
+ );
+
+ const [visible, setVisible] = useCookieValue(
+ CACHE_KEY.navigateToJudgementPagePrompt,
+ true,
+ );
+
+ return useMemo(() => {
+ if (!visible || !needAction) {
+ return {};
+ }
+
+ return {
+ key: CACHE_KEY.navigateToJudgementPagePrompt,
+ message: ,
+ type: PromptTypes.WARNING,
+ close: () => setVisible(false, { expires: 15 }),
+ };
+ }, [needAction, setVisible, visible]);
+}
+
+export default function NavigateToJudgementPagePromptItem({ onClose }) {
+ const prompt = useNavigateToJudgementPagePromptItem();
+
+ if (!prompt?.message) {
+ return null;
+ }
+
+ return (
+ {
+ onClose?.();
+ prompt?.close?.();
+ },
+ }}
+ />
+ );
+}
diff --git a/packages/next-common/components/overview/accountInfo/components/requestJudgementPromptItem.js b/packages/next-common/components/overview/accountInfo/components/requestJudgementPromptItem.js
new file mode 100644
index 0000000000..0261ce7bb8
--- /dev/null
+++ b/packages/next-common/components/overview/accountInfo/components/requestJudgementPromptItem.js
@@ -0,0 +1,109 @@
+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 { useMemo, useState } from "react";
+import { useIdentityInfoContext } from "next-common/context/people/identityInfoContext";
+import RequestJudgementPopup from "next-common/components/requestJudgementPopup";
+
+function analyzeJudgements(judgements) {
+ return {
+ hasPending: judgements.some(({ status }) => ["FeePaid"].includes(status)),
+ hasPositive: judgements.some(({ status }) =>
+ ["KnownGood", "Reasonable"].includes(status),
+ ),
+ hasOutdated: judgements.some(({ status }) => status === "OutOfDate"),
+ hasNegative: judgements.some(({ status }) =>
+ ["LowQuality", "Erroneous"].includes(status),
+ ),
+ };
+}
+
+function useShouldPromptJudgementRequest() {
+ const { info, judgements, isLoading, displayName } = useIdentityInfoContext();
+
+ if (isLoading) {
+ return false;
+ }
+
+ if (!displayName && Object.values(info || {}).every((v) => !v)) {
+ return false;
+ }
+
+ if (!judgements || judgements.length === 0) {
+ return true;
+ }
+
+ const { hasPending, hasPositive, hasNegative } =
+ analyzeJudgements(judgements);
+
+ if (hasPending || hasPositive || hasNegative) {
+ return false;
+ }
+
+ return true;
+}
+
+function RequestJudgementPromptContent() {
+ const [showPopup, setShowPopup] = useState(false);
+
+ return (
+
+ Your on-chain identity is not verified yet.
+ setShowPopup(true)}
+ >
+ Request judgement
+
+ {showPopup && (
+ setShowPopup(false)} />
+ )}
+
+ );
+}
+
+function useRequestJudgementPromptItem() {
+ const shouldRequestJudgement = useShouldPromptJudgementRequest();
+
+ const [visible, setVisible] = useCookieValue(
+ CACHE_KEY.requestJudgementPrompt,
+ true,
+ );
+
+ return useMemo(() => {
+ if (!visible || !shouldRequestJudgement) {
+ return {};
+ }
+
+ return {
+ key: CACHE_KEY.requestJudgementPrompt,
+ message: ,
+ type: PromptTypes.WARNING,
+ close: () => setVisible(false, { expires: 15 }),
+ };
+ }, [setVisible, shouldRequestJudgement, visible]);
+}
+
+export default function RequestJudgementPromptItem({ onClose }) {
+ const prompt = useRequestJudgementPromptItem();
+
+ if (!prompt?.message) {
+ return null;
+ }
+
+ return (
+ {
+ onClose?.();
+ prompt?.close?.();
+ },
+ }}
+ />
+ );
+}
diff --git a/packages/next-common/components/overview/accountInfo/components/setIdentityPromptItem.js b/packages/next-common/components/overview/accountInfo/components/setIdentityPromptItem.js
new file mode 100644
index 0000000000..8b023be27e
--- /dev/null
+++ b/packages/next-common/components/overview/accountInfo/components/setIdentityPromptItem.js
@@ -0,0 +1,79 @@
+import Link from "next-common/components/link";
+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 { useMemo } from "react";
+import { useChainSettings } from "next-common/context/chain";
+import { useRouter } from "next/router";
+import useIdentityInfo from "next-common/hooks/useIdentityInfo";
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+
+const identityPage = "/people";
+
+function useSetIdentityPromptItem() {
+ const router = useRouter();
+ const chainSettings = useChainSettings();
+ const address = useRealAddress();
+ const { hasIdentity, identity } = useIdentityInfo(address);
+
+ const pathName = router.pathname;
+ const { modules } = chainSettings;
+ const supportedPeople = modules?.people;
+
+ const isPeoplePage = pathName?.startsWith(identityPage);
+
+ const isNotVerified = useMemo(() => {
+ return identity?.info?.status === "NOT_VERIFIED";
+ }, [identity?.info?.status]);
+
+ const shouldShow = !hasIdentity && !isPeoplePage;
+
+ const [visible, setVisible] = useCookieValue(
+ CACHE_KEY.setIdentityPromptVisible,
+ true,
+ );
+
+ return useMemo(() => {
+ if (!visible || !supportedPeople || !shouldShow || isNotVerified) {
+ return {};
+ }
+
+ return {
+ key: CACHE_KEY.setIdentityPromptVisible,
+ message: (
+
+ Set your on-chain identity
+
+ here
+
+ .
+
+ ),
+ type: PromptTypes.INFO,
+ close: () => setVisible(false, { expires: 15 }),
+ };
+ }, [isNotVerified, setVisible, shouldShow, supportedPeople, visible]);
+}
+
+export default function SetIdentityPromptItem({ onClose }) {
+ const prompt = useSetIdentityPromptItem();
+
+ if (!prompt?.message) {
+ return null;
+ }
+
+ return (
+ {
+ onClose?.();
+ prompt?.close?.();
+ },
+ }}
+ />
+ );
+}
diff --git a/packages/next-common/components/overview/accountInfo/components/useSetIdentityPrompt.js b/packages/next-common/components/overview/accountInfo/components/useSetIdentityPrompt.js
index 16ce2922b3..ff0838dba7 100644
--- a/packages/next-common/components/overview/accountInfo/components/useSetIdentityPrompt.js
+++ b/packages/next-common/components/overview/accountInfo/components/useSetIdentityPrompt.js
@@ -1,109 +1,9 @@
-import Link from "next-common/components/link";
-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 { useMemo } from "react";
-import { useChainSettings } from "next-common/context/chain";
-import { useRouter } from "next/router";
-import useIdentityInfo from "next-common/hooks/useIdentityInfo";
-import useRealAddress from "next-common/utils/hooks/useRealAddress";
-import { isEmpty } from "lodash-es";
+import SetIdentityPromptItem from "./setIdentityPromptItem";
-const identityPage = "/people";
-const judgementPage = "/people?tab=judgements";
-
-export default function useSetIdentityPrompt() {
- const router = useRouter();
- const chainSettings = useChainSettings();
- const address = useRealAddress();
- const { hasIdentity, identity } = useIdentityInfo(address);
- const pathName = router.pathname;
- const { modules } = chainSettings;
- const supportedPeople = modules?.people;
-
- const isPeoplePage = pathName?.startsWith(identityPage);
- const isJudgementPage = router.asPath?.startsWith(judgementPage);
-
- const isNotVerified = useMemo(() => {
- return identity?.info?.status === "NOT_VERIFIED";
- }, [identity?.info?.status]);
-
- const cacheKey = useMemo(
- () =>
- isNotVerified
- ? CACHE_KEY.requestJudgementPrompt
- : CACHE_KEY.setIdentityPromptVisible,
- [isNotVerified],
- );
-
- const [visible, setVisible] = useCookieValue(cacheKey, true);
-
- return useMemo(() => {
- if (!visible || !supportedPeople) {
- return {};
- }
-
- let message;
- if (hasIdentity && isNotVerified && !isJudgementPage) {
- message = (
-
- Your on-chain identity is not verified yet. Request judgements
-
- here
-
- .
-
- );
- } else if (!hasIdentity && !isPeoplePage) {
- message = (
-
- Set your on-chain identity
-
- here
-
- .
-
- );
- }
-
- if (message) {
- return {
- key: cacheKey,
- message,
- type: PromptTypes.INFO,
- close: () => setVisible(false, { expires: 15 }),
- };
- }
-
- return {};
- }, [
- cacheKey,
- hasIdentity,
- isJudgementPage,
- isNotVerified,
- isPeoplePage,
- setVisible,
- supportedPeople,
- visible,
- ]);
-}
export function IdentityPrompt({ onClose }) {
- const prompt = useSetIdentityPrompt();
- if (isEmpty(prompt)) {
- return null;
- }
return (
- {
- onClose?.();
- prompt?.close();
- },
- }}
- />
+ <>
+
+ >
);
}
diff --git a/packages/next-common/components/pages/people/judgement/authCallback.jsx b/packages/next-common/components/pages/people/judgement/authCallback.jsx
new file mode 100644
index 0000000000..488c72e37c
--- /dev/null
+++ b/packages/next-common/components/pages/people/judgement/authCallback.jsx
@@ -0,0 +1,261 @@
+import ListLayout from "next-common/components/layout/ListLayout";
+import Account from "next-common/components/account";
+import generateLayoutRawTitle from "next-common/utils/generateLayoutRawTitle";
+import PrimaryButton from "next-common/lib/button/primary";
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+import { backendApi } from "next-common/services/nextApi";
+import Link from "next/link";
+import { useEffect, useRef, useState } from "react";
+import { PEOPLE_JUDGEMENT_AUTH_MESSAGE_TYPE } from "next-common/components/people/judgement/consts";
+
+function notifyOpener(payload) {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ try {
+ window.opener?.postMessage(payload, window.location.origin);
+ } catch (e) {
+ // ignore
+ }
+}
+
+function useCloseCountdown(seconds, enabled) {
+ const [time, setTime] = useState(seconds);
+
+ useEffect(() => {
+ if (!enabled) {
+ return;
+ }
+
+ const timer = setInterval(() => {
+ setTime((prev) => {
+ if (prev <= 1) {
+ clearInterval(timer);
+ try {
+ window.close();
+ } catch (e) {
+ // ignore
+ }
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, [enabled]);
+
+ return time;
+}
+
+function useNotifyOpenerOnResult({ provider, result }) {
+ useEffect(() => {
+ if (!result) return;
+
+ notifyOpener({
+ type: PEOPLE_JUDGEMENT_AUTH_MESSAGE_TYPE,
+ provider,
+ ok: !!result.success,
+ who: result.who,
+ message: result.message,
+ ts: Date.now(),
+ });
+ }, [provider, result]);
+}
+
+function useAuthVerifyResult({ code, state, backendCallbackPath }) {
+ const [result, setResult] = useState(null);
+ const hasRequestedRef = useRef(false);
+
+ useEffect(() => {
+ if (hasRequestedRef.current) {
+ return;
+ }
+
+ hasRequestedRef.current = true;
+
+ if (!code || !state) {
+ setResult({ success: false, message: "Missing code or state" });
+ return;
+ }
+
+ let cancelled = false;
+ (async () => {
+ const { result, error } = await backendApi.fetch(backendCallbackPath, {
+ code,
+ state,
+ });
+
+ if (cancelled) {
+ return;
+ }
+
+ if (error) {
+ setResult({
+ success: false,
+ message: error.message || "Verification failed",
+ });
+ return;
+ }
+
+ setResult({
+ success: true,
+ who: result?.who,
+ message: "Verify success",
+ });
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [backendCallbackPath, code, state]);
+
+ return result;
+}
+
+function AuthSuccess({ time }) {
+ return (
+
+ Verification successful! This window will close in {time} seconds…
+
+ );
+}
+
+function AuthFailed({ message, time, providerLabel }) {
+ return (
+
+ {message ? message : `Verify Your ${providerLabel} Account Failed`}
+ {time > 0 ? ` (closing in ${time}s…)` : ""}
+
+ );
+}
+
+function AuthLayout({ providerLabel, children }) {
+ const address = useRealAddress();
+
+ return (
+
+
+ >
+ }
+ >
+ {children}
+
+ );
+}
+
+function AuthResultCard({ providerLabel, result, time }) {
+ return (
+
+ {result === null ? (
+
+ Verifying your {providerLabel} account…
+
+ ) : result?.success ? (
+
+ ) : (
+
+ )}
+
+
+
Go to Judgement Detail
+
+
+
+ );
+}
+
+function AuthErrorFlow({ provider, providerLabel, error, errorDescription }) {
+ const message =
+ error === "access_denied"
+ ? "Authorization cancelled"
+ : `OAuth error: ${error}`;
+
+ const result = {
+ success: false,
+ who: "",
+ message: errorDescription ? `${message}: ${errorDescription}` : message,
+ };
+
+ useNotifyOpenerOnResult({ provider, result });
+ const time = useCloseCountdown(5, true);
+
+ return (
+
+
+
+ );
+}
+
+function AuthVerifyFlow({
+ provider,
+ providerLabel,
+ backendCallbackPath,
+ code,
+ state,
+}) {
+ const result = useAuthVerifyResult({ code, state, backendCallbackPath });
+ useNotifyOpenerOnResult({ provider, result });
+ const time = useCloseCountdown(5, result !== null);
+
+ return (
+
+
+
+ );
+}
+
+export default function PeopleJudgementAuthCallbackPage({
+ provider,
+ providerLabel,
+ backendCallbackPath,
+ code,
+ state,
+ error,
+ errorDescription,
+}) {
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/next-common/components/people/common/getServerSideProps.js b/packages/next-common/components/people/common/getServerSideProps.js
new file mode 100644
index 0000000000..e2d7ea893a
--- /dev/null
+++ b/packages/next-common/components/people/common/getServerSideProps.js
@@ -0,0 +1,7 @@
+import { withCommonProps } from "next-common/lib";
+
+export const getPeopleServerSideProps = withCommonProps(async () => {
+ return {
+ props: {},
+ };
+});
diff --git a/packages/next-common/components/people/hooks/useHasActiveJudgementRequest.js b/packages/next-common/components/people/hooks/useHasActiveJudgementRequest.js
new file mode 100644
index 0000000000..8935ffa8d2
--- /dev/null
+++ b/packages/next-common/components/people/hooks/useHasActiveJudgementRequest.js
@@ -0,0 +1,6 @@
+import useMyJudgementRequest from "./useMyJudgementRequest";
+
+export default function useHasActiveJudgementRequest() {
+ const { value } = useMyJudgementRequest();
+ return !!value;
+}
diff --git a/packages/next-common/components/people/hooks/useMyJudgementRequest.js b/packages/next-common/components/people/hooks/useMyJudgementRequest.js
new file mode 100644
index 0000000000..ab0d1bdea9
--- /dev/null
+++ b/packages/next-common/components/people/hooks/useMyJudgementRequest.js
@@ -0,0 +1,41 @@
+import { backendApi } from "next-common/services/nextApi";
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+import { useCallback, useEffect, useState } from "react";
+
+export default function useMyJudgementRequest() {
+ const realAddress = useRealAddress();
+ const [value, setValue] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const fetch = useCallback(async () => {
+ if (!realAddress) {
+ setValue(null);
+ setLoading(false);
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const { result } = await backendApi.fetch(
+ `people/identities/${realAddress}/active-request`,
+ );
+ if (result) {
+ setValue(result);
+ return;
+ }
+ setValue(null);
+ } finally {
+ setLoading(false);
+ }
+ }, [realAddress]);
+
+ useEffect(() => {
+ fetch();
+ }, [fetch]);
+
+ return {
+ value,
+ loading,
+ fetch,
+ };
+}
diff --git a/packages/next-common/components/people/hooks/usePendingJudgementRequests.js b/packages/next-common/components/people/hooks/usePendingJudgementRequests.js
new file mode 100644
index 0000000000..0eb625536e
--- /dev/null
+++ b/packages/next-common/components/people/hooks/usePendingJudgementRequests.js
@@ -0,0 +1,16 @@
+import { backendApi } from "next-common/services/nextApi";
+import { useAsync } from "react-use";
+
+export default function usePendingJudgementRequests(page, pageSize) {
+ return useAsync(async () => {
+ if (!page || page < 1 || !pageSize || pageSize < 1) {
+ return null;
+ }
+
+ const { result } = await backendApi.fetch(
+ `people/identities/active-requests?page=${page}&pageSize=${pageSize}`,
+ );
+
+ return result || null;
+ }, [page, pageSize]);
+}
diff --git a/packages/next-common/components/people/judgement/cardHeaderLayout.jsx b/packages/next-common/components/people/judgement/cardHeaderLayout.jsx
new file mode 100644
index 0000000000..e325800da7
--- /dev/null
+++ b/packages/next-common/components/people/judgement/cardHeaderLayout.jsx
@@ -0,0 +1,15 @@
+export default function CardHeaderLayout({ tag, actions, Icon, title }) {
+ return (
+
+
+
+
+ ·
+ {title}
+
+
{tag}
+
+ {actions}
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/consts.js b/packages/next-common/components/people/judgement/consts.js
new file mode 100644
index 0000000000..6da2e77a90
--- /dev/null
+++ b/packages/next-common/components/people/judgement/consts.js
@@ -0,0 +1,9 @@
+export const PEOPLE_JUDGEMENT_AUTH_MESSAGE_TYPE = "people:judgement-auth";
+
+export const PeopleSocialType = Object.freeze({
+ discord: "discord",
+ email: "email",
+ twitter: "twitter",
+ github: "github",
+ element: "element",
+});
diff --git a/packages/next-common/components/people/judgement/content.jsx b/packages/next-common/components/people/judgement/content.jsx
new file mode 100644
index 0000000000..5f145f17d7
--- /dev/null
+++ b/packages/next-common/components/people/judgement/content.jsx
@@ -0,0 +1,19 @@
+import { useJudgementContext } from "./context";
+import ContentVerifications from "./contentVerifications";
+import ContentLoading from "./contentLoading";
+import ContentEmpty from "./contentEmpty";
+
+export default function JudgementPageContent() {
+ const { myJudgementRequest: request, isLoadingMyJudgementRequest: loading } =
+ useJudgementContext();
+
+ if (loading) {
+ return ;
+ }
+
+ if (!request) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/packages/next-common/components/people/judgement/contentEmpty.js b/packages/next-common/components/people/judgement/contentEmpty.js
new file mode 100644
index 0000000000..54af8addb7
--- /dev/null
+++ b/packages/next-common/components/people/judgement/contentEmpty.js
@@ -0,0 +1,19 @@
+import Account from "next-common/components/account";
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+import { InfoMessage } from "next-common/components/setting/styled";
+
+export default function ContentEmpty() {
+ const address = useRealAddress();
+ return (
+
+
+
+ You have no identity social account verifications.
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/contentLoading.js b/packages/next-common/components/people/judgement/contentLoading.js
new file mode 100644
index 0000000000..069963a296
--- /dev/null
+++ b/packages/next-common/components/people/judgement/contentLoading.js
@@ -0,0 +1,35 @@
+import Loading from "next-common/components/loading";
+import Account from "next-common/components/account";
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+import SummaryLayout from "next-common/components/summary/layout/layout";
+import SummaryItem from "next-common/components/summary/layout/item";
+import LoadableContent from "next-common/components/common/loadableContent";
+
+export default function ContentLoading() {
+ const address = useRealAddress();
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/packages/next-common/components/people/judgement/contentVerifications.js b/packages/next-common/components/people/judgement/contentVerifications.js
new file mode 100644
index 0000000000..c7a05ad551
--- /dev/null
+++ b/packages/next-common/components/people/judgement/contentVerifications.js
@@ -0,0 +1,63 @@
+import tw from "tailwind-styled-components";
+import Discord from "./discord";
+import Element from "./element";
+import Email from "./email";
+import Github from "./github";
+import JudgementSummary from "./summary";
+import Twitter from "./twitter";
+import { PeopleSocialType } from "./consts";
+
+const SocialAccountWrapper = tw.div`flex bg-neutral100 border-b border-neutral300 p-4 rounded-lg`;
+
+function calcVerificationNumbers(request) {
+ const allSocialTypes = Object.values(PeopleSocialType);
+ const info = request?.info || {};
+ const verifications = request?.verifications || {};
+ const totalSocials = allSocialTypes.filter((key) =>
+ Boolean(info[key === "element" ? "matrix" : key]),
+ ).length;
+ const verified = allSocialTypes.filter(
+ (key) =>
+ Boolean(info[key === "element" ? "matrix" : key]) &&
+ verifications?.[key] === true,
+ ).length;
+ const pending = totalSocials - verified;
+ return { verified, pending };
+}
+
+export default function ContentVerifications({ request }) {
+ const { verified, pending } = calcVerificationNumbers(request);
+
+ return (
+ <>
+
+
+ {request?.info?.email && (
+
+
+
+ )}
+ {request?.info?.matrix && (
+
+
+
+ )}
+ {request?.info?.discord && (
+
+
+
+ )}
+ {request?.info?.twitter && (
+
+
+
+ )}
+ {request?.info?.github && (
+
+
+
+ )}
+
+ >
+ );
+}
diff --git a/packages/next-common/components/people/judgement/context.js b/packages/next-common/components/people/judgement/context.js
new file mode 100644
index 0000000000..0a13052695
--- /dev/null
+++ b/packages/next-common/components/people/judgement/context.js
@@ -0,0 +1,30 @@
+import { createContext, useContext } from "react";
+import useMyJudgementRequest from "../hooks/useMyJudgementRequest";
+
+const JudgementContext = createContext(null);
+
+export default JudgementContext;
+
+export function JudgementContextProvider({ children }) {
+ const {
+ value: myJudgementRequest,
+ loading: isLoadingMyJudgementRequest,
+ fetch: fetchMyJudgementRequest,
+ } = useMyJudgementRequest();
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useJudgementContext() {
+ return useContext(JudgementContext);
+}
diff --git a/packages/next-common/components/people/judgement/discord.jsx b/packages/next-common/components/people/judgement/discord.jsx
new file mode 100644
index 0000000000..66f685dbdb
--- /dev/null
+++ b/packages/next-common/components/people/judgement/discord.jsx
@@ -0,0 +1,25 @@
+import { LinkDiscord } from "@osn/icons/subsquare";
+import usePeopleJudgementSocialAuth from "./hooks/usePeopleJudgementSocialAuth";
+import PeopleJudgementSocialConnect from "./socialConnect";
+import { PeopleSocialType } from "./consts";
+
+export default function Discord({ request }) {
+ const isVerified = request?.verifications?.discord === true;
+ const { loading, connected, openAuthWindow } = usePeopleJudgementSocialAuth({
+ provider: PeopleSocialType.discord,
+ authUrlPath: "people/verifications/auth/discord/auth-url",
+ redirectPath: "/people/verifications/auth/discord",
+ isVerified,
+ });
+
+ return (
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/element/elementAccountRow.jsx b/packages/next-common/components/people/judgement/element/elementAccountRow.jsx
new file mode 100644
index 0000000000..40c4937aeb
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/elementAccountRow.jsx
@@ -0,0 +1,10 @@
+export default function ElementAccountRow({ elementAccount }) {
+ return (
+
+
+ Matrix ID:
+ {elementAccount}
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/element/elementCardHeader.jsx b/packages/next-common/components/people/judgement/element/elementCardHeader.jsx
new file mode 100644
index 0000000000..3da0c6d392
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/elementCardHeader.jsx
@@ -0,0 +1,13 @@
+import CardHeaderLayout from "../cardHeaderLayout";
+import { LinkElement } from "@osn/icons/subsquare";
+
+export default function ElementCardHeader({ tag, actions }) {
+ return (
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/element/elementVerificationTips.jsx b/packages/next-common/components/people/judgement/element/elementVerificationTips.jsx
new file mode 100644
index 0000000000..448441ac13
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/elementVerificationTips.jsx
@@ -0,0 +1,11 @@
+export default function ElementVerificationTips() {
+ return (
+
+
+ Tips: Please login to your Element
+ account and accept our invitation to complete the verification. Code
+ expires in 10 minutes.
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/element/hooks/useFinishElementVerification.js b/packages/next-common/components/people/judgement/element/hooks/useFinishElementVerification.js
new file mode 100644
index 0000000000..c6fcc06331
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/hooks/useFinishElementVerification.js
@@ -0,0 +1,53 @@
+import { useCallback, useState } from "react";
+import { useDispatch } from "react-redux";
+import { nextApi } from "next-common/services/nextApi";
+import { newErrorToast } from "next-common/store/reducers/toastSlice";
+
+export default function useFinishElementVerification({
+ who,
+ setError,
+ onVerified,
+}) {
+ const dispatch = useDispatch();
+ const [verifying, setVerifying] = useState(false);
+
+ const verify = useCallback(async () => {
+ setError?.("");
+ if (!who) {
+ setError?.("Unable to determine who");
+ return;
+ }
+
+ setVerifying(true);
+ try {
+ const { result, error: startError } = await nextApi.fetch(
+ `people/identities/${who}/active-request`,
+ );
+ if (startError) {
+ const message = startError.message || "Failed to start verification";
+ setError?.(message);
+ dispatch(newErrorToast(message));
+ return;
+ }
+
+ const request = result || null;
+
+ const isElementVerified = request?.verifications.element === true;
+ if (isElementVerified) {
+ onVerified?.();
+ } else {
+ const message =
+ "Element verification is not passed yet, please check your Element app.";
+ // setError?.(message);
+ dispatch(newErrorToast(message));
+ }
+ } finally {
+ setVerifying(false);
+ }
+ }, [dispatch, onVerified, setError, who]);
+
+ return {
+ verifying,
+ verify,
+ };
+}
diff --git a/packages/next-common/components/people/judgement/element/hooks/useStartElementVerification.js b/packages/next-common/components/people/judgement/element/hooks/useStartElementVerification.js
new file mode 100644
index 0000000000..e357c99c59
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/hooks/useStartElementVerification.js
@@ -0,0 +1,54 @@
+import { useCallback, useState } from "react";
+import { useDispatch } from "react-redux";
+import { useEnsureLogin } from "next-common/hooks/useEnsureLogin";
+import { nextApi } from "next-common/services/nextApi";
+import {
+ newErrorToast,
+ newSuccessToast,
+} from "next-common/store/reducers/toastSlice";
+
+export default function useStartElementVerification({
+ who,
+ setError,
+ onStarted,
+}) {
+ const dispatch = useDispatch();
+ const { ensureLogin } = useEnsureLogin();
+ const [starting, setStarting] = useState(false);
+
+ const startVerify = useCallback(async () => {
+ setError?.("");
+ if (!who) {
+ setError?.("Unable to determine who");
+ return;
+ }
+
+ setStarting(true);
+ try {
+ if (!(await ensureLogin())) {
+ return;
+ }
+
+ const { result, error: startError } = await nextApi.post(
+ "people/verifications/auth/element/start",
+ { who },
+ );
+ if (startError) {
+ const message = startError.message || "Failed to start verification";
+ setError?.(message);
+ dispatch(newErrorToast(message));
+ return;
+ }
+
+ onStarted?.(result?.code);
+ dispatch(newSuccessToast("Verification code sent"));
+ } finally {
+ setStarting(false);
+ }
+ }, [dispatch, ensureLogin, onStarted, setError, who]);
+
+ return {
+ starting,
+ startVerify,
+ };
+}
diff --git a/packages/next-common/components/people/judgement/element/index.jsx b/packages/next-common/components/people/judgement/element/index.jsx
new file mode 100644
index 0000000000..df1d0bbd00
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/index.jsx
@@ -0,0 +1,30 @@
+import { useEffect, useState } from "react";
+import PendingElementCard from "./pendingElementCard";
+import VerifiedElementCard from "./verifiedElementCard";
+import { useJudgementContext } from "../context";
+
+export default function Element({ request }) {
+ const { fetchMyJudgementRequest } = useJudgementContext();
+ const elementAccount = request?.info?.matrix || "";
+ const initialVerified = request?.verifications?.element === true;
+ const [verified, setVerified] = useState(initialVerified);
+
+ useEffect(() => {
+ setVerified(initialVerified);
+ }, [initialVerified]);
+
+ if (verified) {
+ return ;
+ }
+
+ return (
+ {
+ setVerified(true);
+ fetchMyJudgementRequest();
+ }}
+ />
+ );
+}
diff --git a/packages/next-common/components/people/judgement/element/pendingElementCard.jsx b/packages/next-common/components/people/judgement/element/pendingElementCard.jsx
new file mode 100644
index 0000000000..447edcbbcc
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/pendingElementCard.jsx
@@ -0,0 +1,38 @@
+import { useState } from "react";
+import PendingElementNotStartedCard from "./pendingElementNotStartedCard";
+import PendingElementStartedCard from "./pendingElementStartedCard";
+
+export default function PendingElementCard({
+ request,
+ elementAccount,
+ onVerified,
+}) {
+ const isVerifying = request?.verifications?.element?.verifying === true;
+ const verifyingCode = request?.verifications?.element?.code || "";
+ const [code, setCode] = useState(verifyingCode);
+ const [hasStarted, setHasStarted] = useState(isVerifying);
+
+ if (!hasStarted) {
+ return (
+ {
+ setCode(verificationCode);
+ setHasStarted(true);
+ }}
+ />
+ );
+ }
+
+ return (
+ {
+ onVerified?.();
+ }}
+ />
+ );
+}
diff --git a/packages/next-common/components/people/judgement/element/pendingElementNotStartedCard.jsx b/packages/next-common/components/people/judgement/element/pendingElementNotStartedCard.jsx
new file mode 100644
index 0000000000..e0bb5c3596
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/pendingElementNotStartedCard.jsx
@@ -0,0 +1,45 @@
+import { useState } from "react";
+import { WarningTag } from "next-common/components/tags/state/styled";
+import SecondaryButton from "next-common/lib/button/secondary";
+import ElementCardHeader from "./elementCardHeader";
+import ElementAccountRow from "./elementAccountRow";
+import useStartElementVerification from "./hooks/useStartElementVerification";
+import Tooltip from "next-common/components/tooltip";
+
+export default function PendingElementNotStartedCard({
+ request,
+ elementAccount,
+ onStarted,
+}) {
+ const [error, setError] = useState("");
+ const who = request?.who || "";
+
+ const { starting, startVerify } = useStartElementVerification({
+ who,
+ setError,
+ onStarted,
+ });
+
+ return (
+
+
Pending}
+ actions={
+
+
+
+ Start verify
+
+
+
+ }
+ />
+
+ {error && {error}
}
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/element/pendingElementStartedCard.jsx b/packages/next-common/components/people/judgement/element/pendingElementStartedCard.jsx
new file mode 100644
index 0000000000..f96c85801d
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/pendingElementStartedCard.jsx
@@ -0,0 +1,64 @@
+import { useState } from "react";
+import { WarningTag } from "next-common/components/tags/state/styled";
+import SecondaryButton from "next-common/lib/button/secondary";
+import Tooltip from "next-common/components/tooltip";
+import ElementCardHeader from "./elementCardHeader";
+import ElementAddressRow from "./elementAccountRow";
+import ElementVerificationTips from "./elementVerificationTips";
+import useFinishElementVerification from "./hooks/useFinishElementVerification";
+import Copyable from "next-common/components/copyable";
+import { GreyPanel } from "next-common/components/styled/containers/greyPanel";
+
+export default function PendingElementStartedCard({
+ request,
+ elementAccount,
+ verificationCode,
+ onVerified,
+}) {
+ const [error, setError] = useState("");
+ const who = request?.who || "";
+
+ const { verifying, verify } = useFinishElementVerification({
+ who,
+ code: verificationCode,
+ setError,
+ onVerified: () => {
+ onVerified?.();
+ },
+ });
+
+ return (
+
+
Pending}
+ actions={
+
+
+
+ Confirm finished
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+ {verificationCode}
+
+
+ {error &&
{error}
}
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/element/verifiedElementCard.jsx b/packages/next-common/components/people/judgement/element/verifiedElementCard.jsx
new file mode 100644
index 0000000000..3971e4c76f
--- /dev/null
+++ b/packages/next-common/components/people/judgement/element/verifiedElementCard.jsx
@@ -0,0 +1,13 @@
+import { PositiveTag } from "next-common/components/tags/state/styled";
+import ElementCardHeader from "./elementCardHeader";
+import ElementAccountRow from "./elementAccountRow";
+
+export default function VerifiedElementCard({ elementAccount }) {
+ return (
+
+ Verified} />
+
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/email/emailAddressRow.jsx b/packages/next-common/components/people/judgement/email/emailAddressRow.jsx
new file mode 100644
index 0000000000..89726276c3
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/emailAddressRow.jsx
@@ -0,0 +1,12 @@
+export default function EmailAddressRow({ email }) {
+ return (
+
+
+
+ Email Address:
+
+ {email}
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/email/emailCardHeader.jsx b/packages/next-common/components/people/judgement/email/emailCardHeader.jsx
new file mode 100644
index 0000000000..b33632c1b5
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/emailCardHeader.jsx
@@ -0,0 +1,13 @@
+import CardHeaderLayout from "../cardHeaderLayout";
+import { LinkEmail } from "@osn/icons/subsquare";
+
+export default function EmailCardHeader({ tag, actions }) {
+ return (
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/email/emailVerificationTips.jsx b/packages/next-common/components/people/judgement/email/emailVerificationTips.jsx
new file mode 100644
index 0000000000..e9b8f1ac0c
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/emailVerificationTips.jsx
@@ -0,0 +1,10 @@
+export default function EmailVerificationTips() {
+ return (
+
+
+ Tips: We‘ve sent a 6-digit
+ code to your email. Code expires in 10 minutes.
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/email/hooks/useSendJudgementEmailCode.js b/packages/next-common/components/people/judgement/email/hooks/useSendJudgementEmailCode.js
new file mode 100644
index 0000000000..e371aeca40
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/hooks/useSendJudgementEmailCode.js
@@ -0,0 +1,50 @@
+import { useCallback, useState } from "react";
+import { useDispatch } from "react-redux";
+import { useEnsureLogin } from "next-common/hooks/useEnsureLogin";
+import { nextApi } from "next-common/services/nextApi";
+import {
+ newErrorToast,
+ newSuccessToast,
+} from "next-common/store/reducers/toastSlice";
+
+export default function useSendJudgementEmailCode({ who, setError, onSent }) {
+ const dispatch = useDispatch();
+ const { ensureLogin } = useEnsureLogin();
+ const [sending, setSending] = useState(false);
+
+ const sendCode = useCallback(async () => {
+ setError?.("");
+ if (!who) {
+ setError?.("Unable to determine who");
+ return;
+ }
+
+ setSending(true);
+ try {
+ if (!(await ensureLogin())) {
+ return;
+ }
+
+ const { error: sendError } = await nextApi.post(
+ "people/verifications/auth/email/send-code",
+ { who },
+ );
+ if (sendError) {
+ const message = sendError.message || "Failed to send code";
+ setError?.(message);
+ dispatch(newErrorToast(message));
+ return;
+ }
+
+ onSent?.();
+ dispatch(newSuccessToast("Verification code sent"));
+ } finally {
+ setSending(false);
+ }
+ }, [dispatch, ensureLogin, onSent, setError, who]);
+
+ return {
+ sending,
+ sendCode,
+ };
+}
diff --git a/packages/next-common/components/people/judgement/email/hooks/useVerifyJudgementEmailCode.js b/packages/next-common/components/people/judgement/email/hooks/useVerifyJudgementEmailCode.js
new file mode 100644
index 0000000000..9e963f4aa7
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/hooks/useVerifyJudgementEmailCode.js
@@ -0,0 +1,55 @@
+import { useCallback, useState } from "react";
+import { useDispatch } from "react-redux";
+import { useEnsureLogin } from "next-common/hooks/useEnsureLogin";
+import { nextApi } from "next-common/services/nextApi";
+import { newSuccessToast } from "next-common/store/reducers/toastSlice";
+
+export default function useVerifyJudgementEmailCode({
+ who,
+ code,
+ setError,
+ onVerified,
+}) {
+ const dispatch = useDispatch();
+ const { ensureLogin } = useEnsureLogin();
+ const [verifying, setVerifying] = useState(false);
+
+ const verifyCode = useCallback(async () => {
+ setError?.("");
+ if (!who) {
+ setError?.("Unable to determine who");
+ return;
+ }
+
+ if (!code) {
+ setError?.("Code is required");
+ return;
+ }
+
+ setVerifying(true);
+ try {
+ if (!(await ensureLogin())) {
+ return;
+ }
+
+ const { error: verifyError } = await nextApi.post(
+ "people/verifications/auth/email/verify-code",
+ { who, code },
+ );
+ if (verifyError) {
+ setError?.(verifyError.message || "Verification failed");
+ return;
+ }
+
+ dispatch(newSuccessToast("Email verified"));
+ onVerified?.();
+ } finally {
+ setVerifying(false);
+ }
+ }, [code, dispatch, ensureLogin, onVerified, setError, who]);
+
+ return {
+ verifying,
+ verifyCode,
+ };
+}
diff --git a/packages/next-common/components/people/judgement/email/index.jsx b/packages/next-common/components/people/judgement/email/index.jsx
new file mode 100644
index 0000000000..a3bcb67bf8
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/index.jsx
@@ -0,0 +1,30 @@
+import { useEffect, useState } from "react";
+import PendingEmailCard from "./pendingEmailCard";
+import VerifiedEmailCard from "./verifiedEmailCard";
+import { useJudgementContext } from "../context";
+
+export default function Email({ request }) {
+ const { fetchMyJudgementRequest } = useJudgementContext();
+ const email = request?.info?.email || "";
+ const initialVerified = request?.verifications?.email === true;
+ const [verified, setVerified] = useState(initialVerified);
+
+ useEffect(() => {
+ setVerified(initialVerified);
+ }, [initialVerified]);
+
+ if (verified) {
+ return ;
+ }
+
+ return (
+ {
+ setVerified(true);
+ fetchMyJudgementRequest();
+ }}
+ />
+ );
+}
diff --git a/packages/next-common/components/people/judgement/email/pendingEmailCard.jsx b/packages/next-common/components/people/judgement/email/pendingEmailCard.jsx
new file mode 100644
index 0000000000..c3d360a114
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/pendingEmailCard.jsx
@@ -0,0 +1,40 @@
+import { useState } from "react";
+import useCountdown from "next-common/hooks/useCountdown";
+import PendingEmailNotSentCard from "./pendingEmailNotSentCard";
+import PendingEmailSentCard from "./pendingEmailSentCard";
+
+export default function PendingEmailCard({ request, email, onVerified }) {
+ const [hasSent, setHasSent] = useState(false);
+
+ const {
+ countdown,
+ start: startCountdown,
+ stop: stopCountdown,
+ } = useCountdown();
+
+ if (!hasSent) {
+ return (
+ {
+ setHasSent(true);
+ startCountdown(60);
+ }}
+ />
+ );
+ }
+
+ return (
+ {
+ stopCountdown();
+ onVerified?.();
+ }}
+ />
+ );
+}
diff --git a/packages/next-common/components/people/judgement/email/pendingEmailNotSentCard.jsx b/packages/next-common/components/people/judgement/email/pendingEmailNotSentCard.jsx
new file mode 100644
index 0000000000..d4305da993
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/pendingEmailNotSentCard.jsx
@@ -0,0 +1,41 @@
+import { useState } from "react";
+import { WarningTag } from "next-common/components/tags/state/styled";
+import SecondaryButton from "next-common/lib/button/secondary";
+import EmailCardHeader from "./emailCardHeader";
+import EmailAddressRow from "./emailAddressRow";
+import useSendJudgementEmailCode from "./hooks/useSendJudgementEmailCode";
+import Tooltip from "next-common/components/tooltip";
+
+export default function PendingEmailNotSentCard({ request, email, onSent }) {
+ const [error, setError] = useState("");
+ const who = request?.who || "";
+
+ const { sending, sendCode } = useSendJudgementEmailCode({
+ who,
+ setError,
+ onSent,
+ });
+
+ return (
+
+
Pending}
+ actions={
+
+
+
+ Send code
+
+
+
+ }
+ />
+
+ {error && {error}
}
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/email/pendingEmailSentCard.jsx b/packages/next-common/components/people/judgement/email/pendingEmailSentCard.jsx
new file mode 100644
index 0000000000..d72bbb756b
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/pendingEmailSentCard.jsx
@@ -0,0 +1,97 @@
+import { useState } from "react";
+import { WarningTag } from "next-common/components/tags/state/styled";
+import SecondaryButton from "next-common/lib/button/secondary";
+import Input from "next-common/lib/input";
+import EmailCardHeader from "./emailCardHeader";
+import EmailAddressRow from "./emailAddressRow";
+import EmailVerificationTips from "./emailVerificationTips";
+import useSendJudgementEmailCode from "./hooks/useSendJudgementEmailCode";
+import useVerifyJudgementEmailCode from "./hooks/useVerifyJudgementEmailCode";
+import Tooltip from "next-common/components/tooltip";
+
+export default function PendingEmailSentCard({
+ request,
+ email,
+ countdown,
+ startCountdown,
+ onVerified,
+}) {
+ const [error, setError] = useState("");
+ const who = request?.who || "";
+ const [code, setCode] = useState("");
+ const trimmedCode = String(code || "").trim();
+ const canVerify = Boolean(trimmedCode);
+
+ const { sending, sendCode } = useSendJudgementEmailCode({
+ who,
+ setError,
+ onSent: () => {
+ startCountdown(60);
+ },
+ });
+
+ const { verifying, verifyCode } = useVerifyJudgementEmailCode({
+ who,
+ code: trimmedCode,
+ setError,
+ onVerified: () => {
+ onVerified?.();
+ },
+ });
+
+ let message = "Click to verify the code";
+ if (!canVerify) {
+ message = "Please enter the verification code to verify";
+ }
+
+ return (
+
+
Pending}
+ actions={
+
+
+ 0}
+ size="small"
+ >
+ Resend{countdown > 0 ? ` ${countdown}s` : ""}
+
+
+
+
+ Verify code
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+ setCode(e.target.value)}
+ />
+
+ {error &&
{error}
}
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/email/verifiedEmailCard.jsx b/packages/next-common/components/people/judgement/email/verifiedEmailCard.jsx
new file mode 100644
index 0000000000..1281cf549f
--- /dev/null
+++ b/packages/next-common/components/people/judgement/email/verifiedEmailCard.jsx
@@ -0,0 +1,13 @@
+import { PositiveTag } from "next-common/components/tags/state/styled";
+import EmailCardHeader from "./emailCardHeader";
+import EmailAddressRow from "./emailAddressRow";
+
+export default function VerifiedEmailCard({ email }) {
+ return (
+
+ Verified} />
+
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/github.jsx b/packages/next-common/components/people/judgement/github.jsx
new file mode 100644
index 0000000000..a8f002ce64
--- /dev/null
+++ b/packages/next-common/components/people/judgement/github.jsx
@@ -0,0 +1,26 @@
+import { LinkGithub } from "@osn/icons/subsquare";
+import usePeopleJudgementSocialAuth from "./hooks/usePeopleJudgementSocialAuth";
+import PeopleJudgementSocialConnect from "./socialConnect";
+import { PeopleSocialType } from "./consts";
+
+export default function Github({ request }) {
+ const isVerified = request?.verifications?.github === true;
+
+ const { loading, connected, openAuthWindow } = usePeopleJudgementSocialAuth({
+ provider: PeopleSocialType.github,
+ authUrlPath: "people/verifications/auth/github/auth-url",
+ redirectPath: "/people/verifications/auth/github",
+ isVerified,
+ });
+
+ return (
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/hooks/usePeopleJudgementSocialAuth.js b/packages/next-common/components/people/judgement/hooks/usePeopleJudgementSocialAuth.js
new file mode 100644
index 0000000000..f3091f95cc
--- /dev/null
+++ b/packages/next-common/components/people/judgement/hooks/usePeopleJudgementSocialAuth.js
@@ -0,0 +1,92 @@
+import { backendApi } from "next-common/services/nextApi";
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+import { trimEndSlash } from "next-common/utils/url";
+import { useCallback, useEffect, useState } from "react";
+import { PEOPLE_JUDGEMENT_AUTH_MESSAGE_TYPE } from "next-common/components/people/judgement/consts";
+import { useJudgementContext } from "../context";
+
+function isJudgementAuthOpenerMessage(data, provider) {
+ return (
+ data?.type === PEOPLE_JUDGEMENT_AUTH_MESSAGE_TYPE &&
+ data?.provider === provider
+ );
+}
+
+export default function usePeopleJudgementSocialAuth({
+ provider,
+ authUrlPath,
+ redirectPath,
+ isVerified,
+}) {
+ const { fetchMyJudgementRequest } = useJudgementContext();
+ const realAddress = useRealAddress();
+
+ const [loading, setLoading] = useState(false);
+ const [connected, setConnected] = useState(isVerified);
+
+ useEffect(() => {
+ setConnected(isVerified);
+ }, [isVerified]);
+
+ const getAuthLink = useCallback(async () => {
+ if (!realAddress) {
+ return "";
+ }
+
+ setLoading(true);
+ try {
+ const redirect = `${trimEndSlash(
+ process.env.NEXT_PUBLIC_SITE_URL,
+ )}${redirectPath}`;
+ const { result } = await backendApi.fetch(
+ `${authUrlPath}?who=${realAddress}&redirectUri=${redirect}`,
+ );
+ return result?.url || "";
+ } finally {
+ setLoading(false);
+ }
+ }, [authUrlPath, realAddress, redirectPath]);
+
+ const openAuthWindow = useCallback(async () => {
+ const link = await getAuthLink();
+ if (!link) {
+ return;
+ }
+
+ window.open(link);
+ }, [getAuthLink]);
+
+ useEffect(() => {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ const handleMessage = (event) => {
+ if (event.origin !== window.location.origin) {
+ return;
+ }
+
+ const data = event.data;
+ if (!isJudgementAuthOpenerMessage(data, provider)) {
+ return;
+ }
+
+ if (data?.ok && data?.who && data.who === realAddress) {
+ setConnected(true);
+ fetchMyJudgementRequest();
+ }
+ };
+
+ window.addEventListener("message", handleMessage);
+
+ return () => {
+ window.removeEventListener("message", handleMessage);
+ };
+ }, [provider, realAddress, fetchMyJudgementRequest]);
+
+ return {
+ loading,
+ connected,
+ openAuthWindow,
+ };
+}
diff --git a/packages/next-common/components/people/judgement/index.jsx b/packages/next-common/components/people/judgement/index.jsx
new file mode 100644
index 0000000000..1e67bfecc7
--- /dev/null
+++ b/packages/next-common/components/people/judgement/index.jsx
@@ -0,0 +1,37 @@
+import ListLayout from "next-common/components/layout/ListLayout";
+import ChainSocialLinks from "next-common/components/chain/socialLinks";
+import PeopleCommonProvider from "next-common/components/people/common/commonProvider";
+import generateLayoutRawTitle from "next-common/utils/generateLayoutRawTitle";
+import NoWalletConnected from "next-common/components/noWalletConnected";
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+import JudgementPageContent from "./content";
+import { JudgementContextProvider } from "./context";
+
+export default function VerificationsPage() {
+ const realAddress = useRealAddress();
+
+ return (
+
+ }
+ >
+ {!realAddress ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/socialConnect.jsx b/packages/next-common/components/people/judgement/socialConnect.jsx
new file mode 100644
index 0000000000..2b5e735332
--- /dev/null
+++ b/packages/next-common/components/people/judgement/socialConnect.jsx
@@ -0,0 +1,49 @@
+import {
+ WarningTag,
+ PositiveTag,
+} from "next-common/components/tags/state/styled";
+import Tooltip from "next-common/components/tooltip";
+import PrimaryButton from "next-common/lib/button/primary";
+import CardHeaderLayout from "./cardHeaderLayout";
+
+export default function PeopleJudgementSocialConnect({
+ Icon,
+ title,
+ username,
+ connected,
+ loading,
+ onConnect,
+}) {
+ let message = `Click to verify your ${title} account`;
+ let tag = Pending;
+ if (connected) {
+ message = `${title} account already verified`;
+ tag = Verified;
+ }
+
+ return (
+
+
+
+
+
+ Username:
+ {username}
+
+
+ {!connected && (
+
+
+ Verify
+
+
+ )}
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/summary.js b/packages/next-common/components/people/judgement/summary.js
new file mode 100644
index 0000000000..c46e30900b
--- /dev/null
+++ b/packages/next-common/components/people/judgement/summary.js
@@ -0,0 +1,33 @@
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+import SummaryLayout from "next-common/components/summary/layout/layout";
+import SummaryItem from "next-common/components/summary/layout/item";
+import Account from "next-common/components/account";
+import { InfoMessage } from "next-common/components/setting/styled";
+
+export default function JudgementSummary({ verified, pending }) {
+ const address = useRealAddress();
+
+ return (
+
+
+
+
+
+ {verified}
+
+
+ {pending}
+
+
+
+ {pending === 0 && (
+
+ All social account verifications are done. Our registrar will do a
+ final check and give the judgement soon.
+
+ )}
+
+ );
+}
diff --git a/packages/next-common/components/people/judgement/twitter.jsx b/packages/next-common/components/people/judgement/twitter.jsx
new file mode 100644
index 0000000000..d28c68bf47
--- /dev/null
+++ b/packages/next-common/components/people/judgement/twitter.jsx
@@ -0,0 +1,26 @@
+import { LinkTwitter } from "@osn/icons/subsquare";
+import usePeopleJudgementSocialAuth from "./hooks/usePeopleJudgementSocialAuth";
+import PeopleJudgementSocialConnect from "./socialConnect";
+import { PeopleSocialType } from "./consts";
+
+export default function Twitter({ request }) {
+ const isVerified = request?.verifications?.twitter === true;
+
+ const { loading, connected, openAuthWindow } = usePeopleJudgementSocialAuth({
+ provider: PeopleSocialType.twitter,
+ authUrlPath: "people/verifications/auth/twitter/auth-url",
+ redirectPath: "/people/verifications/auth/twitter",
+ isVerified,
+ });
+
+ return (
+
+ );
+}
diff --git a/packages/next-common/components/people/judgementRequests/index.jsx b/packages/next-common/components/people/judgementRequests/index.jsx
new file mode 100644
index 0000000000..f8848f72f1
--- /dev/null
+++ b/packages/next-common/components/people/judgementRequests/index.jsx
@@ -0,0 +1,20 @@
+import ListLayout from "next-common/components/layout/ListLayout";
+import ChainSocialLinks from "next-common/components/chain/socialLinks";
+import PeopleCommonProvider from "next-common/components/people/common/commonProvider";
+import generateLayoutRawTitle from "next-common/utils/generateLayoutRawTitle";
+import JudgementRequestsList from "./list";
+
+export default function JudgementRequestsPage() {
+ return (
+
+ }
+ >
+
+
+
+ );
+}
diff --git a/packages/next-common/components/people/judgementRequests/list.jsx b/packages/next-common/components/people/judgementRequests/list.jsx
new file mode 100644
index 0000000000..59b478e46b
--- /dev/null
+++ b/packages/next-common/components/people/judgementRequests/list.jsx
@@ -0,0 +1,177 @@
+import { SystemVoteAye } from "@osn/icons/subsquare";
+import { useEffect, useMemo, useState } from "react";
+
+import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard";
+import ScrollerX from "next-common/components/styled/containers/scrollerX";
+import { MapDataList } from "next-common/components/dataList";
+import ListTitleBar from "next-common/components/listTitleBar";
+import Tooltip from "next-common/components/tooltip";
+import usePaginationComponent from "next-common/components/pagination/usePaginationComponent";
+import { AddressUser } from "next-common/components/user";
+
+import usePendingJudgementRequests from "next-common/components/people/hooks/usePendingJudgementRequests";
+import { PeopleSocialType } from "next-common/components/people/judgement/consts";
+
+const PAGE_SIZE = 20;
+
+function getSocialInfoKey(type) {
+ return type === PeopleSocialType.element ? "matrix" : type;
+}
+
+function getConfiguredSocialAccounts(info = {}) {
+ const allTypes = Object.values(PeopleSocialType);
+ return allTypes
+ .map((type) => {
+ const infoKey = getSocialInfoKey(type);
+ const value = info?.[infoKey];
+ if (!value) {
+ return null;
+ }
+ return { type, value };
+ })
+ .filter(Boolean);
+}
+
+function getSocialCounts(info = {}, verifications = {}) {
+ const configured = getConfiguredSocialAccounts(info);
+ const total = configured.length;
+ const verified = configured.filter(
+ ({ type }) => verifications?.[type] === true,
+ ).length;
+ return { verified, total };
+}
+
+function SocialAccountsTooltipContent({ judgementRequest }) {
+ const info = judgementRequest?.info || {};
+ const verifications = judgementRequest?.verifications || {};
+ const configured = getConfiguredSocialAccounts(info);
+
+ if (!configured.length) {
+ return No social accounts set
;
+ }
+
+ return (
+
+ {configured.map(({ type, value }) => (
+
+
+ {type}
+
+ {value}
+
+ {verifications?.[type] === true && (
+
+ )}
+
+
+ ))}
+
+ );
+}
+
+function SocialAccounts({ judgementRequest }) {
+ const { verified, total } = getSocialCounts(
+ judgementRequest?.info,
+ judgementRequest?.verifications,
+ );
+
+ return (
+
+ }
+ >
+
+ {verified}
+ /
+ {total}
+
+
+ );
+}
+
+function AllVerified({ judgementRequest }) {
+ const { verified, total } = getSocialCounts(
+ judgementRequest?.info,
+ judgementRequest?.verifications,
+ );
+
+ const allVerified = total > 0 && verified === total;
+ return allVerified ? (
+
+ ) : (
+ -
+ );
+}
+
+export default function JudgementRequestsList() {
+ const [total, setTotal] = useState(0);
+ const { page, component: pagination } = usePaginationComponent(
+ total,
+ PAGE_SIZE,
+ {
+ buttonMode: true,
+ },
+ );
+
+ const { value: pageData, loading } = usePendingJudgementRequests(
+ page,
+ PAGE_SIZE,
+ );
+
+ useEffect(() => {
+ setTotal(pageData?.total || 0);
+ }, [pageData?.total]);
+
+ const items = pageData?.items || [];
+
+ const columnsDef = useMemo(
+ () => [
+ {
+ name: "Applicant",
+ style: { textAlign: "left", minWidth: "240px" },
+ render: (judgementRequest) => (
+
+ ),
+ },
+ {
+ name: "Social Accounts",
+ style: { textAlign: "left", width: "220px", minWidth: "220px" },
+ render: (judgementRequest) => (
+
+ ),
+ },
+ {
+ name: "All Verified",
+ style: { textAlign: "left", width: "120px", minWidth: "120px" },
+ render: (judgementRequest) => (
+
+ ),
+ },
+ ],
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ `${item?.who || ""}-${item?.indexer?.blockHeight || ""}`
+ }
+ />
+
+ {pagination}
+
+
+ );
+}
diff --git a/packages/next-common/components/people/overview/hooks/useDefaultRegistrar.js b/packages/next-common/components/people/overview/hooks/useDefaultRegistrar.js
new file mode 100644
index 0000000000..d4d01cb6d3
--- /dev/null
+++ b/packages/next-common/components/people/overview/hooks/useDefaultRegistrar.js
@@ -0,0 +1,40 @@
+import { useState, useMemo, useEffect } from "react";
+import { isEmpty, isNil } from "lodash-es";
+import { useRegistrarContext } from "next-common/context/people/registrarContext";
+import useJudgementsData from "next-common/components/people/overview/hooks/useJudgementsData";
+
+export default function useDefaultRegistrar(defaultRegistrarIndex) {
+ const { registrars } = useRegistrarContext();
+ const { data = [] } = useJudgementsData();
+ const [dafaultValue, setDefaultValue] = useState(null);
+
+ const selectableRegistrars = useMemo(() => {
+ if (isEmpty(registrars)) {
+ return [];
+ }
+
+ return registrars?.filter(
+ (s) => !data?.some((r) => r.account === s.account),
+ );
+ }, [data, registrars]);
+
+ const defaultRegistrarObj = useMemo(() => {
+ if (isEmpty(selectableRegistrars)) {
+ return null;
+ }
+
+ return selectableRegistrars?.find(
+ (registrar) => String(registrar?.index) === defaultRegistrarIndex,
+ );
+ }, [selectableRegistrars, defaultRegistrarIndex]);
+
+ useEffect(() => {
+ if (isNil(defaultRegistrarObj)) {
+ return;
+ }
+
+ setDefaultValue(defaultRegistrarObj?.account);
+ }, [defaultRegistrarObj]);
+
+ return dafaultValue;
+}
diff --git a/packages/next-common/components/people/overview/identity/checkJudgement.jsx b/packages/next-common/components/people/overview/identity/checkJudgement.jsx
new file mode 100644
index 0000000000..b8b4d6bec6
--- /dev/null
+++ b/packages/next-common/components/people/overview/identity/checkJudgement.jsx
@@ -0,0 +1,105 @@
+import { useState, useCallback, useMemo } from "react";
+import { useIdentityInfoContext } from "next-common/context/people/identityInfoContext";
+import dynamicPopup from "next-common/lib/dynamic/popup";
+import { isIdentityEmpty } from "next-common/components/people/common";
+import PrimaryButton from "next-common/lib/button/primary";
+import { isEmpty } from "lodash-es";
+import { useRouter } from "next/router";
+import useDefaultRegistrar from "next-common/components/people/overview/hooks/useDefaultRegistrar";
+
+const RequestJudgementPopup = dynamicPopup(
+ () => import("next-common/components/requestJudgementPopup"),
+ {
+ ssr: false,
+ },
+);
+
+// TODO: default subsquare registrar index.
+const SUBSQUARE_REGISTRAR_INDEX = "1";
+const NO_ACTION_REQUIRED_STATUSES = ["Reasonable", "KnownGood", "Erroneous"];
+
+function isPendingRequestBySubsquare(judgements) {
+ return judgements?.some(
+ (judgement) => judgement.index === SUBSQUARE_REGISTRAR_INDEX,
+ );
+}
+
+function isNoActionRequired(judgements) {
+ return judgements?.some((judgement) =>
+ NO_ACTION_REQUIRED_STATUSES.includes(judgement.status),
+ );
+}
+
+function RequestJudgementPopupImpl({ onClose }) {
+ const defaultRegistrar = useDefaultRegistrar(SUBSQUARE_REGISTRAR_INDEX);
+
+ return (
+
+ );
+}
+
+function RequestJudgement() {
+ const [showPopup, setShowPopup] = useState(false);
+ const { info, isLoading } = useIdentityInfoContext();
+
+ const isInfoEmpty = useMemo(() => isIdentityEmpty(info), [info]);
+
+ const handleOpenPopup = useCallback(() => {
+ setShowPopup(true);
+ }, []);
+
+ const handleClosePopup = useCallback(() => {
+ setShowPopup(false);
+ }, []);
+
+ if (isLoading || isInfoEmpty) {
+ return null;
+ }
+
+ return (
+ <>
+
+ Request Judgement
+
+ {showPopup && }
+ >
+ );
+}
+
+function CheckSubsquareJudgement() {
+ const router = useRouter();
+
+ const handleCheckJudgement = useCallback(() => {
+ router.push("/people/judgement");
+ }, [router]);
+
+ return (
+
+ Check Your Judgement
+
+ );
+}
+
+export default function CheckJudgement() {
+ const { judgements, isLoading } = useIdentityInfoContext();
+ if (isLoading || isNoActionRequired(judgements)) {
+ return null;
+ }
+
+ if (isEmpty(judgements)) {
+ return ;
+ }
+
+ if (isPendingRequestBySubsquare(judgements)) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/packages/next-common/components/people/overview/identity/directIdentityActions.jsx b/packages/next-common/components/people/overview/identity/directIdentityActions.jsx
index e500e790ad..1e16d0019f 100644
--- a/packages/next-common/components/people/overview/identity/directIdentityActions.jsx
+++ b/packages/next-common/components/people/overview/identity/directIdentityActions.jsx
@@ -13,6 +13,7 @@ import { useExtensionAccounts } from "next-common/components/popupWithSigner/con
import getChainSettings from "next-common/utils/consts/settings";
import { useCallback, useState } from "react";
import dynamicPopup from "next-common/lib/dynamic/popup";
+import CheckJudgement from "./checkJudgement";
const SetIdentityPopup = dynamicPopup(
() => import("next-common/components/setIdentityPopup"),
@@ -65,6 +66,7 @@ export function DirectIdentityActions() {
return (
<>
+
setShowSetIdentityPopup(true)}
>
-
+
-
+
diff --git a/packages/next-common/components/requestJudgementPopup/content.jsx b/packages/next-common/components/requestJudgementPopup/content.jsx
index 88c302e48b..128e27478b 100644
--- a/packages/next-common/components/requestJudgementPopup/content.jsx
+++ b/packages/next-common/components/requestJudgementPopup/content.jsx
@@ -26,8 +26,10 @@ const StyledSignerWithBalance = styled.div`
}
`;
-export default function RequestJudgementPopupContent() {
- const [value, setValue] = useState();
+export default function RequestJudgementPopupContent({
+ defaultRegistrar = null,
+}) {
+ const [value, setValue] = useState(defaultRegistrar);
const { symbol, decimals } = useChainSettings();
const { registrars, isLoading } = useRegistrarContext();
const api = useContextApi();
diff --git a/packages/next-common/components/requestJudgementPopup/index.jsx b/packages/next-common/components/requestJudgementPopup/index.jsx
index 4eb78519cb..2df3666c39 100644
--- a/packages/next-common/components/requestJudgementPopup/index.jsx
+++ b/packages/next-common/components/requestJudgementPopup/index.jsx
@@ -2,11 +2,14 @@ import Popup from "../popup/wrapper/Popup";
import SignerPopupWrapper from "../popupWithSigner/signerPopupWrapper";
import RequestJudgementPopupContent from "./content";
-export default function RequestJudgementPopup({ onClose }) {
+export default function RequestJudgementPopup({
+ onClose,
+ defaultRegistrar = null,
+}) {
return (
-
+
);
diff --git a/packages/next-common/components/tags/state/styled.js b/packages/next-common/components/tags/state/styled.js
index d048222526..7c090d00bb 100644
--- a/packages/next-common/components/tags/state/styled.js
+++ b/packages/next-common/components/tags/state/styled.js
@@ -32,6 +32,10 @@ export const NegativeTag = styled(Common)`
background: var(--red500);
`;
+export const WarningTag = styled(Common)`
+ background: var(--orange500);
+`;
+
export const ClosedTag = styled(Common)`
background: var(--neutral500);
`;
diff --git a/packages/next-common/context/api/index.js b/packages/next-common/context/api/index.js
index 781d346187..dc5c69e82b 100644
--- a/packages/next-common/context/api/index.js
+++ b/packages/next-common/context/api/index.js
@@ -20,11 +20,19 @@ export default function ApiProvider({ children }) {
return;
}
- getOriginApi(chain, currentEndpoint).then((api) => {
- if (isMounted()) {
- setNowApi(api);
- }
- });
+ getOriginApi(chain, currentEndpoint)
+ .then((api) => {
+ if (isMounted()) {
+ setNowApi(api);
+ }
+ })
+ .catch((error) => {
+ console.error("Failed to getOriginApi", {
+ chain,
+ currentEndpoint,
+ error,
+ });
+ });
}, [currentEndpoint, dispatch, endpoints, chain, isMounted]);
return {children};
diff --git a/packages/next-common/context/global.js b/packages/next-common/context/global.js
index 0654bf3b04..653d1cb431 100644
--- a/packages/next-common/context/global.js
+++ b/packages/next-common/context/global.js
@@ -39,12 +39,12 @@ export default function GlobalProvider({
-
-
+
+
-
-
+
+
diff --git a/packages/next-common/context/nav/index.jsx b/packages/next-common/context/nav/index.jsx
index 1448a92a88..3b26deeb3a 100644
--- a/packages/next-common/context/nav/index.jsx
+++ b/packages/next-common/context/nav/index.jsx
@@ -4,6 +4,9 @@ import { useCookieValue } from "next-common/utils/hooks/useCookieValue";
import { createContext, useContext, useMemo, useState } from "react";
import { useIsomorphicLayoutEffect } from "react-use";
import { matchNewMenu } from "next-common/utils/consts/menu";
+import { useRouter } from "next/router";
+import useIsAdmin from "next-common/hooks/useIsAdmin";
+import useHasActiveJudgementRequest from "next-common/components/people/hooks/useHasActiveJudgementRequest";
const NavCollapsedContext = createContext([]);
const NavSubmenuVisibleContext = createContext([]);
@@ -29,6 +32,7 @@ export default function NavProvider({
export function useNavSubmenuVisible() {
return useContext(NavSubmenuVisibleContext);
}
+
function NavCollapsedProvider({ children, value }) {
try {
value = JSON.parse(value);
@@ -50,6 +54,7 @@ function NavCollapsedProvider({ children, value }) {
export function useNavCollapsed() {
return useContext(NavCollapsedContext);
}
+
function NavSubmenuVisibleProvider({ children, value }) {
try {
value = JSON.parse(decodeURIComponent(value));
@@ -71,14 +76,19 @@ function NavSubmenuVisibleProvider({ children, value }) {
);
}
-const menu = getMainMenu();
export function useNavMenuType() {
return useContext(NavMenuTypeContext);
}
-import { useRouter } from "next/router";
function NavMenuTypeProvider({ children }) {
+ const hasActiveJudgementRequest = useHasActiveJudgementRequest();
const router = useRouter();
+ const isAdmin = useIsAdmin();
+ const menu = useMemo(
+ () => getMainMenu({ isAdmin, hasActiveJudgementRequest }),
+ [isAdmin, hasActiveJudgementRequest],
+ );
+
const matchMenu = useMemo(() => {
return (
matchNewMenu(menu, router.pathname) || {
@@ -86,7 +96,8 @@ function NavMenuTypeProvider({ children }) {
menu: null,
}
);
- }, [router.pathname]);
+ }, [router.pathname, menu]);
+
const [navMenuType, setNavMenuType] = useState(matchMenu);
useIsomorphicLayoutEffect(() => {
diff --git a/packages/next-common/context/people/api.js b/packages/next-common/context/people/api.js
index 27e052d4aa..e6c62a9b6e 100644
--- a/packages/next-common/context/people/api.js
+++ b/packages/next-common/context/people/api.js
@@ -1,10 +1,4 @@
-import {
- isCollectivesChain,
- isKusamaChain,
- isPolkadotChain,
- isWestendChain,
- isPaseoChain,
-} from "next-common/utils/chain";
+import { getRelayChain } from "next-common/utils/chain";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { useChain } from "../chain";
import getChainSettings from "next-common/utils/consts/settings";
@@ -50,19 +44,19 @@ export function usePeopleApi() {
}
export function getPeopleChain(chain) {
- if (isPolkadotChain(chain) || isCollectivesChain(chain)) {
+ if (getRelayChain(chain) === Chains.polkadot) {
return Chains.polkadotPeople;
}
- if (isKusamaChain(chain)) {
+ if (getRelayChain(chain) === Chains.kusama) {
return Chains.kusamaPeople;
}
- if (isWestendChain(chain)) {
+ if (getRelayChain(chain) === Chains.westend) {
return Chains.westendPeople;
}
- if (isPaseoChain(chain)) {
+ if (getRelayChain(chain) === Chains.paseo) {
return Chains.paseoPeople;
}
diff --git a/packages/next-common/hooks/people/useSubMyIdentityInfo.js b/packages/next-common/hooks/people/useSubMyIdentityInfo.js
index d57705b12e..db69ca3c1f 100644
--- a/packages/next-common/hooks/people/useSubMyIdentityInfo.js
+++ b/packages/next-common/hooks/people/useSubMyIdentityInfo.js
@@ -1,12 +1,12 @@
import { useEffect, useState } from "react";
-import { useContextApi } from "next-common/context/api";
+import { useIdentityApi } from "../useIdentityApi";
import useRealAddress from "next-common/utils/hooks/useRealAddress";
import { useIdentityOf } from "next-common/hooks/identity/useIdentityOf";
import { fetchIdentityOf } from "../identity/identityFetch";
function useSuperOfIdentityDisplayName(identity) {
const address = useRealAddress();
- const api = useContextApi();
+ const api = useIdentityApi();
const [subDisplay, setSubDisplay] = useState(null);
useEffect(() => {
@@ -34,6 +34,7 @@ function useSuperOfIdentityDisplayName(identity) {
}
const identityResult = await fetchIdentityOf(
+ api,
superOfResult.parentAddress,
).then((res) => res.info);
@@ -61,7 +62,7 @@ function useSuperOfIdentityDisplayName(identity) {
}
export default function useSubMyIdentityInfo() {
- const api = useContextApi();
+ const api = useIdentityApi();
const address = useRealAddress();
const { info, judgements, isLoading } = useIdentityOf(api, address);
const { result: superResult } = useSuperOfIdentityDisplayName(info);
diff --git a/packages/next-common/hooks/scanHeight/useSubScanHeightStream.js b/packages/next-common/hooks/scanHeight/useSubScanHeightStream.js
index e84256c2ee..b72f523f14 100644
--- a/packages/next-common/hooks/scanHeight/useSubScanHeightStream.js
+++ b/packages/next-common/hooks/scanHeight/useSubScanHeightStream.js
@@ -1,6 +1,6 @@
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { sleep } from "next-common/utils";
-import { noop } from "lodash-es";
+import { isNil, noop } from "lodash-es";
export function useSubScanHeightStream({
url,
@@ -9,30 +9,44 @@ export function useSubScanHeightStream({
enabled = true,
}) {
const [reconnect, setReconnect] = useState(0);
+ const callbackRef = useRef(callback);
+
+ useEffect(() => {
+ callbackRef.current = callback;
+ }, [callback]);
useEffect(() => {
if (!enabled) {
return;
}
- let aborted = false;
+ const controller = new AbortController();
+ let reader = null;
+
+ (async () => {
+ try {
+ const response = await fetch(
+ new URL(url, process.env.NEXT_PUBLIC_BACKEND_API_END_POINT),
+ { signal: controller.signal },
+ );
- fetch(new URL(url, process.env.NEXT_PUBLIC_BACKEND_API_END_POINT))
- .then(async (response) => {
if (!response.ok) {
throw new Error(response.statusText);
}
+
const decoder = new TextDecoder();
- const reader = response.body.getReader();
+ let buffer = "";
+ reader = response.body?.getReader?.() ?? null;
if (!reader) {
throw new Error("Reader is null");
}
+
// eslint-disable-next-line no-constant-condition
while (true) {
- if (aborted) {
- reader.cancel();
+ if (controller.signal.aborted) {
break;
}
+
const { value, done } = await Promise.race([
reader.read(),
new Promise((_, reject) =>
@@ -42,28 +56,55 @@ export function useSubScanHeightStream({
),
),
]);
+
if (done) {
throw new Error("Scan height stream closed");
}
+
try {
- const data = JSON.parse(decoder.decode(value));
- const possibleValue = data?.value;
- if (possibleValue) {
- callback(possibleValue);
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split("\n");
+ buffer = lines.pop() ?? "";
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed) {
+ continue;
+ }
+ const data = JSON.parse(trimmed);
+ const possibleValue = data?.value;
+ if (!isNil(possibleValue)) {
+ callbackRef.current(possibleValue);
+ }
}
} catch (e) {
console.error("Error parsing scan height data:", e);
}
}
- })
- .catch(async (e) => {
+ } catch (e) {
+ if (controller.signal.aborted || e?.name === "AbortError") {
+ return;
+ }
+
console.error("Error fetching scan height:", e);
await sleep(5000);
+ if (controller.signal.aborted) {
+ return;
+ }
setReconnect((prev) => prev + 1);
- });
+ } finally {
+ try {
+ await reader?.cancel?.();
+ } catch {
+ // ignore
+ }
+ }
+ })();
return () => {
- aborted = true;
+ controller.abort();
+ // reader.cancel() is async and may reject with AbortError; swallow it.
+ reader?.cancel?.().catch(() => null);
};
- }, [reconnect, url, timeout, callback, enabled]);
+ }, [reconnect, url, timeout, enabled]);
}
diff --git a/packages/next-common/hooks/useAddressVestingData.js b/packages/next-common/hooks/useAddressVestingData.js
index f103972e9a..3186d269f7 100644
--- a/packages/next-common/hooks/useAddressVestingData.js
+++ b/packages/next-common/hooks/useAddressVestingData.js
@@ -19,6 +19,10 @@ export default function useAddressVestingData(address) {
return;
}
+ if (!api.query?.vesting?.vesting) {
+ return;
+ }
+
try {
if (!silent) {
setIsLoading(true);
diff --git a/packages/next-common/hooks/useCountdown.js b/packages/next-common/hooks/useCountdown.js
new file mode 100644
index 0000000000..2deb9d448f
--- /dev/null
+++ b/packages/next-common/hooks/useCountdown.js
@@ -0,0 +1,27 @@
+import { useCallback, useEffect, useState } from "react";
+
+export default function useCountdown() {
+ const [countdown, setCountdown] = useState(0);
+
+ useEffect(() => {
+ let timer;
+ if (countdown > 0) {
+ timer = setTimeout(() => setCountdown((v) => v - 1), 1000);
+ }
+ return () => clearTimeout(timer);
+ }, [countdown]);
+
+ const start = useCallback((seconds) => {
+ setCountdown(seconds);
+ }, []);
+
+ const stop = useCallback(() => {
+ setCountdown(0);
+ }, []);
+
+ return {
+ countdown,
+ start,
+ stop,
+ };
+}
diff --git a/packages/next-common/hooks/useFetchIdentityInfo.js b/packages/next-common/hooks/useFetchIdentityInfo.js
index 45247be4ba..11f6467956 100644
--- a/packages/next-common/hooks/useFetchIdentityInfo.js
+++ b/packages/next-common/hooks/useFetchIdentityInfo.js
@@ -4,7 +4,7 @@ import { backendApi } from "next-common/services/nextApi";
import { useAsync } from "react-use";
import { isPolkadotChain, isKusamaChain } from "next-common/utils/chain";
-function useIdentityApi(address = "") {
+function useIdentityApiUrl(address = "") {
const chain = useChain();
return useMemo(() => {
@@ -22,17 +22,17 @@ function useIdentityApi(address = "") {
}
export default function useFetchIdentityInfo(address = "") {
- const identityApi = useIdentityApi(address);
+ const identityApiUrl = useIdentityApiUrl(address);
const [isLoading, setIsLoading] = useState(true);
const { value } = useAsync(async () => {
setIsLoading(true);
- if (!identityApi) {
+ if (!identityApiUrl) {
return {};
}
try {
- const resp = await backendApi.fetch(identityApi);
+ const resp = await backendApi.fetch(identityApiUrl);
const { subs = [], info } = resp?.result || {};
return {
@@ -44,7 +44,7 @@ export default function useFetchIdentityInfo(address = "") {
} finally {
setIsLoading(false);
}
- }, [identityApi]);
+ }, [identityApiUrl]);
return {
data: value,
diff --git a/packages/next-common/hooks/useIdentityApi.js b/packages/next-common/hooks/useIdentityApi.js
new file mode 100644
index 0000000000..cfa0b111de
--- /dev/null
+++ b/packages/next-common/hooks/useIdentityApi.js
@@ -0,0 +1,31 @@
+import { useEffect, useState } from "react";
+import { useContextApi } from "next-common/context/api";
+import { getPeopleChain } from "next-common/context/people/api";
+import { useChain } from "next-common/context/chain";
+import { getChainApi } from "next-common/utils/getChainApi";
+import getChainSettings from "next-common/utils/consts/settings";
+import { isPeopleChain } from "next-common/utils/chain";
+
+export function useIdentityApi() {
+ const chain = useChain();
+ const defaultApi = useContextApi();
+ const [peopleApi, setPeopleApi] = useState(null);
+ const peopleChain = getPeopleChain(chain);
+
+ useEffect(() => {
+ if (!peopleChain || isPeopleChain(chain)) {
+ return;
+ }
+ const endpointUrls = getChainSettings(peopleChain)?.endpoints?.map(
+ (item) => item.url,
+ );
+ if (endpointUrls?.length > 0) {
+ getChainApi(endpointUrls).then(setPeopleApi);
+ }
+ }, [peopleChain, chain]);
+
+ if (!peopleChain || isPeopleChain(chain)) {
+ return defaultApi;
+ }
+ return peopleApi;
+}
diff --git a/packages/next-common/hooks/useIdentityInfo.js b/packages/next-common/hooks/useIdentityInfo.js
index 883cd70e99..345181c90f 100644
--- a/packages/next-common/hooks/useIdentityInfo.js
+++ b/packages/next-common/hooks/useIdentityInfo.js
@@ -8,7 +8,6 @@ import {
getCachedBountyIdentity,
} from "next-common/services/identity";
import getChainSettings from "next-common/utils/consts/settings";
-import { isPeopleChain } from "next-common/utils/chain";
import { cloneDeep } from "lodash-es";
import { isNil } from "lodash-es";
import useRealAddress from "next-common/utils/hooks/useRealAddress";
@@ -20,8 +19,10 @@ const emptyIdentityInfo = {};
export function useChainAddressIdentityInfo(chain, address, realAddress = "") {
const { identity: identityChain } = getChainSettings(chain);
+ const identityContextData = useIdentityInfoContext();
const { displayName, info: myIdentityInfo = emptyIdentityInfo } =
- useIdentityInfoContext() || {};
+ identityContextData || {};
+ const hasIdentityContext = !isNil(identityContextData);
// Render the identity immediately if it's already in cache
const encodedAddress = encodeAddressToChain(address, identityChain);
@@ -44,7 +45,7 @@ export function useChainAddressIdentityInfo(chain, address, realAddress = "") {
fetchIdentity(identityChain, encodeAddressToChain(address, identityChain))
.then((identity) => {
- if (!isPeopleChain(chain) || !isSameAddress(realAddress, address)) {
+ if (!hasIdentityContext || !isSameAddress(realAddress, address)) {
resolvedIdentity = identity;
setIdentity(identity);
return;
@@ -100,11 +101,20 @@ export function useChainAddressIdentityInfo(chain, address, realAddress = "") {
setIsLoading(false);
});
- }, [address, identityChain, myIdentityInfo, displayName, chain, realAddress]);
+ }, [
+ address,
+ identityChain,
+ hasIdentityContext,
+ myIdentityInfo,
+ displayName,
+ chain,
+ realAddress,
+ ]);
return {
identity,
- hasIdentity: identity && identity?.info?.status !== "NO_ID",
+ hasIdentity:
+ !isNil(identity?.info?.status) && identity.info.status !== "NO_ID",
isLoading,
};
}
diff --git a/packages/next-common/hooks/useMainMenuData.js b/packages/next-common/hooks/useMainMenuData.js
index e76c4b9c32..9530747c66 100644
--- a/packages/next-common/hooks/useMainMenuData.js
+++ b/packages/next-common/hooks/useMainMenuData.js
@@ -2,11 +2,17 @@ import { usePageProps } from "next-common/context/page";
import { getMainMenu } from "next-common/utils/consts/menu";
import { useUser } from "next-common/context/user";
import { useMemo } from "react";
+import useIsAdmin from "./useIsAdmin";
+import useHasActiveJudgementRequest from "next-common/components/people/hooks/useHasActiveJudgementRequest";
export default function useMainMenuData() {
const user = useUser();
const { tracks, fellowshipTracks, summary, detail, ambassadorTracks } =
usePageProps();
+ const hasActiveJudgementRequest = useHasActiveJudgementRequest();
+
+ const isAdmin = useIsAdmin();
+
return useMemo(() => {
return getMainMenu({
tracks,
@@ -14,6 +20,8 @@ export default function useMainMenuData() {
ambassadorTracks,
summary,
currentTrackId: detail?.track,
+ isAdmin,
+ hasActiveJudgementRequest,
}).filter((item) => {
if (item.value === "account") {
// not connect wallet
@@ -28,5 +36,7 @@ export default function useMainMenuData() {
summary,
tracks,
user?.address,
+ isAdmin,
+ hasActiveJudgementRequest,
]);
}
diff --git a/packages/next-common/utils/chain.js b/packages/next-common/utils/chain.js
index 30613d94cb..9e2ed477de 100644
--- a/packages/next-common/utils/chain.js
+++ b/packages/next-common/utils/chain.js
@@ -154,24 +154,24 @@ export function getRelayChain(chain) {
return chain;
} else if (isPolkadotAssetHubChain(chain)) {
return Chains.polkadot;
- } else if (isKusamaAssetHubChain(chain)) {
- return Chains.kusama;
- } else if (isWestendAssetHubChain(chain)) {
- return Chains.westend;
- } else if (isPaseoAssetHubChain(chain)) {
- return Chains.paseo;
} else if (isCollectivesChain(chain)) {
return Chains.polkadot;
} else if (isPolkadotPeopleChain(chain)) {
return Chains.polkadot;
+ } else if (isHyperBridgeChain(chain)) {
+ return Chains.polkadot;
+ } else if (isKusamaAssetHubChain(chain)) {
+ return Chains.kusama;
} else if (isKusamaPeopleChain(chain)) {
return Chains.kusama;
- } else if (isPaseoPeopleChain(chain)) {
- return Chains.paseo;
+ } else if (isWestendAssetHubChain(chain)) {
+ return Chains.westend;
} else if (isWestendPeopleChain(chain)) {
return Chains.westend;
- } else if (isHyperBridgeChain(chain)) {
- return Chains.polkadot;
+ } else if (isPaseoAssetHubChain(chain)) {
+ return Chains.paseo;
+ } else if (isPaseoPeopleChain(chain)) {
+ return Chains.paseo;
}
throw new Error("Unsupported relay chain");
diff --git a/packages/next-common/utils/constants.js b/packages/next-common/utils/constants.js
index d6f68ac933..1c8a205c29 100644
--- a/packages/next-common/utils/constants.js
+++ b/packages/next-common/utils/constants.js
@@ -133,6 +133,7 @@ export const CACHE_KEY = {
assetsPromptVisible: "assets-management-prompt-visible",
multisigManagementPromptVisible: "multisig-management-prompt-visible",
requestJudgementPrompt: "request-judgement-prompt",
+ navigateToJudgementPagePrompt: "navigate-to-judgement-page-prompt",
walletConnectSession: "walletconnect-session",
totalRequestingAssets: "total-requesting-assets",
treasurySpendsPendingNotice: "treasury-spends-pending-notice",
diff --git a/packages/next-common/utils/consts/menu/index.js b/packages/next-common/utils/consts/menu/index.js
index da9893a3e3..aee3a417be 100644
--- a/packages/next-common/utils/consts/menu/index.js
+++ b/packages/next-common/utils/consts/menu/index.js
@@ -17,7 +17,7 @@ import { getCommunityTreasuryMenu } from "./communityTreasury";
import getChainSettings from "../settings";
import getArchivedMenu from "./archived";
import { coretimeMenu } from "./coretime";
-import { peopleMenu } from "./people";
+import { getPeopleMenu } from "./people";
import { stakingMenu } from "./staking";
import whitelist from "./whitelist";
import Data from "./data";
@@ -36,6 +36,8 @@ export function getHomeMenu({
summary = {},
ambassadorTracks = [],
currentTrackId,
+ isAdmin = false,
+ hasActiveJudgementRequest = false,
} = {}) {
const { modules, hasMultisig = false } = getChainSettings(CHAIN);
@@ -56,7 +58,7 @@ export function getHomeMenu({
modules?.alliance && getAllianceMenu(summary),
modules?.communityCouncil && getCommunityCouncilMenu(summary),
modules?.staking && stakingMenu,
- modules?.people && peopleMenu,
+ modules?.people && getPeopleMenu({ isAdmin, hasActiveJudgementRequest }),
modules?.coretime && coretimeMenu,
getAdvancedMenu(
[
@@ -90,6 +92,8 @@ export function getMainMenu({
fellowshipTracks = [],
ambassadorTracks = [],
currentTrackId,
+ isAdmin = false,
+ hasActiveJudgementRequest = false,
} = {}) {
const { hotMenu = {} } = getChainSettings(CHAIN);
@@ -104,6 +108,8 @@ export function getMainMenu({
fellowshipTracks,
ambassadorTracks,
currentTrackId,
+ isAdmin,
+ hasActiveJudgementRequest,
});
const activeModulesMenu = [];
diff --git a/packages/next-common/utils/consts/menu/people.jsx b/packages/next-common/utils/consts/menu/people.jsx
index 21d97d1150..331cc2748d 100644
--- a/packages/next-common/utils/consts/menu/people.jsx
+++ b/packages/next-common/utils/consts/menu/people.jsx
@@ -5,43 +5,65 @@ import {
MenuData,
MenuRegistrars,
InfoUsers,
+ MenuJudgements,
+ MenuIdentity,
} from "@osn/icons/subsquare";
import { NAV_MENU_TYPE } from "next-common/utils/constants";
-export const peopleMenu = {
- name: "People",
- value: "people",
- pathname: "/people",
- icon: ,
- extra: ,
- type: NAV_MENU_TYPE.subspace,
- items: [
- {
- name: "Overview",
- value: "overview",
- pathname: "/people",
- icon: ,
- },
- {
- name: "Identities",
- value: "identities",
- pathname: "/people/identities",
- icon: ,
- },
- {
- name: "Registrars",
- value: "registrars",
- pathname: "/people/registrars",
- icon: ,
- },
- {
- type: "divider",
- },
- {
- name: "Usernames",
- value: "usernames",
- pathname: "/people/usernames",
- icon: ,
- },
- ],
-};
+export function getPeopleMenu({ isAdmin, hasActiveJudgementRequest } = {}) {
+ return {
+ name: "People",
+ value: "people",
+ pathname: "/people",
+ icon: ,
+ extra: ,
+ type: NAV_MENU_TYPE.subspace,
+ items: [
+ {
+ name: "Overview",
+ value: "overview",
+ pathname: "/people",
+ icon: ,
+ },
+ {
+ name: "Identities",
+ value: "identities",
+ pathname: "/people/identities",
+ icon: ,
+ },
+ {
+ name: "Registrars",
+ value: "registrars",
+ pathname: "/people/registrars",
+ icon: ,
+ },
+ {
+ name: "Verifications",
+ value: "verifications",
+ pathname: "/people/verifications",
+ icon: ,
+ extraMatchNavMenuActivePathnames: [
+ "/people/verifications/auth/discord",
+ "/people/verifications/auth/twitter",
+ "/people/verifications/auth/github",
+ ],
+ visible: hasActiveJudgementRequest,
+ },
+ isAdmin && {
+ name: "Judgement Requests",
+ value: "judgement-requests",
+ pathname: "/people/judgement-requests",
+ icon: ,
+ },
+ {
+ type: "divider",
+ },
+ {
+ name: "Usernames",
+ value: "usernames",
+ pathname: "/people/usernames",
+ icon: ,
+ },
+ ].filter(Boolean),
+ };
+}
diff --git a/packages/next-common/utils/url.js b/packages/next-common/utils/url.js
index c22b1807a4..65e45a0981 100644
--- a/packages/next-common/utils/url.js
+++ b/packages/next-common/utils/url.js
@@ -23,3 +23,7 @@ export function objectToQueryString(obj) {
});
return searchParams.toString();
}
+
+export function trimEndSlash(url) {
+ return url.replace(/\/+$/, "");
+}
diff --git a/packages/next/pages/people/identities/index.jsx b/packages/next/pages/people/identities/index.jsx
index 09c3290a8e..c2d5cb140d 100644
--- a/packages/next/pages/people/identities/index.jsx
+++ b/packages/next/pages/people/identities/index.jsx
@@ -1,13 +1,8 @@
-import { withCommonProps } from "next-common/lib";
-import { createStore } from "next-common/store";
-import ChainProvider from "next-common/context/chain";
-import ApiProvider from "next-common/context/api";
-import { Provider } from "react-redux";
-import { commonReducers } from "next-common/store/reducers";
import { CHAIN } from "next-common/utils/constants";
import getChainSettings from "next-common/utils/consts/settings";
-import RelayInfoProvider from "next-common/context/relayInfo";
import dynamicClientOnly from "next-common/lib/dynamic/clientOnly";
+import { PeopleGlobalProvider } from "..";
+import { getPeopleServerSideProps } from "next-common/components/people/common/getServerSideProps";
const isPeopleSupported = !!getChainSettings(CHAIN).modules?.people;
@@ -15,32 +10,11 @@ const PeopleIdentitiesPageImpl = dynamicClientOnly(() =>
import("next-common/components/people/identities"),
);
-let chain;
-let store;
-
-if (isPeopleSupported) {
- chain = `${CHAIN}-people`;
- store = createStore({
- chain,
- reducer: commonReducers,
- });
-}
-
export default function PeopleIdentitiesPage() {
- if (!isPeopleSupported) {
- return null;
- }
-
return (
-
-
-
-
-
-
-
-
-
+
+
+
);
}
@@ -51,9 +25,5 @@ export const getServerSideProps = async (ctx) => {
};
}
- return withCommonProps(async () => {
- return {
- props: {},
- };
- })(ctx);
+ return await getPeopleServerSideProps(ctx);
};
diff --git a/packages/next/pages/people/index.jsx b/packages/next/pages/people/index.jsx
index 2846bb1072..5bc47aa7fb 100644
--- a/packages/next/pages/people/index.jsx
+++ b/packages/next/pages/people/index.jsx
@@ -1,7 +1,7 @@
import { CHAIN } from "next-common/utils/constants";
import { createStore } from "next-common/store";
import { commonReducers } from "next-common/store/reducers";
-import { withCommonProps } from "next-common/lib";
+import { getPeopleServerSideProps } from "next-common/components/people/common/getServerSideProps";
import RelayInfoProvider from "next-common/context/relayInfo";
import ApiProvider from "next-common/context/api";
import ChainProvider from "next-common/context/chain";
@@ -57,9 +57,5 @@ export const getServerSideProps = async (ctx) => {
};
}
- return withCommonProps(async () => {
- return {
- props: {},
- };
- })(ctx);
+ return await getPeopleServerSideProps(ctx);
};
diff --git a/packages/next/pages/people/judgement-requests/index.jsx b/packages/next/pages/people/judgement-requests/index.jsx
new file mode 100644
index 0000000000..e901549ecd
--- /dev/null
+++ b/packages/next/pages/people/judgement-requests/index.jsx
@@ -0,0 +1,48 @@
+import { getPeopleServerSideProps } from "next-common/components/people/common/getServerSideProps";
+import { CHAIN } from "next-common/utils/constants";
+import getChainSettings from "next-common/utils/consts/settings";
+import dynamicClientOnly from "next-common/lib/dynamic/clientOnly";
+import { PeopleGlobalProvider } from "..";
+import useIsAdmin from "next-common/hooks/useIsAdmin";
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+
+const isPeopleSupported = !!getChainSettings(CHAIN).modules?.people;
+
+const JudgementRequestsPageImpl = dynamicClientOnly(() =>
+ import("next-common/components/people/judgementRequests"),
+);
+
+export default function PeopleJudgementRequestsPage() {
+ const router = useRouter();
+ const isAdmin = useIsAdmin();
+
+ useEffect(() => {
+ if (!isAdmin) {
+ const timer = setTimeout(() => {
+ router.replace("/people");
+ }, 3000);
+ return () => clearTimeout(timer);
+ }
+ }, [isAdmin, router]);
+
+ if (!isAdmin) {
+ return Only admins can access this page. redirecting...
;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export const getServerSideProps = async (ctx) => {
+ if (!isPeopleSupported) {
+ return {
+ notFound: true,
+ };
+ }
+
+ return await getPeopleServerSideProps(ctx);
+};
diff --git a/packages/next/pages/people/registrars/index.jsx b/packages/next/pages/people/registrars/index.jsx
index b9a41ab888..0709272048 100644
--- a/packages/next/pages/people/registrars/index.jsx
+++ b/packages/next/pages/people/registrars/index.jsx
@@ -1,13 +1,8 @@
-import { withCommonProps } from "next-common/lib";
-import { createStore } from "next-common/store";
-import ChainProvider from "next-common/context/chain";
-import ApiProvider from "next-common/context/api";
-import { Provider } from "react-redux";
-import { commonReducers } from "next-common/store/reducers";
+import { getPeopleServerSideProps } from "next-common/components/people/common/getServerSideProps";
import { CHAIN } from "next-common/utils/constants";
import getChainSettings from "next-common/utils/consts/settings";
-import RelayInfoProvider from "next-common/context/relayInfo";
import dynamicClientOnly from "next-common/lib/dynamic/clientOnly";
+import { PeopleGlobalProvider } from "..";
const isPeopleSupported = !!getChainSettings(CHAIN).modules?.people;
@@ -15,32 +10,11 @@ const PeopleRegistrarsPageImpl = dynamicClientOnly(() =>
import("next-common/components/people/registrars"),
);
-let chain;
-let store;
-
-if (isPeopleSupported) {
- chain = `${CHAIN}-people`;
- store = createStore({
- chain,
- reducer: commonReducers,
- });
-}
-
export default function PeopleRegistrarsPage() {
- if (!isPeopleSupported) {
- return null;
- }
-
return (
-
-
-
-
-
-
-
-
-
+
+
+
);
}
@@ -51,9 +25,5 @@ export const getServerSideProps = async (ctx) => {
};
}
- return withCommonProps(async () => {
- return {
- props: {},
- };
- })(ctx);
+ return await getPeopleServerSideProps(ctx);
};
diff --git a/packages/next/pages/people/usernames/index.jsx b/packages/next/pages/people/usernames/index.jsx
index bccd0685f8..6fb5a377b9 100644
--- a/packages/next/pages/people/usernames/index.jsx
+++ b/packages/next/pages/people/usernames/index.jsx
@@ -1,6 +1,10 @@
+import { getPeopleServerSideProps } from "next-common/components/people/common/getServerSideProps";
import dynamicClientOnly from "next-common/lib/dynamic/clientOnly";
import { PeopleGlobalProvider } from "..";
-export { getServerSideProps } from "../index";
+import getChainSettings from "next-common/utils/consts/settings";
+import { CHAIN } from "next-common/utils/constants";
+
+const isPeopleSupported = !!getChainSettings(CHAIN).modules?.people;
const PeopleUsernamesPageImpl = dynamicClientOnly(() =>
import("next-common/components/people/usernames"),
@@ -13,3 +17,13 @@ export default function PeopleUsernamesPage() {
);
}
+
+export const getServerSideProps = async (ctx) => {
+ if (!isPeopleSupported) {
+ return {
+ notFound: true,
+ };
+ }
+
+ return await getPeopleServerSideProps(ctx);
+};
diff --git a/packages/next/pages/people/verifications/auth/discord.jsx b/packages/next/pages/people/verifications/auth/discord.jsx
new file mode 100644
index 0000000000..68b34b6d30
--- /dev/null
+++ b/packages/next/pages/people/verifications/auth/discord.jsx
@@ -0,0 +1,48 @@
+import { PeopleGlobalProvider } from "../../index";
+import { CHAIN } from "next-common/utils/constants";
+import { withCommonProps } from "next-common/lib";
+import getChainSettings from "next-common/utils/consts/settings";
+import PeopleJudgementAuthCallbackPage from "next-common/components/pages/people/judgement/authCallback";
+import { PeopleSocialType } from "next-common/components/people/judgement/consts";
+
+const isPeopleSupported = !!getChainSettings(CHAIN).modules?.people;
+
+export default function Page({ code, state, error, errorDescription }) {
+ return (
+
+
+
+ );
+}
+
+export const getServerSideProps = async (ctx) => {
+ if (!isPeopleSupported) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const code = ctx.query.code || "";
+ const state = ctx.query.state || "";
+ const error = ctx.query.error || "";
+ const errorDescription = ctx.query.error_description || "";
+
+ return withCommonProps(async () => {
+ return {
+ props: {
+ code,
+ state,
+ error,
+ errorDescription,
+ },
+ };
+ })(ctx);
+};
diff --git a/packages/next/pages/people/verifications/auth/github.jsx b/packages/next/pages/people/verifications/auth/github.jsx
new file mode 100644
index 0000000000..7016630728
--- /dev/null
+++ b/packages/next/pages/people/verifications/auth/github.jsx
@@ -0,0 +1,48 @@
+import { PeopleGlobalProvider } from "../../index";
+import { CHAIN } from "next-common/utils/constants";
+import { withCommonProps } from "next-common/lib";
+import getChainSettings from "next-common/utils/consts/settings";
+import PeopleJudgementAuthCallbackPage from "next-common/components/pages/people/judgement/authCallback";
+import { PeopleSocialType } from "next-common/components/people/judgement/consts";
+
+const isPeopleSupported = !!getChainSettings(CHAIN).modules?.people;
+
+export default function Page({ code, state, error, errorDescription }) {
+ return (
+
+
+
+ );
+}
+
+export const getServerSideProps = async (ctx) => {
+ if (!isPeopleSupported) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const code = ctx.query.code || "";
+ const state = ctx.query.state || "";
+ const error = ctx.query.error || "";
+ const errorDescription = ctx.query.error_description || "";
+
+ return withCommonProps(async () => {
+ return {
+ props: {
+ code,
+ state,
+ error,
+ errorDescription,
+ },
+ };
+ })(ctx);
+};
diff --git a/packages/next/pages/people/verifications/auth/twitter.jsx b/packages/next/pages/people/verifications/auth/twitter.jsx
new file mode 100644
index 0000000000..ff3199f41c
--- /dev/null
+++ b/packages/next/pages/people/verifications/auth/twitter.jsx
@@ -0,0 +1,48 @@
+import { PeopleGlobalProvider } from "../../index";
+import { CHAIN } from "next-common/utils/constants";
+import { withCommonProps } from "next-common/lib";
+import getChainSettings from "next-common/utils/consts/settings";
+import PeopleJudgementAuthCallbackPage from "next-common/components/pages/people/judgement/authCallback";
+import { PeopleSocialType } from "next-common/components/people/judgement/consts";
+
+const isPeopleSupported = !!getChainSettings(CHAIN).modules?.people;
+
+export default function Page({ code, state, error, errorDescription }) {
+ return (
+
+
+
+ );
+}
+
+export const getServerSideProps = async (ctx) => {
+ if (!isPeopleSupported) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const code = ctx.query.code || "";
+ const state = ctx.query.state || "";
+ const error = ctx.query.error || "";
+ const errorDescription = ctx.query.error_description || "";
+
+ return withCommonProps(async () => {
+ return {
+ props: {
+ code,
+ state,
+ error,
+ errorDescription,
+ },
+ };
+ })(ctx);
+};
diff --git a/packages/next/pages/people/verifications/index.jsx b/packages/next/pages/people/verifications/index.jsx
new file mode 100644
index 0000000000..5525aea32e
--- /dev/null
+++ b/packages/next/pages/people/verifications/index.jsx
@@ -0,0 +1,48 @@
+import { getPeopleServerSideProps } from "next-common/components/people/common/getServerSideProps";
+import { CHAIN } from "next-common/utils/constants";
+import dynamicClientOnly from "next-common/lib/dynamic/clientOnly";
+import getChainSettings from "next-common/utils/consts/settings";
+import { PeopleGlobalProvider } from "..";
+import useRealAddress from "next-common/utils/hooks/useRealAddress";
+import { useEffect } from "react";
+import { useRouter } from "next/router";
+
+const isPeopleSupported = !!getChainSettings(CHAIN).modules?.people;
+
+const PeopleOverviewPageImpl = dynamicClientOnly(() =>
+ import("next-common/components/people/judgement"),
+);
+
+export default function VerificationPage() {
+ const router = useRouter();
+ const realAddress = useRealAddress();
+
+ useEffect(() => {
+ if (!realAddress) {
+ const timer = setTimeout(() => {
+ router.replace("/people");
+ }, 3000);
+ return () => clearTimeout(timer);
+ }
+ }, [realAddress, router]);
+
+ if (!realAddress) {
+ return Only connected users can access this page. redirecting...
;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export const getServerSideProps = async (ctx) => {
+ if (!isPeopleSupported) {
+ return {
+ notFound: true,
+ };
+ }
+
+ return await getPeopleServerSideProps(ctx);
+};