diff --git a/package.json b/package.json index 7f985567..5642ffd4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "npx playwright test" }, "dependencies": { - "alem": "1.1.2" + "alem": "1.1.3" }, "devDependencies": { "@playwright/test": "^1.38.1", diff --git a/src/assets/svgs/SuccessfulIcon.tsx b/src/assets/svgs/SuccessfulIcon.tsx new file mode 100644 index 00000000..ce3d93b1 --- /dev/null +++ b/src/assets/svgs/SuccessfulIcon.tsx @@ -0,0 +1,9 @@ +const SuccessfulIcon = (props: React.SVGProps) => ( + + + +); +export default SuccessfulIcon; diff --git a/src/components/Cart/BreakdownSummary/BreakdownSummary.tsx b/src/components/Cart/BreakdownSummary/BreakdownSummary.tsx index 16d0a9de..8f539314 100644 --- a/src/components/Cart/BreakdownSummary/BreakdownSummary.tsx +++ b/src/components/Cart/BreakdownSummary/BreakdownSummary.tsx @@ -1,7 +1,6 @@ import { State, props, state } from "alem"; import DonateSDK from "@app/SDK/donate"; import NearIcon from "@app/assets/svgs/near-icon"; -import constants from "@app/constants"; import basisPointsToPercent from "@app/utils/basisPointsToPercent"; import { BreakdownAmount, @@ -18,11 +17,10 @@ const BreakdownSummary = ({ referrerId, totalAmount, bypassProtocolFee, - recipientId, potRferralFeeBasisPoints, ftIcon, bypassChefFee, - chef, + label, chefFeeBasisPoints, }: any) => { const donationContractConfig = DonateSDK.getConfig(); @@ -36,6 +34,7 @@ const BreakdownSummary = ({ const { protocol_fee_basis_points } = donationContractConfig; const protocolFeeBasisPoints = props.protocolFeeBasisPoints ?? protocol_fee_basis_points; + const referralFeeBasisPoints = potRferralFeeBasisPoints || props.referralFeeBasisPoints; const TOTAL_BASIS_POINTS = 10_000; @@ -81,7 +80,7 @@ const BreakdownSummary = ({ show: true, }, { - label: "Project allocation", + label: `${label ?? "Project"} allocation`, percentage: projectAllocationPercent, amount: projectAllocationAmount, show: true, diff --git a/src/components/ToastNotification/getToastContainer.tsx b/src/components/ToastNotification/getToastContainer.tsx new file mode 100644 index 00000000..166ba2bf --- /dev/null +++ b/src/components/ToastNotification/getToastContainer.tsx @@ -0,0 +1,23 @@ +import { useEffect } from "alem"; +import SuccessfulIcon from "@app/assets/svgs/SuccessfulIcon"; +import ToastProvider, { ToastProps } from "@app/contexts/ToastProvider"; +import { useToastNotification } from "@app/hooks/useToast"; +import { Container, Description, Header } from "./styles"; + +const ToastContainer = ({ toastContent }: { toastContent: ToastProps }) => { + // ToastProvider(); + + // const { toastContent } = useToastNotification(); + + return ( + +
+ + {toastContent.title} +
+ {toastContent.description} +
+ ); +}; + +export default ToastContainer; diff --git a/src/components/ToastNotification/styles.ts b/src/components/ToastNotification/styles.ts new file mode 100644 index 00000000..64bda572 --- /dev/null +++ b/src/components/ToastNotification/styles.ts @@ -0,0 +1,42 @@ +import styled from "styled-components"; + +export const Container = styled.div` + position: fixed; + right: -310px; + top: 10%; + opacity: 0; + display: flex; + flex-direction: column; + max-width: 300px; + padding: 1rem; + background: #fff; + box-shadow: 0px 0px 0px 1px rgba(5, 5, 5, 0.08), 0px 8px 8px -4px rgba(15, 15, 15, 0.15), + 0px 4px 15px -2px rgba(5, 5, 5, 0.08); + border-radius: 6px; + gap: 6px; + font-size: 14px; + transition: all 300ms cubic-bezier(0.23, 1, 0.32, 1); + &.active { + right: 10px; + opacity: 1; + } +`; + +export const Header = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + div { + line-height: 142%; + font-weight: 600; + } + svg { + width: 18px; + height: 18px; + } +`; + +export const Description = styled.div` + padding-left: 1.5rem; + color: #656565; +`; diff --git a/src/components/mob.near/ProfileImage.tsx b/src/components/mob.near/ProfileImage.tsx index daeba109..0d5e16f1 100644 --- a/src/components/mob.near/ProfileImage.tsx +++ b/src/components/mob.near/ProfileImage.tsx @@ -60,8 +60,8 @@ const ProfileImage = (profileImgProps: Props) => { accountId, }); } - const fallbackUrl = props.fallbackUrl; - + const fallbackUrl = + props.fallbackUrl ?? "https://ipfs.near.social/ipfs/bafkreiccpup6f2kihv7bhlkfi4omttbjpawnsns667gti7jbhqvdnj4vsm"; const imageProps = { image, alt: title, diff --git a/src/contexts/ToastProvider.ts b/src/contexts/ToastProvider.ts new file mode 100644 index 00000000..b6a54a21 --- /dev/null +++ b/src/contexts/ToastProvider.ts @@ -0,0 +1,41 @@ +import { createContext } from "alem"; + +export interface ToastProps { + title: string; + description: string; + type?: "success" | "error" | "warning"; +} + +export interface ToastContextProps { + toast: (newValue: ToastProps) => void; + toastContent: ToastProps; +} + +const ToastProvider = () => { + // Create a provider using a reference key + const { setDefaultData, updateData, getSelf } = createContext("toast-notification"); + + const EMPTY_TOAST = { + title: "", + description: "", + }; + + setDefaultData({ + toastContent: EMPTY_TOAST, + toast: (toastContent: ToastProps) => { + updateData({ + toastContent, + }); + setTimeout(() => { + updateData({ + toastContent: EMPTY_TOAST, + }); + // Wait 7sec before clearing the notification + }, 7000); + }, + }); + + return getSelf(); +}; + +export default ToastProvider; diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts new file mode 100644 index 00000000..f79bda0c --- /dev/null +++ b/src/hooks/useToast.ts @@ -0,0 +1,4 @@ +import { useContext } from "alem"; +import { ToastContextProps } from "@app/contexts/ToastProvider"; + +export const useToastNotification = () => useContext("toast-notification"); diff --git a/src/modals/ModalOverlay.tsx b/src/modals/ModalOverlay.tsx index be9494eb..48859eaa 100644 --- a/src/modals/ModalOverlay.tsx +++ b/src/modals/ModalOverlay.tsx @@ -3,8 +3,8 @@ import styled from "styled-components"; type Props = { children: JSX.Element | JSX.Element[]; onOverlayClick?: (event: any) => void; - contentStyle?: any; - overlayStyle?: any; + contentStyle?: React.CSSProperties; + overlayStyle?: React.CSSProperties; }; const ModalOverlay = ({ children, onOverlayClick, contentStyle }: Props) => { diff --git a/src/modals/ModalSuccess/ModalSuccess.tsx b/src/modals/ModalSuccess/ModalSuccess.tsx index 2d6464d2..c284c79f 100644 --- a/src/modals/ModalSuccess/ModalSuccess.tsx +++ b/src/modals/ModalSuccess/ModalSuccess.tsx @@ -163,17 +163,20 @@ const ModalSuccess = () => { }, })); }) - .catch((err) => console.log(err)); + .catch((err) => { + console.log(err); + return ""; + }); } else { - onClose(); + return ""; } } else { - onClose(); + return ""; } }) .catch((err) => { console.log(err); - onClose(); + return ""; }); } } @@ -219,7 +222,7 @@ const ModalSuccess = () => { const needsToVerify = isUserHumanVerified === false; - return ( + return successfulDonation || successfulApplication ? ( <> {successfulApplication ? ( @@ -322,9 +325,13 @@ const ModalSuccess = () => { /> {needsToVerify && !successfulDonationVals[0]?.recipient_id && } - ) : null} + ) : ( + "" + )} + ) : ( + "" ); }; diff --git a/src/pages/CreateProject/utils/helpers.ts b/src/pages/CreateProject/utils/helpers.ts index 12971d11..c1d6a3cf 100644 --- a/src/pages/CreateProject/utils/helpers.ts +++ b/src/pages/CreateProject/utils/helpers.ts @@ -65,7 +65,7 @@ export const projectDisabled = () => export function extractRepoPath(url: string) { if (url) { // Define a regular expression pattern to match GitHub repository URLs - const pattern = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([^\/]+\/[^\/]+(?:\/.*)?)\/?$/; + const pattern = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([^\/]+(?:\/[^\/]+)?)\/?$/; // Execute the regular expression on the URL const match = url.match(pattern); // If a match is found, return the extracted repository path; otherwise, return null diff --git a/src/pages/Pot/NavPages/Applications/Applications.tsx b/src/pages/Pot/NavPages/Applications/Applications.tsx index 3eeb72ac..11985433 100644 --- a/src/pages/Pot/NavPages/Applications/Applications.tsx +++ b/src/pages/Pot/NavPages/Applications/Applications.tsx @@ -1,11 +1,13 @@ -import { Social, State, context, state, useParams, Tooltip, OverlayTrigger } from "alem"; +import { Social, State, context, state, useParams, Tooltip, OverlayTrigger, useEffect } from "alem"; import PotSDK from "@app/SDK/pot"; import Button from "@app/components/Button"; import Dropdown from "@app/components/Inputs/Dropdown/Dropdown"; +import ToastContainer from "@app/components/ToastNotification/getToastContainer"; import ProfileImage from "@app/components/mob.near/ProfileImage"; import { PotDetail } from "@app/types"; import _address from "@app/utils/_address"; import daysAgo from "@app/utils/daysAgo"; +import getTransactionsFromHashes from "@app/utils/getTransactionsFromHashes"; import hrefWithParams from "@app/utils/hrefWithParams"; import ApplicationReviewModal from "../../components/ApplicationReviewModal/ApplicationReviewModal"; import APPLICATIONS_FILTERS_TAGS from "./APPLICATIONS_FILTERS_TAGS"; @@ -21,7 +23,8 @@ import { } from "./styles"; const Applications = ({ potDetail }: { potDetail: PotDetail }) => { - const { potId } = useParams(); + const accountId = context.accountId; + const { potId, transactionHashes } = useParams(); State.init({ newStatus: "", @@ -30,9 +33,13 @@ const Applications = ({ potDetail }: { potDetail: PotDetail }) => { allApplications: null, filteredApplications: [], filterVal: "ALL", + toastContent: { + title: "", + description: "", + }, }); - const { newStatus, projectId, searchTerm, allApplications, filteredApplications, filterVal } = state; + const { newStatus, projectId, searchTerm, allApplications, filteredApplications, filterVal, toastContent } = state; const applications = PotSDK.getApplications(potId); @@ -56,8 +63,41 @@ const Applications = ({ potDetail }: { potDetail: PotDetail }) => { const { owner, admins, chef } = potDetail; - const isChefOrGreater = - context.accountId === chef || admins.includes(context.accountId || "") || context.accountId === owner; + const toast = (newStatus: string) => { + State.update({ + toastContent: { + title: "Updated Successfully!", + description: `Application status has been successfully updated to ${newStatus}.`, + }, + }); + setTimeout(() => { + State.update({ + toastContent: { + title: "", + description: "", + }, + }); + }, 7000); + }; + + // Handle update application status for web wallet + useEffect(() => { + if (accountId && transactionHashes) { + getTransactionsFromHashes(transactionHashes, accountId).then((trxs) => { + const transaction = trxs[0].body.result.transaction; + + const methodName = transaction.actions[0].FunctionCall.method_name; + const successVal = trxs[0].body.result.status?.SuccessValue; + const result = JSON.parse(Buffer.from(successVal, "base64").toString("utf-8")); + + if (methodName === "chef_set_application_status" && result) { + toast(result.status); + } + }); + } + }, []); + + const isChefOrGreater = accountId === chef || admins.includes(accountId || "") || accountId === owner; const handleApproveApplication = (projectId: string) => { State.update({ newStatus: "Approved", projectId }); @@ -197,7 +237,7 @@ const Applications = ({ potDetail }: { potDetail: PotDetail }) => {
- + {profile?.name &&
{_address(profile?.name, 10)}
} {project_id}}> @@ -270,7 +310,10 @@ const Applications = ({ potDetail }: { potDetail: PotDetail }) => {
No applications to display
)} - {projectId && } + {projectId && ( + + )} + ); }; diff --git a/src/pages/Pot/NavPages/ConfigForm/ConfigForm.tsx b/src/pages/Pot/NavPages/ConfigForm/ConfigForm.tsx index a8466161..1d69262d 100644 --- a/src/pages/Pot/NavPages/ConfigForm/ConfigForm.tsx +++ b/src/pages/Pot/NavPages/ConfigForm/ConfigForm.tsx @@ -1,5 +1,6 @@ -import { Big, Near, State, context, state, useMemo, useParams } from "alem"; +import { Big, Near, context, useEffect, useMemo, useParams, useState } from "alem"; import ListsSDK from "@app/SDK/lists"; +import PotSDK from "@app/SDK/pot"; import PotFactorySDK from "@app/SDK/potfactory"; import AccountsList from "@app/components/AccountsList/AccountsList"; import Button from "@app/components/Button"; @@ -8,8 +9,11 @@ import DateInput from "@app/components/Inputs/Date/Date"; import Text from "@app/components/Inputs/Text/Text"; import TextArea from "@app/components/Inputs/TextArea/TextArea"; import ModalMultiAccount from "@app/components/ModalMultiAccount/ModalMultiAccount"; +import ToastContainer from "@app/components/ToastNotification/getToastContainer"; import constants from "@app/constants"; import { PotDetail } from "@app/types"; +import deepEqual from "@app/utils/deepEqual"; +import getTransactionsFromHashes from "@app/utils/getTransactionsFromHashes"; import validateNearAddress from "@app/utils/validateNearAddress"; import { CheckboxWrapper, @@ -24,7 +28,8 @@ import { } from "./styles"; const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any }) => { - const { potId } = useParams(); + const { potId, transactionHashes } = useParams(); + const { NADABOT_HUMAN_METHOD, ONE_TGAS, @@ -73,7 +78,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } // console.log("potDetail: ", potDetail); - State.init({ + const inital_state = { owner: isUpdate ? potDetail.owner : context.accountId, ownerError: "", admin: "", @@ -118,12 +123,23 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } usePotlockRegistry: isUpdate ? potDetail.registry_provider == DEFAULT_REGISTRY_PROVIDER : true, latestSourceCodeCommitHash: "", deploymentSuccess: false, - }); + toastContent: { + title: "", + description: "", + }, + }; + + const [state, setState] = useState(inital_state); + const updateState = (updatedState: any) => + setState({ + ...inital_state, + ...updatedState, + }); if (!isUpdate && !state.latestSourceCodeCommitHash) { const res: any = fetch("https://api.github.com/repos/PotLock/core/commits"); if (res.ok && res.body.length > 0) { - State.update({ + updateState({ latestSourceCodeCommitHash: res.body[0].sha, }); } @@ -132,7 +148,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } const getPotDetailArgsFromState = () => { const args = { owner: state.owner, - admins: state.admins.filter((admin: any) => !admin.remove).map((admin: any) => admin.accountId), + admins: state.admins?.filter((admin: any) => !admin.remove).map((admin: any) => admin.accountId), chef: state.chef || null, pot_name: state.name, pot_description: state.description, @@ -229,17 +245,50 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } const pot = pots.find((pot: any) => pot.deployed_by === context.accountId && pot.deployed_at_ms > now); if (pot) { clearInterval(pollId); - State.update({ deploymentSuccess: true }); + updateState({ deploymentSuccess: true }); } }); }, pollIntervalMs); }); }; + const toast = () => { + updateState({ + toastContent: { + title: "Saved Successfully!", + description: "Changes to pot has been saved successfully.", + }, + }); + setTimeout(() => { + updateState({ + toastContent: { + title: "", + description: "", + }, + }); + }, 7000); + }; + + useEffect(() => { + if (context.accountId && transactionHashes) { + getTransactionsFromHashes(transactionHashes, context.accountId).then((trxs) => { + console.log("trxs", trxs); + + const transaction = trxs[0].body.result.transaction; + + const methodName = transaction.actions[0].FunctionCall.method_name; + const successVal = trxs[0].body.result.status?.SuccessValue; + const result = JSON.parse(Buffer.from(successVal, "base64").toString("utf-8")); + if (methodName === "admin_dangerously_set_pot_config" && result) { + toast(); + } + }); + } + }, []); + const handleUpdate = () => { // create update pot args const updateArgs = getPotDetailArgsFromState(); - // console.log("updateArgs: ", updateArgs); const depositFloat = JSON.stringify(updateArgs).length * 0.00003; const deposit = Big(depositFloat).mul(Big(10).pow(24)); const transactions = [ @@ -252,12 +301,21 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } }, ]; Near.call(transactions); - // NB: we won't get here if user used a web wallet, as it will redirect to the wallet - // <---- EXTENSION WALLET HANDLING ----> - // TODO: IMPLEMENT - }; - // console.log("state: ", state); + const potConfigSuccess = setInterval(() => { + PotSDK.asyncGetConfig(potId).then((detail: PotDetail) => { + if (deepEqual(updateArgs, detail, ["source_metadata", "toastContent"])) { + toast(); + clearInterval(potConfigSuccess); + } + }); + }, 1000); + + // Clear the interval after 60 seconds + setTimeout(() => { + clearInterval(potConfigSuccess); + }, 60000); + }; const validateAndUpdatePercentages = (percent: any, stateKey: any, errorKey: any, maxVal: any) => { // TODO: move this to separate component for percentage input that accepts "basisPoints" bool parameter @@ -276,7 +334,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } } // if it ends with a period and this is the only period in the string, set on state if (percent.endsWith(".") && percent.indexOf(".") === percent.length - 1) { - State.update({ + updateState({ [stateKey]: percent, }); return; @@ -290,13 +348,13 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } } } } - State.update(updates); + updateState(updates); }; const handleAddAdmin = () => { let isValid = validateNearAddress(state.admin); if (!isValid) { - State.update({ + updateState({ adminsError: "Invalid NEAR account ID", }); return; @@ -311,7 +369,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } }; const admins = [...state.admins, newAdmin]; // console.log("admins: ", admins); - State.update({ + updateState({ admins, admin: "", adminsError: "", @@ -320,7 +378,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } }; const handleRemoveAdmin = (accountId: any) => { - State.update({ + updateState({ admins: state.admins.map((admin: any) => { if (admin.accountId == accountId) { return { ...admin, remove: true }; @@ -353,11 +411,11 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } label: "Owner *", placeholder: `E.g. ${context.accountId}`, value: state.owner, - onChange: (owner) => State.update({ owner, ownerError: "" }), + onChange: (owner) => updateState({ owner, ownerError: "" }), validate: () => { // **CALLED ON BLUR** const valid = validateNearAddress(state.owner); - State.update({ ownerError: valid ? "" : "Invalid NEAR account ID" }); + updateState({ ownerError: valid ? "" : "Invalid NEAR account ID" }); }, error: state.ownerError, disabled: isUpdate ? !userIsOwner : true, @@ -367,7 +425,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } !account.remove) .map((account: any) => account.accountId), allowRemove: isUpdate ? userIsOwner : true, @@ -380,7 +438,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } {...{ varient: "outline", style: { width: "fit-content" }, - onClick: () => State.update({ isAdminsModalOpen: true }), + onClick: () => updateState({ isAdminsModalOpen: true }), }} > Add admins @@ -392,11 +450,11 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } label: "Name *", placeholder: "E.g. DeFi Center", value: state.name, - onChange: (name) => State.update({ name, nameError: "" }), + onChange: (name) => updateState({ name, nameError: "" }), validate: () => { // **CALLED ON BLUR** const valid = state.name.length <= MAX_POT_NAME_LENGTH; - State.update({ + updateState({ nameError: valid ? "" : `Name must be ${MAX_POT_NAME_LENGTH} characters or less`, }); }, @@ -409,7 +467,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } label: "Custom handle (optional - will slugify name by default)", placeholder: "e.g. my-pot-handle", value: state.customHandle, - onChange: (customHandle) => State.update({ customHandle, customHandleError: "" }), + onChange: (customHandle) => updateState({ customHandle, customHandleError: "" }), validate: () => { // **CALLED ON BLUR** const suffix = `.${potFactoryContractId}`; @@ -423,7 +481,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } ? "" : `Invalid handle (can only contain lowercase alphanumeric symbols + _ or -)`; } - State.update({ + updateState({ customHandleError, }); }, @@ -437,11 +495,11 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } label: "Description", placeholder: "Type description", value: state.description, - onChange: (description: string) => State.update({ description }), + onChange: (description: string) => updateState({ description }), validate: () => { // **CALLED ON BLUR** const valid = state.description.length <= MAX_POT_DESCRIPTION_LENGTH; - State.update({ + updateState({ descriptionError: valid ? "" : `Description must be ${MAX_POT_DESCRIPTION_LENGTH} characters or less`, }); }, @@ -509,7 +567,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } selectTime: true, value: state.applicationStartDate, onChange: (date) => { - State.update({ applicationStartDate: date }); + updateState({ applicationStartDate: date }); }, validate: () => { // **CALLED ON BLUR** @@ -520,7 +578,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } const applicationEndDate = new Date(state.applicationEndDate).getTime(); const valid = applicationStartDate > now && (!applicationEndDate || applicationStartDate < applicationEndDate); - State.update({ + updateState({ applicationStartDateError: valid ? "" : "Invalid application start date", }); }, @@ -534,14 +592,14 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } // placeholder: "0", // TODO: possibly add this back in selectTime: true, value: state.applicationEndDate, - onChange: (date) => State.update({ applicationEndDate: date }), + onChange: (date) => updateState({ applicationEndDate: date }), validate: () => { // **CALLED ON BLUR** // must be before matching round start date const valid = (!state.matchingRoundStartDate || state.applicationEndDate < state.matchingRoundStartDate) && (!state.applicationStartDate || state.applicationEndDate > state.applicationStartDate); - State.update({ + updateState({ applicationEndDateError: valid ? "" : "Invalid application end date", }); }, @@ -554,14 +612,14 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } label: "Matching round start date", selectTime: true, value: state.matchingRoundStartDate, - onChange: (date) => State.update({ matchingRoundStartDate: date }), + onChange: (date) => updateState({ matchingRoundStartDate: date }), validate: () => { // **CALLED ON BLUR** // must be after application end and before matching round end const valid = (!state.applicationEndDate || state.matchingRoundStartDate > state.applicationEndDate) && (!state.matchingRoundEndDate || state.matchingRoundStartDate < state.matchingRoundEndDate); - State.update({ + updateState({ matchingRoundStartDateError: valid ? "" : "Invalid round start date", }); }, @@ -576,13 +634,13 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } // placeholder: "0", // TODO: possibly add this back in selectTime: true, value: state.matchingRoundEndDate, - onChange: (date) => State.update({ matchingRoundEndDate: date }), + onChange: (date) => updateState({ matchingRoundEndDate: date }), validate: () => { // **CALLED ON BLUR** // must be after matching round start const valid = !state.matchingRoundStartDate || state.matchingRoundEndDate > state.matchingRoundStartDate; - State.update({ matchingRoundEndDateError: valid ? "" : "Invalid round end date" }); + updateState({ matchingRoundEndDateError: valid ? "" : "Invalid round end date" }); }, error: state.matchingRoundEndDateError, disabled: isUpdate ? !isAdminOrGreater : false, @@ -596,7 +654,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } placeholder: "0", value: state.minMatchingPoolDonationAmount, onChange: (amountNear) => { - State.update({ minMatchingPoolDonationAmount: amountNear }); + updateState({ minMatchingPoolDonationAmount: amountNear }); }, validate: () => { // **CALLED ON BLUR** @@ -618,11 +676,11 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } label: "Assign chef", placeholder: "E.g. user.near", value: state.chef, - onChange: (chef) => State.update({ chef }), + onChange: (chef) => updateState({ chef }), validate: () => { // **CALLED ON BLUR** const valid = validateNearAddress(state.chef); - State.update({ chefError: valid ? "" : "Invalid NEAR account ID" }); + updateState({ chefError: valid ? "" : "Invalid NEAR account ID" }); }, error: state.chefError, disabled: isUpdate ? !isAdminOrGreater : false, @@ -662,11 +720,11 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } label: "Max. approved projects", placeholder: "e.g. 20", value: state.maxProjects, - onChange: (maxProjects) => State.update({ maxProjects }), + onChange: (maxProjects) => updateState({ maxProjects }), validate: () => { // **CALLED ON BLUR** const valid = parseInt(state.maxProjects) <= MAX_MAX_PROJECTS; - State.update({ maxProjectsError: valid ? "" : `Maximum ${MAX_MAX_PROJECTS}` }); + updateState({ maxProjectsError: valid ? "" : `Maximum ${MAX_MAX_PROJECTS}` }); }, error: state.maxProjectsError, disabled: isUpdate ? !isAdminOrGreater : false, @@ -684,7 +742,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } id: "registrationSelector", checked: state.usePotlockRegistry, onClick: (e: any) => { - State.update({ + updateState({ usePotlockRegistry: e.target.checked, }); }, @@ -707,7 +765,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } id: "sybilSelector", checked: state.useNadabotSybil, onClick: (e: any) => { - State.update({ + updateState({ useNadabotSybil: e.target.checked, }); }, @@ -749,12 +807,12 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } {state.isAdminsModalOpen && ( State.update({ isAdminsModalOpen: false }), + onClose: () => updateState({ isAdminsModalOpen: false }), titleText: "Add admins", descriptionText: "Add NEAR account IDs for your admins.", inputValue: state.admin, onInputChange: (admin: any) => { - State.update({ admin, adminsError: "" }); + updateState({ admin, adminsError: "" }); }, handleAddAccount: handleAddAdmin, handleRemoveAccount: handleRemoveAdmin, @@ -764,6 +822,7 @@ const ConfigForm = ({ potDetail, style }: { potDetail?: PotDetail; style?: any } }} /> )} + ); }; diff --git a/src/pages/Pot/NavPages/Projects/Projects.tsx b/src/pages/Pot/NavPages/Projects/Projects.tsx index fb780080..d0a275a5 100644 --- a/src/pages/Pot/NavPages/Projects/Projects.tsx +++ b/src/pages/Pot/NavPages/Projects/Projects.tsx @@ -1,4 +1,4 @@ -import { useState, Social, context, useParams, createDebounce } from "alem"; +import { useState, Social, context, useParams, createDebounce, useEffect } from "alem"; import PotSDK from "@app/SDK/pot"; import Card from "@app/components/Card/Card"; import ListSection from "@app/pages/Projects/components/ListSection"; @@ -15,7 +15,7 @@ type Props = { const Projects = (props: Props) => { const [filteredProjects, setFilteredProjects] = useState([]); const [projects, setProjects] = useState(null); - const [flaggedAddresses, setFlaggedAddresses] = useState(null); + const [flaggedAddresses, setFlaggedAddresses] = useState(null); const [payouts, setPayouts] = useState(null); // get projects @@ -24,10 +24,16 @@ const Projects = (props: Props) => { const { potDetail, allDonations } = props; if (!projects) { - PotSDK.asyncGetApprovedApplications(potId).then((projects: any) => { - setProjects(projects); - setFilteredProjects(projects); - }); + PotSDK.asyncGetApprovedApplications(potId) + .then((projects: any) => { + setProjects(projects); + setFilteredProjects(projects); + }) + .catch((err: any) => { + console.log("error fetching projects ", err); + setProjects([]); + setFilteredProjects([]); + }); } const Loading = () => ( @@ -56,24 +62,28 @@ const Projects = (props: Props) => { .catch((err) => console.log("error getting the flagged accounts ", err)); } - if (!payouts) { - if (allDonations.length && flaggedAddresses) - calculatePayouts(allDonations, potDetail.matching_pool_balance, flaggedAddresses) - .then((payouts: any) => { - setPayouts(payouts ?? []); - }) - .catch((err) => { - console.log("error while calculating payouts ", err); - setPayouts([]); - }); - } + useEffect(() => { + if (!payouts) { + if (allDonations.length && flaggedAddresses) + calculatePayouts(allDonations, potDetail.matching_pool_balance, flaggedAddresses) + .then((payouts: any) => { + setPayouts(payouts ?? []); + }) + .catch((err) => { + console.log("error while calculating payouts ", err); + setPayouts([]); + }); + else if (allDonations.length === 0 && flaggedAddresses?.length === 0) { + setPayouts([]); + } + } + }, [allDonations, flaggedAddresses]); if (!flaggedAddresses || !payouts) return ; const searchByWords = (searchTerm: string) => { if (projects.length) { searchTerm = searchTerm.toLowerCase().trim(); - // setSearchTerm(searchTerm); const updatedProjects = projects.filter((project: any) => { const profile: any = Social.getr(`${project.project_id}/profile`); const fields = [ @@ -113,39 +123,43 @@ const Projects = (props: Props) => { className="search-input" /> - { - return ( - - ); - }} - /> + {filteredProjects.length > 0 ? ( + { + return ( + + ); + }} + /> + ) : ( +
No projects
+ )} ); }; diff --git a/src/pages/Pot/components/ApplicationReviewModal/ApplicationReviewModal.tsx b/src/pages/Pot/components/ApplicationReviewModal/ApplicationReviewModal.tsx index 675d0219..41e0dabe 100644 --- a/src/pages/Pot/components/ApplicationReviewModal/ApplicationReviewModal.tsx +++ b/src/pages/Pot/components/ApplicationReviewModal/ApplicationReviewModal.tsx @@ -1,18 +1,22 @@ import { Near, State, state, useParams } from "alem"; +import PotSDK from "@app/SDK/pot"; import Button from "@app/components/Button"; import TextArea from "@app/components/Inputs/TextArea/TextArea"; import constants from "@app/constants"; import ModalOverlay from "@app/modals/ModalOverlay"; +import { PotApplication } from "@app/types"; import { ModalBody, ModalFooter, ModalHeader } from "./styles"; const ApplicationReviewModal = ({ projectId, onClose, newStatus, + toast, }: { projectId: string; newStatus: string; onClose: () => void; + toast: (newStatus: string) => void; }) => { State.init({ reviewMessage: "", @@ -35,6 +39,21 @@ const ApplicationReviewModal = ({ onClose(); }; + const handleSuccess = () => { + const applicationSuccess = setInterval(() => { + PotSDK.asyncGetApplicationByProjectId(potId, projectId).then((application: PotApplication) => { + if (application.status === newStatus) { + toast(newStatus); + } + }); + }, 1000); + // Clear the interval after 60 seconds + setTimeout(() => { + onClose(); + clearInterval(applicationSuccess); + }, 60000); + }; + const handleSubmit = () => { const args = { project_id: projectId, @@ -51,8 +70,7 @@ const ApplicationReviewModal = ({ }, ]; Near.call(transactions); - // NB: we won't get here if user used a web wallet, as it will redirect to the wallet - // <---- TODO: IMPLEMENT EXTENSION WALLET HANDLING ----> + handleSuccess(); }; return ( diff --git a/src/pages/Pot/components/DonationsTable/DonationsTable.tsx b/src/pages/Pot/components/DonationsTable/DonationsTable.tsx index a88ff86e..6f5e70b5 100644 --- a/src/pages/Pot/components/DonationsTable/DonationsTable.tsx +++ b/src/pages/Pot/components/DonationsTable/DonationsTable.tsx @@ -6,17 +6,23 @@ import ProfileImage from "@app/components/mob.near/ProfileImage"; import _address from "@app/utils/_address"; import calcNetDonationAmount from "@app/utils/calcNetDonationAmount"; import getTimePassed from "@app/utils/getTimePassed"; +import getTransactionsFromHashes from "@app/utils/getTransactionsFromHashes"; import hrefWithParams from "@app/utils/hrefWithParams"; import FlagModal from "../FlagModal/FlagModal"; import FlagSuccessModal from "../FlagSuccessModal/FlagSuccessModal"; import FlagBtn from "./FlagBtn"; import { Container, FlagTooltipWrapper, SearchBar, SearchBarContainer, SearchIcon, TrRow } from "./styles"; +type FlagSuccess = { + account: string; + reason: string; +}; + const DonationsTable = (props: any) => { const accountId = context.accountId; const { filteredDonations, filter, handleSearch, sortDonation, currentFilter, potDetail } = props; - const { potId } = useParams(); + const { potId, transactionHashes } = useParams(); const { admins, owner, chef, all_paid_out } = potDetail; @@ -24,7 +30,7 @@ const DonationsTable = (props: any) => { const [currentPage, setCurrentPage] = useState(1); const [flagAddress, setFlagAddress] = useState(null); - const [successFlag, setSuccessFlag] = useState(null); + const [successFlag, setSuccessFlag] = useState(null); const [updateFlaggedAddresses, setUpdateFlaggedAddresses] = useState(false); const [flaggedAddresses, setFlaggedAddresses] = useState([]); const perPage = 30; @@ -42,6 +48,37 @@ const DonationsTable = (props: any) => { const potAdmins = [owner, chef, ...admins]; const hasAuthority = potAdmins.includes(accountId) && !all_paid_out; + // Handle flag success for extention wallet + useEffect(() => { + if (accountId && transactionHashes) { + getTransactionsFromHashes(transactionHashes, accountId).then((trxs) => { + const transaction = trxs[0].body.result.transaction; + + const methodName = transaction.actions[0].FunctionCall.method_name; + const signer_id = transaction.signer_id; + const receiver_id = transaction.receiver_id; + + const { data } = JSON.parse(Buffer.from(transaction.actions[0].FunctionCall.args, "base64").toString("utf-8")); + + if (methodName === "set" && receiver_id === "social.near" && data) { + try { + const pLBlacklistedAccounts = JSON.parse(data[signer_id].profile.pLBlacklistedAccounts); + const pLBlacklistedAccountsForPot = pLBlacklistedAccounts[potId]; + const allPotFlaggedAccounts = Object.keys(pLBlacklistedAccountsForPot); + const account = allPotFlaggedAccounts[allPotFlaggedAccounts.length - 1]; + const reason = pLBlacklistedAccountsForPot[account]; + setSuccessFlag({ + account, + reason, + }); + } catch (err) { + console.log("error parsing flag transaction ", err); + } + } + }); + } + }, []); + const checkIfIsFlagged = (address: string) => flaggedAddresses.find((obj: any) => obj.potFlaggedAcc[address]); const FlagTooltip = ({ flag, href, address }: any) => ( @@ -192,7 +229,7 @@ const DonationsTable = (props: any) => { perPage: perPage, }} /> - {flagAddress != null && ( + {flagAddress && ( { }} /> )} - {successFlag != null && ( + {accountId && successFlag && ( { }; return ( - +
@@ -158,9 +158,9 @@ const FlagModal = (props: any) => {
Flagging this account will remove their donations when calculating payouts for this pot
- + diff --git a/src/pages/Pot/components/FlagModal/styles.ts b/src/pages/Pot/components/FlagModal/styles.ts index 6a0f1d28..034f8fd3 100644 --- a/src/pages/Pot/components/FlagModal/styles.ts +++ b/src/pages/Pot/components/FlagModal/styles.ts @@ -97,11 +97,6 @@ export const ButtonsWrapper = styled.div` gap: 1.5rem; grid-template-columns: repeat(2, 1fr); button { - font-weight: 500; - } - .cancel { - border: none; - background: none; - color: #dd3345; + width: 100%; } `; diff --git a/src/pages/Pot/components/FundModal/FundModal.tsx b/src/pages/Pot/components/FundModal/FundModal.tsx index 016f1113..ff3a0df7 100644 --- a/src/pages/Pot/components/FundModal/FundModal.tsx +++ b/src/pages/Pot/components/FundModal/FundModal.tsx @@ -1,4 +1,5 @@ import { State, state, useParams, Big, Social, Near, context } from "alem"; +import PotSDK from "@app/SDK/pot"; import Button from "@app/components/Button"; import CheckBox from "@app/components/Inputs/Checkbox/Checkbox"; import Text from "@app/components/Inputs/Text/Text"; @@ -6,21 +7,25 @@ import TextArea from "@app/components/Inputs/TextArea/TextArea"; import ProfileImage from "@app/components/mob.near/ProfileImage"; import constants from "@app/constants"; import ModalOverlay from "@app/modals/ModalOverlay"; -import { PotDetail } from "@app/types"; +import { PotDetail, PotDonation } from "@app/types"; import _address from "@app/utils/_address"; import doesUserHaveDaoFunctionCallProposalPermissions from "@app/utils/doesUserHaveDaoFunctionCallProposalPermissions"; import hrefWithParams from "@app/utils/hrefWithParams"; import yoctosToNear from "@app/utils/yoctosToNear"; +import { ExtendedFundDonation } from "../SuccessFundModal/SuccessFundModal"; import { FeeText, Label, ModalTitle, Row, TextBold, UserChipLink } from "./styles"; type Props = { potDetail: PotDetail; onClose: () => void; + setFundDonation: (fundDonation: ExtendedFundDonation) => void; }; -const FundModal = ({ potDetail, onClose }: Props) => { +const FundModal = ({ potDetail, onClose, setFundDonation }: Props) => { const { referrerId, potId } = useParams(); + const accountId = context.accountId; + const { MAX_DONATION_MESSAGE_LENGTH, SUPPORTED_FTS, ONE_TGAS } = constants; const { @@ -80,6 +85,29 @@ const FundModal = ({ potDetail, onClose }: Props) => { ? (matchingPoolDonationAmountNear * referral_fee_matching_pool_basis_points) / 10_000 || 0 : 0; + const handleSuccess = (afterTs: number) => { + const donateSuccess = setInterval(() => { + PotSDK.asyncGetDonationsForDonor(potId, accountId).then((donations: PotDonation[]) => { + const fundDonation = donations.find((donation) => donation.donated_at > afterTs && !donation.project_id); + if (fundDonation) { + setFundDonation({ + ...fundDonation, + potId, + potDetail, + }); + clearInterval(donateSuccess); + onClose(); + } + }); + }, 1000); + + // Clear the interval after 60 seconds + setTimeout(() => { + onClose(); + clearInterval(donateSuccess); + }, 60000); + }; + const handleMatchingPoolDonation = () => { const args: any = { message: matchingPoolDonationMessage, @@ -139,8 +167,7 @@ const FundModal = ({ potDetail, onClose }: Props) => { } Near.call(transactions); - // NB: we won't get here if user used a web wallet, as it will redirect to the wallet - // <---- EXTENSION WALLET HANDLING ----> // TODO: implement + handleSuccess(Date.now()); }; const disabled = @@ -179,7 +206,7 @@ const FundModal = ({ potDetail, onClose }: Props) => { if (!policy) { State.update({ daoAddressError: "Invalid DAO address" }); } - if (!doesUserHaveDaoFunctionCallProposalPermissions(context.accountId || "", policy)) { + if (!doesUserHaveDaoFunctionCallProposalPermissions(accountId || "", policy)) { State.update({ daoAddressError: "Your account does not have permission to create proposals", }); diff --git a/src/pages/Pot/components/Header/Header.tsx b/src/pages/Pot/components/Header/Header.tsx index b580f0ad..146ddd5b 100644 --- a/src/pages/Pot/components/Header/Header.tsx +++ b/src/pages/Pot/components/Header/Header.tsx @@ -7,6 +7,7 @@ import useModals from "@app/hooks/useModals"; import CopyIcon from "@app/pages/Project/components/CopyIcon"; import { PotDetail } from "@app/types"; import calculatePayouts from "@app/utils/calculatePayouts"; +import getTransactionsFromHashes from "@app/utils/getTransactionsFromHashes"; import nearToUsd from "@app/utils/nearToUsd"; import yoctosToNear from "@app/utils/yoctosToNear"; import yoctosToUsdWithFallback from "@app/utils/yoctosToUsdWithFallback"; @@ -14,6 +15,7 @@ import ChallengeModal from "../ChallengeModal/ChallengeModal"; import FundModal from "../FundModal/FundModal"; import NewApplicationModal from "../NewApplicationModal/NewApplicationModal"; import PoolAllocationTable from "../PoolAllocationTable/PoolAllocationTable"; +import SuccessFundModal, { ExtendedFundDonation } from "../SuccessFundModal/SuccessFundModal"; import { ButtonsWrapper, Container, Description, Fund, HeaderWrapper, Referral, Title } from "./styles"; const Header = ({ potDetail, allDonations }: { potDetail: PotDetail; allDonations: any }) => { @@ -30,11 +32,12 @@ const Header = ({ potDetail, allDonations }: { potDetail: PotDetail; allDonation application_end_ms, cooldown_end_ms: _cooldown_end_ms, all_paid_out, + registry_provider, } = potDetail; const { IPFS_BASE_URL, NADA_BOT_URL } = constants; - const { potId } = useParams(); + const { potId, transactionHashes } = useParams(); // Start Modals provider const Modals = useModals(); @@ -52,6 +55,8 @@ const Header = ({ potDetail, allDonations }: { potDetail: PotDetail; allDonation const [isDao, setIsDao] = useState(null); const [applicationSuccess, setApplicationSuccess] = useState(null); const [flaggedAddresses, setFlaggedAddresses] = useState(null); + // set fund mathcing pool success + const [fundDonation, setFundDonation] = useState(null); const verifyIsOnRegistry = (address: any) => { Near.asyncView("lists.potlock.near", "get_registrations_for_registrant", { @@ -72,6 +77,28 @@ const Header = ({ potDetail, allDonations }: { potDetail: PotDetail; allDonation } }, []); + // Handle fund success for web wallet + useEffect(() => { + if (accountId && transactionHashes) { + getTransactionsFromHashes(transactionHashes, accountId).then((trxs) => { + const transaction = trxs[0].body.result.transaction; + + const methodName = transaction.actions[0].FunctionCall.method_name; + const receiver_id = transaction.receiver_id; + const successVal = trxs[0].body.result.status?.SuccessValue; + const result = JSON.parse(Buffer.from(successVal, "base64").toString("utf-8")); // atob not working + + if (methodName === "donate" && receiver_id === potId && result) { + setFundDonation({ + ...result, + potId, + potDetail, + }); + } + }); + } + }, []); + const projectNotRegistered = registryStatus === null; const userIsAdminOrGreater = admins.includes(accountId) || owner === accountId; const userIsChefOrGreater = userIsAdminOrGreater || chef === accountId; @@ -152,6 +179,8 @@ const Header = ({ potDetail, allDonations }: { potDetail: PotDetail; allDonation const registrationApproved = registryStatus === "Approved"; + const registrationApprovedOrNoRegistryProvider = registrationApproved || !registry_provider; + return ( @@ -191,12 +220,12 @@ const Header = ({ potDetail, allDonations }: { potDetail: PotDetail; allDonation )} {canApply && ( )} {now > public_round_end_ms && now < cooldown_end_ms && ( @@ -226,18 +255,29 @@ const Header = ({ potDetail, allDonations }: { potDetail: PotDetail; allDonation potDetail={potDetail} /> )} + {showChallengePayoutsModal && ( + setShowChallengePayoutsModal(false)} + /> + )} + {/* Fund Matching Pool Modal */} {isMatchingPoolModalOpen && ( { setIsMatchingPoolModalOpen(false); }} /> )} - {showChallengePayoutsModal && ( - setShowChallengePayoutsModal(false)} + {/* Fund Matching Pool Success Modal */} + {fundDonation && ( + { + setFundDonation(null); + }} /> )} diff --git a/src/pages/Pot/components/PoolAllocationTable/PoolAllocationTable.tsx b/src/pages/Pot/components/PoolAllocationTable/PoolAllocationTable.tsx index 2fe15c59..f6692ef6 100644 --- a/src/pages/Pot/components/PoolAllocationTable/PoolAllocationTable.tsx +++ b/src/pages/Pot/components/PoolAllocationTable/PoolAllocationTable.tsx @@ -31,6 +31,7 @@ const PoolAllocationTable = ({ potDetail, allDonations }: Props) => { } let sponsorshipDonations = PotSDK.getMatchingPoolDonations(potId); + if (sponsorshipDonations) sponsorshipDonations.sort((a: any, b: any) => b.net_amount - a.net_amount); const calcMatchedAmount = (donations: any) => { diff --git a/src/pages/Pot/components/SponsorsBoard/SponsorsBoard.tsx b/src/pages/Pot/components/SponsorsBoard/SponsorsBoard.tsx index fa3617d6..eac88d19 100644 --- a/src/pages/Pot/components/SponsorsBoard/SponsorsBoard.tsx +++ b/src/pages/Pot/components/SponsorsBoard/SponsorsBoard.tsx @@ -13,7 +13,7 @@ const Sponsor = ({ donation: { amount, donor_id, percentage_share }, colIdx }: a return (
- + {_address(profile.name || donor_id, 15)} diff --git a/src/pages/Pot/components/SuccessFundModal/SuccessFundModal.tsx b/src/pages/Pot/components/SuccessFundModal/SuccessFundModal.tsx new file mode 100644 index 00000000..f0dcb23b --- /dev/null +++ b/src/pages/Pot/components/SuccessFundModal/SuccessFundModal.tsx @@ -0,0 +1,125 @@ +import { Social, context, useMemo } from "alem"; +import BreakdownSummary from "@app/components/Cart/BreakdownSummary/BreakdownSummary"; +import constants from "@app/constants"; +import ModalOverlay from "@app/modals/ModalOverlay"; +import { PotDetail, PotDonation } from "@app/types"; +import formatWithCommas from "@app/utils/formatWithCommas"; +import hrefWithParams from "@app/utils/hrefWithParams"; +import yoctosToUsd from "@app/utils/yoctosToUsd"; +import { + Amount, + AmountUsd, + HeaderIcon, + ModalHeader, + ModalMain, + ModalMiddel, + ProjectName, + TwitterShare, +} from "./styles"; + +export type ExtendedFundDonation = PotDonation & { + potId: string; + potDetail: PotDetail; +}; + +const SuccessFundModal = ({ fundDonation, onClose }: { fundDonation: ExtendedFundDonation; onClose: () => void }) => { + const DEFAULT_GATEWAY = "https://bos.potlock.org/"; + const POTLOCK_TWITTER_ACCOUNT_ID = "PotLock_"; + const DEFAULT_SHARE_HASHTAGS = ["BOS", "PublicGoods", "Fund"]; + + const { ownerId, SUPPORTED_FTS } = constants; + + const twitterIntent = useMemo(() => { + const twitterIntentBase = "https://twitter.com/intent/tweet?text="; + + let url = + DEFAULT_GATEWAY + `${ownerId}/widget/Index?tab=pot&potId=${fundDonation.potId}&referrerId=${context.accountId}`; + let text = `I just funded ${fundDonation.potDetail.pot_name} on @${POTLOCK_TWITTER_ACCOUNT_ID}! Support public goods at `; + text = encodeURIComponent(text); + url = encodeURIComponent(url); + return twitterIntentBase + text + `&url=${url}` + `&hashtags=${DEFAULT_SHARE_HASHTAGS.join(",")}`; + }, [fundDonation]); + + const baseCurrency = fundDonation.potDetail.base_currency.toUpperCase() as "NEAR"; + + const nearAmount = formatWithCommas(SUPPORTED_FTS[baseCurrency].fromIndivisible(fundDonation.total_amount)); + const usdAmount = nearToUsd ? yoctosToUsd(fundDonation.total_amount) : null; + + return fundDonation ? ( + + + + + + + + +
Donation Successful
+ +
Share to
+
+ + + +
+
+
+ +
+ + + + + + + + + + + + + {nearAmount} + NEAR + + {usdAmount && {usdAmount} } +
+ + + + {fundDonation.potDetail.pot_name} + +
Round has been funded with
+
+
+ +
+
+ ) : ( + "" + ); +}; + +export default SuccessFundModal; diff --git a/src/pages/Pot/components/SuccessFundModal/styles.ts b/src/pages/Pot/components/SuccessFundModal/styles.ts new file mode 100644 index 00000000..c09de0aa --- /dev/null +++ b/src/pages/Pot/components/SuccessFundModal/styles.ts @@ -0,0 +1,93 @@ +import styled from "styled-components"; + +export const ModalMain = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + gap: 2rem; + padding: 40px 32px; +`; + +export const ModalHeader = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +`; + +export const HeaderIcon = styled.div` + padding: 12px; + width: 48px; + height: 48px; + border-radius: 50%; + background: #dd3345; + box-shadow: 0px 0px 0px 6px #fee6e5; + svg { + width: 100%; + height: 100%; + } +`; + +export const TwitterShare = styled.a` + display: flex; + gap: 8px; + color: white; + border-radius: 4px; + padding: 6px 1rem; + background: rgb(41, 41, 41); + align-items: center; + font-size: 14px; + cursor: pointer; + :hover { + text-decoration: none; + } +`; +export const ModalMiddel = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + .amount-wrapper { + display: flex; + align-items: center; + gap: 8px; + justify-content: center; + img, + svg { + width: 20px; + height: 20px; + } + img { + border-radius: 50%; + } + } +`; + +export const Amount = styled.div` + font-size: 22px; + font-weight: 500; + letter-spacing: -0.33px; + text-transform: uppercase; +`; + +export const AmountUsd = styled.div` + color: #7b7b7b; + font-size: 22px; +`; + +export const ProjectName = styled.div` + display: flex; + align-items: center; + gap: 3px; + font-size: 14px; + div { + color: #7b7b7b; + } + a { + color: #525252; + &:hover { + text-decoration: none; + } + } +`; diff --git a/src/pages/Profile/components/Body/Body.tsx b/src/pages/Profile/components/Body/Body.tsx index 87784dcf..8a831d48 100644 --- a/src/pages/Profile/components/Body/Body.tsx +++ b/src/pages/Profile/components/Body/Body.tsx @@ -1,9 +1,12 @@ -import { Near, context, props, useState, useParams, useMemo } from "alem"; +import { Near, context, props, useState, useParams, useMemo, useEffect } from "alem"; import ListsSDK from "@app/SDK/lists"; import Button from "@app/components/Button"; import TextArea from "@app/components/Inputs/TextArea/TextArea"; +import ToastContainer from "@app/components/ToastNotification/getToastContainer"; import constants from "@app/constants"; import ModalOverlay from "@app/modals/ModalOverlay"; +import { Registration, RegistrationStatus } from "@app/types"; +import getTransactionsFromHashes from "@app/utils/getTransactionsFromHashes"; import Select from "../../../../components/Inputs/Select/Select"; import BannerHeader from "../BannerHeader/BannerHeader"; import BodyHeader from "../BodyHeader/BodyHeader"; @@ -19,8 +22,11 @@ import { Container, Details, ModalTitle, Row, Wrapper } from "./styles"; // }; const Body = (props: any) => { - const { projectId, registration } = props; - const { accountId: _accountId } = useParams(); + const { projectId } = props; + const { accountId: _accountId, transactionHashes } = useParams(); + + const registration = ListsSDK.getRegistration(null, projectId); + const accountId = _accountId ?? context.accountId; const { PROJECT_STATUSES, @@ -28,10 +34,28 @@ const Body = (props: any) => { } = constants; const [statusReview, setStatusReview] = useState({ modalOpen: false, notes: "", newStatus: "" }); + const [toastContent, setToastContent] = useState({ + title: "", + description: "", + }); const listsContractId = ListsSDK.getContractId(); const userIsRegistryAdmin = ListsSDK.isRegistryAdmin(context.accountId); + const statusToast = (status: RegistrationStatus) => { + setToastContent({ + title: "Updated Successfully!", + description: `Project has been successfully updated to ${status.toLowerCase()}.`, + }); + + setTimeout(() => { + setToastContent({ + title: "", + description: ``, + }); + }, 7000); + }; + const handleUpdateStatus = () => { Near.call([ { @@ -45,8 +69,37 @@ const Body = (props: any) => { deposit: NEAR.toIndivisible(0.01).toString(), }, ]); + + // success update project notification + const updateProjectSuccess = setInterval(() => { + ListsSDK.asyncGetRegistration(null, projectId).then((registration: Registration) => { + if (registration.status === statusReview.newStatus) { + statusToast(registration.status); + } + }); + }, 1000); + + // Clear the interval after 60 seconds + setTimeout(() => { + clearInterval(updateProjectSuccess); + }, 60000); }; + // Handle update project status for web wallet + useEffect(() => { + if (accountId && transactionHashes) { + getTransactionsFromHashes(transactionHashes, accountId).then((trxs) => { + const transaction = trxs[0].body.result.transaction; + const methodName = transaction.actions[0].FunctionCall.method_name; + const successVal = trxs[0].body.result.status?.SuccessValue; + const result = JSON.parse(Buffer.from(successVal, "base64").toString("utf-8")); + if (methodName === "update_registration" && result) { + statusToast(result.status); + } + }); + } + }, []); + const SelectedNavComponent = useMemo(() => { return props.navOptions.find((option: any) => option.id == props.nav).source; }, []); @@ -69,7 +122,7 @@ const Body = (props: any) => { value: status, text: status, })), - value: { text: props.registration.status, value: props.registration.status }, + value: { text: registration.status, value: registration.status }, onChange: (status) => { if (status.value != registration.status) { setStatusReview({ ...statusReview, newStatus: status.value, modalOpen: true }); @@ -121,6 +174,7 @@ const Body = (props: any) => { )} + ); }; diff --git a/src/pages/Profile/components/BodyHeader/BodyHeader.tsx b/src/pages/Profile/components/BodyHeader/BodyHeader.tsx index a69e9798..b9c1e90d 100644 --- a/src/pages/Profile/components/BodyHeader/BodyHeader.tsx +++ b/src/pages/Profile/components/BodyHeader/BodyHeader.tsx @@ -79,12 +79,11 @@ const BodyHeader = ({ profile, accountId, projectId }: Props) => { style={{ marginLeft: "auto" }} href={hrefWithParams(`?tab=editproject&projectId=${projectId}`)} > - edit profile + Edit profile )} {accountId === context.accountId && !projectId && ( )} diff --git a/src/pages/Project/NavPages/About/About.tsx b/src/pages/Project/NavPages/About/About.tsx index a9a8e8cb..2349d698 100644 --- a/src/pages/Project/NavPages/About/About.tsx +++ b/src/pages/Project/NavPages/About/About.tsx @@ -30,7 +30,9 @@ const About = ({ projectId, accountId }: Props) => { }, []) : []; - const githubRepos = profile.plGithubRepos ? JSON.parse(profile.plGithubRepos) : []; + const githubRepos = (profile.plGithubRepos ? JSON.parse(profile.plGithubRepos) : []).map((url: string) => + url.replace("github.com/github.com/", "github.com/"), + ); function convertToGitHubURL(path: string) { const prefix = "https://github.com/"; diff --git a/src/pages/Project/Project.tsx b/src/pages/Project/Project.tsx index 10f6e133..f4303d50 100644 --- a/src/pages/Project/Project.tsx +++ b/src/pages/Project/Project.tsx @@ -116,7 +116,6 @@ const ProjectPage = () => { div, button { padding: 10px 0; width: 160px; diff --git a/src/pages/Project/components/FollowButton/FollowButton.tsx b/src/pages/Project/components/FollowButton/FollowButton.tsx index c2004e8a..ae4a39fa 100644 --- a/src/pages/Project/components/FollowButton/FollowButton.tsx +++ b/src/pages/Project/components/FollowButton/FollowButton.tsx @@ -25,47 +25,32 @@ const FollowButton = ({ accountId, classname }: Props) => { const type = follow ? "unfollow" : "follow"; - const socialArgs = { - data: { - [context.accountId]: { - graph: { follow: { [accountId]: follow ? null : "" } }, - index: { - graph: JSON.stringify({ - key: "follow", - value: { - type, - accountId: accountId, - }, - }), - notify: JSON.stringify({ - key: accountId, - value: { - type, - }, - }), + const data = { + graph: { follow: { [accountId]: follow ? null : "" } }, + index: { + graph: JSON.stringify({ + key: "follow", + value: { + type, + accountId: accountId, }, - }, + }), + notify: JSON.stringify({ + key: accountId, + value: { + type, + }, + }), }, }; const buttonText = loading ? "Loading" : follow ? "Following" : inverse ? "Follow back" : "Follow"; return ( - { - const transactions = [ - { - contractName: "social.near", - methodName: "set", - deposit: Big(JSON.stringify(socialArgs).length * 0.00003).mul(Big(10).pow(24)), - args: socialArgs, - }, - ]; - Near.call(transactions); - }} - > - {buttonText} + + + {buttonText} + ); }; diff --git a/src/pages/Project/components/FollowButton/styles.ts b/src/pages/Project/components/FollowButton/styles.ts index e02ac21f..6e4e6790 100644 --- a/src/pages/Project/components/FollowButton/styles.ts +++ b/src/pages/Project/components/FollowButton/styles.ts @@ -1,38 +1,43 @@ import styled from "styled-components"; export const FollowContainer = styled.div<{ buttonText?: string }>` - position: relative; - cursor: pointer; - border-radius: 6px; - border: 1px solid #dd3345; - font-size: 14px; - font-weight: 600; - color: #dd3345; - word-wrap: break-word; - transition: all 300ms; - &::before { - background: #dd3345; - position: absolute; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - content: "Unfollow"; - color: white; - opacity: 0; + button { + position: relative; + cursor: pointer; + border-radius: 6px; + border: 1px solid #dd3345; + background: transparent; + font-size: 14px; + font-weight: 600; + color: #dd3345; + word-wrap: break-word; transition: all 300ms; - } - :hover { - background: #dd3345; - color: white; - ${(props) => - props.buttonText === "Following" - ? ` + &::before { + background: #dd3345; + position: absolute; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + content: "Unfollow"; + color: white; + opacity: 0; + transition: all 300ms; + } + :hover { + background: #dd3345; + color: white; + ${(props) => + props.buttonText === "Following" + ? ` ::before { opacity: 1; } ` - : ""} + : ""} + } } `; diff --git a/src/pages/Projects/components/AllProjects/AllProjects.tsx b/src/pages/Projects/components/AllProjects/AllProjects.tsx index 20c4a003..617bd242 100644 --- a/src/pages/Projects/components/AllProjects/AllProjects.tsx +++ b/src/pages/Projects/components/AllProjects/AllProjects.tsx @@ -24,8 +24,9 @@ const AllProjects = () => { useEffect(() => { if (projects.length === 0 && projectsData) { const { allProjects, approvedProjects } = projectsData; + const shuffledProjects = [...approvedProjects].sort(() => Math.random() - 0.5); setProjects(allProjects); - setFilteredProjects(approvedProjects); + setFilteredProjects(shuffledProjects); } }, [projectsData]); @@ -156,9 +157,9 @@ const AllProjects = () => { {filteredProjects.length ? ( } + renderItem={(project: Project) => } /> ) : (
No results
diff --git a/src/pages/Projects/components/ListSection.tsx b/src/pages/Projects/components/ListSection.tsx index d5810a36..69a60f67 100644 --- a/src/pages/Projects/components/ListSection.tsx +++ b/src/pages/Projects/components/ListSection.tsx @@ -9,7 +9,7 @@ type BreakPoint = { type Props = { shouldShuffle?: boolean; renderItem: any; - items: any; + items: any[]; maxCols?: number; responsive?: BreakPoint[]; }; diff --git a/src/types.ts b/src/types.ts index d3b2aa6d..5f9c2a2b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,7 +25,7 @@ export type PotDetail = { pot_name: string; pot_description: string; max_projects: number; - base_currency: string; + base_currency: "near"; application_start_ms: number; application_end_ms: number; public_round_start_ms: number; @@ -43,7 +43,7 @@ export type PotDetail = { total_public_donations: string; public_donations_count: number; payouts: []; - cooldown_end_ms?: number; + cooldown_end_ms: number | null; all_paid_out: boolean; protocol_config_provider: string; }; @@ -53,3 +53,60 @@ export type Pot = { deployed_by: string; deployed_at_ms: number; }; + +export type PotDonation = { + id: string; + donor_id: string; + total_amount: string; + net_amount: string; + message: string; + donated_at: number; + project_id: null | string; + referrer_id: null | string; + referrer_fee: null | string; + protocol_fee: string; + matching_pool: boolean; + chef_id: null | string; + chef_fee: null | string; +}; + +export type FundDonation = { + id: string; + donor_id: string; + total_amount: string; + net_amount: string; + message: string; + donated_at: number; + project_id: null; + referrer_id: null | string; + referrer_fee: null | string; + protocol_fee: string; + matching_pool: true; + chef_id: null | string; + chef_fee: null | string; +}; + +export type RegistrationStatus = "Approved" | "Rejected" | "Pending" | "Graylisted" | "Blacklisted"; + +export type ApplicationStatus = "Pending" | "Approved" | "Rejected"; + +export type Registration = { + id: string; + registrant_id: string; + list_id: number; + status: RegistrationStatus; + submitted_ms: number; + updated_ms: number; + admin_notes: null | string; + registrant_notes: null | string; + registered_by: string; +}; + +export type PotApplication = { + project_id: string; + message: string; + status: ApplicationStatus; + submitted_at: number; + updated_at: null | string; + review_notes: null | string; +}; diff --git a/src/utils/calculatePayouts.ts b/src/utils/calculatePayouts.ts index 209641ac..73b38fa8 100644 --- a/src/utils/calculatePayouts.ts +++ b/src/utils/calculatePayouts.ts @@ -6,8 +6,8 @@ const calculatePayouts = (allPotDonations: any, totalMatchingPool: any, blacklis // * QF/CLR logic taken from https://github.com/gitcoinco/quadratic-funding/blob/master/quadratic-funding/clr.py * return new Promise((resolve, reject) => { - console.log("Calculting payouts; ignoring blacklisted donors &/or projects: ", blacklistedAccounts.join(", ")); - console.log("totalMatchingPool: ", totalMatchingPool); + // console.log("Calculting payouts; ignoring blacklisted donors &/or projects: ", blacklistedAccounts.join(", ")); + // console.log("totalMatchingPool: ", totalMatchingPool); // first, flatten the list of donations into a list of contributions const projectContributions: any = []; const allDonors = new Set(); @@ -50,7 +50,7 @@ const calculatePayouts = (allPotDonations: any, totalMatchingPool: any, blacklis console.error("error fetching human scores. Continuing anyway: ", e); }) .finally(() => { - console.log("human scores: ", humanScores); + // console.log("human scores: ", humanScores); // take the flattened list of contributions and aggregate // the amounts contributed by each user to each project. // create a dictionary where each key is a projectId and its value @@ -59,7 +59,7 @@ const calculatePayouts = (allPotDonations: any, totalMatchingPool: any, blacklis const contributions: any = {}; for (const [proj, user, amount] of projectContributions) { if (!humanScores[user] || !humanScores[user].is_human) { - console.log("skipping non-human: ", user); + // console.log("skipping non-human: ", user); continue; } if (!contributions[proj]) { @@ -67,7 +67,7 @@ const calculatePayouts = (allPotDonations: any, totalMatchingPool: any, blacklis } contributions[proj][user] = Big(contributions[proj][user] || 0).plus(amount); } - console.log("contributions: ", contributions); + // console.log("contributions: ", contributions); // calculate the total overlapping contribution amounts between pairs of users for each project. // create a nested dictionary where the outer keys are userIds and the inner keys are also userIds, // and the inner values are the total overlap between these two users' contributions. @@ -119,10 +119,10 @@ const calculatePayouts = (allPotDonations: any, totalMatchingPool: any, blacklis matching_amount_str: tot.toFixed(0), }); } - console.log("totals before: ", totals); + // console.log("totals before: ", totals); // if we reach saturation, we need to normalize if (bigtot.gte(totalPot)) { - console.log("NORMALIZING"); + // console.log("NORMALIZING"); for (const t of totals) { t.matching_amount_str = Big(t.matching_amount_str).div(bigtot).times(totalPot).toFixed(0); } @@ -136,7 +136,7 @@ const calculatePayouts = (allPotDonations: any, totalMatchingPool: any, blacklis } let residual = totalPot.minus(totalAllocatedBeforeRounding); - console.log("first round residual: ", residual.toFixed(0)); + // console.log("first round residual: ", residual.toFixed(0)); // Check if there is a residual due to rounding if (residual.abs().gt(Big("0"))) { @@ -150,14 +150,14 @@ const calculatePayouts = (allPotDonations: any, totalMatchingPool: any, blacklis totals[i].matching_amount_str = Big(totals[i].matching_amount_str).plus(additionalAllocation).toFixed(0); } - console.log("CALCULATING TOTALS AFTER RESIDUAL DISTRIBUTION"); + // console.log("CALCULATING TOTALS AFTER RESIDUAL DISTRIBUTION"); totalAllocatedBeforeRounding = Big(0); // Initialize the accumulator as a Big object for (const t of totals) { const currentMatchingAmount = Big(t.matching_amount_str); totalAllocatedBeforeRounding = totalAllocatedBeforeRounding.plus(currentMatchingAmount); } residual = totalPot.minus(totalAllocatedBeforeRounding); - console.log("second round residual: ", residual.toFixed(0)); + // console.log("second round residual: ", residual.toFixed(0)); // OLD RESIDUAL ADJUSTMENT LOGIC // if (residual.abs().gt(Big("0"))) { @@ -215,7 +215,7 @@ const calculatePayouts = (allPotDonations: any, totalMatchingPool: any, blacklis totalAllocatedBeforeRounding = totalAllocatedBeforeRounding.plus(currentMatchingAmount); } residual = totalPot.minus(totalAllocatedBeforeRounding); - console.log("Residual after final adjustment: ", residual.toFixed(0)); + // console.log("Residual after final adjustment: ", residual.toFixed(0)); } } diff --git a/src/utils/deepEqual.ts b/src/utils/deepEqual.ts new file mode 100644 index 00000000..8f869266 --- /dev/null +++ b/src/utils/deepEqual.ts @@ -0,0 +1,20 @@ +function deepEqual(obj1: any, obj2: any, keysToIgnore?: string[]): boolean { + if (obj1 === obj2) return true; + + if (typeof obj1 !== "object" || obj1 === null || typeof obj2 !== "object" || obj2 === null) { + return false; + } + + const keys1 = Object.keys(obj1).filter((key) => !keysToIgnore?.includes(key)); + const keys2 = Object.keys(obj2).filter((key) => !keysToIgnore?.includes(key)); + + // if (keys1.length !== keys2.length) return false; + + for (let key of keys1) { + if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key], keysToIgnore)) return false; + } + + return true; +} + +export default deepEqual; diff --git a/src/utils/getTransactionsFromHashes.ts b/src/utils/getTransactionsFromHashes.ts new file mode 100644 index 00000000..40111589 --- /dev/null +++ b/src/utils/getTransactionsFromHashes.ts @@ -0,0 +1,26 @@ +import { asyncFetch } from "alem"; + +export default function getTransactionsFromHashes(transactionHashes: string, accountId: string) { + const transactionHashesList = transactionHashes.split(","); + + const transactions = transactionHashesList.map((transaction) => { + const body = JSON.stringify({ + jsonrpc: "2.0", + id: "dontcare", + method: "tx", + params: [transaction, accountId], + }); + + // archival RPC node + // https://archival-rpc.mainnet.near.org + return asyncFetch("https://near.lava.build", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }); + }); + + return Promise.all(transactions); +} diff --git a/yarn.lock b/yarn.lock index 7d7fe774..576d1fa1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1229,10 +1229,10 @@ accepts@~1.3.4, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -alem@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/alem/-/alem-1.1.2.tgz#40c79e983f2c262b1095a46138e1e1a9ba0b957b" - integrity sha512-D2Mg9vUQQPgDEcZKErNRuKUfZg8yiFr8HzLiVotMTO+qvGNpD7hbOiP8gJ29WE8+46JLFwLVzPT4/lnm5N1xzQ== +alem@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/alem/-/alem-1.1.3.tgz#0f1375824a886a9b7f69e478f9938621aa3564e0" + integrity sha512-Lhprl34/dWuZ091y+g5duIwCBI0znnfuIugsrpqNS+YeG/c0CE6UW3ZOnPkEi3PgccZrLpN0im0ZWa2fQwvP/Q== dependencies: "@babel/core" "^7.24.3" "@babel/plugin-syntax-jsx" "^7.24.1" @@ -2811,7 +2811,16 @@ string-argv@0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2829,7 +2838,14 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==