diff --git a/packages/next-common/components/nav/menu/item/index.jsx b/packages/next-common/components/nav/menu/item/index.jsx index cde537358a..660ff51571 100644 --- a/packages/next-common/components/nav/menu/item/index.jsx +++ b/packages/next-common/components/nav/menu/item/index.jsx @@ -5,7 +5,7 @@ import NavMenuDivider from "../../divider"; import { useNavSubmenuVisible } from "next-common/context/nav"; export default function NavMenuItem({ collapsed, ...menu } = {}) { - const { type, items } = menu || {}; + const { type, items, visible = true } = menu || {}; const router = useRouter(); const routePathname = router.asPath.split("?")[0]; const [navSubmenuVisible, setNavSubmenuVisible] = useNavSubmenuVisible(); @@ -14,6 +14,10 @@ export default function NavMenuItem({ collapsed, ...menu } = {}) { return ; } + if (visible === false) { + return null; + } + if (items?.length && !menu?.hideItemsOnMenu) { return ( 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); +};