Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
453 changes: 274 additions & 179 deletions src/app/[pohid]/[chain]/[request]/ActionBar.tsx

Large diffs are not rendered by default.

731 changes: 354 additions & 377 deletions src/app/[pohid]/[chain]/[request]/Appeal.tsx

Large diffs are not rendered by default.

341 changes: 213 additions & 128 deletions src/app/[pohid]/[chain]/[request]/Challenge.tsx

Large diffs are not rendered by default.

516 changes: 208 additions & 308 deletions src/app/[pohid]/[chain]/[request]/Evidence.tsx

Large diffs are not rendered by default.

158 changes: 77 additions & 81 deletions src/app/[pohid]/[chain]/[request]/Funding.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useCallback, useMemo, useState } from "react";
import { toast } from "react-toastify";
import CurrencyField from "components/CurrencyField";
import Modal from "components/Modal";
import ActionButton from "components/ActionButton";
import Progress from "components/Progress";
import RequestModal, {
RequestModalActions,
RequestModalHeader,
} from "components/RequestModal";
import usePoHWrite from "contracts/hooks/usePoHWrite";
import { useLoading } from "hooks/useLoading";
import useEnoughFunds from "hooks/useEnoughFunds";
import useFundingAmount from "hooks/useFundingAmount";
import { resolveTxState } from "utils/txState";
import type { RequestOptimisticOverlay } from "optimistic/types";
import { Hash, formatEther, parseEther } from "viem";
import { Hash, formatEther } from "viem";
import useChainParam from "hooks/useChainParam";
import { useAccount, useChainId } from "wagmi";
import { formatEth } from "utils/misc";
import { idToChain } from "config/chains";
import { useRequestOptimistic } from "optimistic/request";
Expand Down Expand Up @@ -42,17 +45,35 @@ const FundButton: React.FC<FundButtonProps> = ({
}) => {
const { effective, pendingAction, applyAction } = useRequestOptimistic();
const chain = useChainParam()!;
const userChainId = useChainId();
const [addedFundInput, setAddedFundInput] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const { isConnected } = useAccount();
const loading = useLoading();
const [isLoading, loadingMessage] = loading.use();
const isReconciling = pendingAction !== null;
const {
input: addedFundInput,
setInput: setAddedFundInput,
resetInput,
inputAmount,
remainingAmount,
unit,
isWrongChain,
disabled: isDisabled,
tooltip: submitTooltip,
} = useFundingAmount({
chainId: chain.id,
funded,
totalCost,
defaultToRemaining: true,
checks: [
{ active: isReconciling, message: "Waiting for indexer" },
{ active: isLoading },
],
});
const closeModal = useCallback(() => {
setIsModalOpen(false);
setAddedFundInput("");
resetInput();
loading.stop();
}, [loading]);
}, [loading, resetInput]);

const [prepareFund] = usePoHWrite(
"fundRequest",
Expand Down Expand Up @@ -87,27 +108,12 @@ const FundButton: React.FC<FundButtonProps> = ({
),
);

const remainingAmount = totalCost - funded;
const maxFundAmount = formatEther(remainingAmount);

const inputAmount = useMemo(() => {
if (!addedFundInput) return 0n;
try {
const parsed = parseEther(addedFundInput);
return parsed < 0n ? 0n : parsed;
} catch {
return null;
}
}, [addedFundInput]);

const isInvalidInput = inputAmount === null;
const isNonPositive = !isInvalidInput && inputAmount! <= 0n;
const exceedsRemaining = !isInvalidInput && inputAmount! > remainingAmount;
const funds = useEnoughFunds({
chainId: chain.id,
amount: !isInvalidInput && inputAmount ? inputAmount : undefined,
});
const isReconciling = pendingAction !== null;
const totalCostEth = formatEth(totalCost);
const progress =
totalCostEth > 0
? Math.min(100, (formatEth(funded) * 100) / totalCostEth)
: 0;

const handleSubmit = () => {
if (inputAmount === null || inputAmount <= 0n) return;
Expand All @@ -119,86 +125,76 @@ const FundButton: React.FC<FundButtonProps> = ({
});
};

const { disabled: isDisabled, tooltip: submitTooltip } = resolveTxState([
{ active: isReconciling, message: "Syncing" },
{ active: !isConnected, message: "Please connect your wallet" },
{
active: userChainId !== chain.id,
message: `Switch your chain above to ${idToChain(chain.id)?.name || "the correct chain"}`,
},
{ active: !addedFundInput, message: "Please enter an amount to fund" },
{ active: isInvalidInput, message: "Please enter a valid amount" },
{ active: isNonPositive, message: "Amount must be greater than 0" },
{
active: exceedsRemaining,
message: `Amount exceeds remaining needed (${formatEth(remainingAmount)} ${chain.nativeCurrency.symbol})`,
},
{ active: funds.isLoading, message: "Checking balance" },
{ active: funds.insufficient, message: funds.message },
{ active: isLoading },
]);

const trigger = resolveTxState([
{ active: !!externalDisabled, message: externalTooltip },
{ active: isReconciling, message: "Syncing" },
{ active: isReconciling, message: "Waiting for indexer" },
{
active: userChainId !== chain.id,
active: isWrongChain,
message: `Switch your chain above to ${idToChain(chain.id)?.name || "the correct chain"}`,
},
]);

return (
<>
<ActionButton
onClick={() => setIsModalOpen(true)}
onClick={() => {
setAddedFundInput(maxFundAmount);
setIsModalOpen(true);
}}
label="Fund"
className="mb-2 w-auto"
disabled={trigger.disabled}
tooltip={trigger.tooltip}
/>
<Modal
formal
header="Fund"
<RequestModal
open={isModalOpen}
onClose={closeModal}
canClose={!isLoading}
>
<div className="flex flex-col p-4">
<div className="flex w-full justify-center rounded p-4 font-bold">
<span
onClick={() =>
setAddedFundInput(formatEth(remainingAmount).toString())
}
className="text-orange mx-1 cursor-pointer font-semibold underline underline-offset-2"
>
{maxFundAmount}
</span>{" "}
<span className="text-primaryText">
{chain.nativeCurrency.symbol} Needed
</span>
</div>
<RequestModalHeader
title={
<>
Fund{" "}
<span className="text-peach">the profile&apos;s submission</span>
</>
}
description={
<>
<p>Anyone can help funding the profile&apos;s submission.</p>
<p>Type the value you want to contribute below.</p>
</>
}
/>
<div className="mt-12">
<Progress
value={progress}
label={`${formatEth(funded)} ${unit} out of ${formatEth(totalCost)} ${unit} required`}
labelClassName="text-primaryText"
/>
</div>
<div className="mt-4">
<CurrencyField
label="Amount funding"
symbol={chain.nativeCurrency.symbol}
label={`Amount (${unit})`}
labelClassName="!mb-2 !mt-0 text-sm !font-normal normal-case !text-secondaryText"
symbol={unit}
step="any"
min={0}
max={maxFundAmount}
value={addedFundInput}
onChange={(e) => setAddedFundInput(e.target.value)}
disabled={isLoading}
/>
<div className="mt-6 flex justify-center">
<ActionButton
disabled={isDisabled}
isLoading={isLoading}
onClick={handleSubmit}
label={loadingMessage || "Fund request"}
className="mx-auto w-auto"
tooltip={submitTooltip}
/>
</div>
</div>
</Modal>
<RequestModalActions
onReturn={closeModal}
returnDisabled={isLoading}
primaryLabel={loadingMessage || "Fund"}
onPrimary={handleSubmit}
primaryDisabled={isDisabled}
primaryLoading={isLoading}
primaryTooltip={submitTooltip}
/>
</RequestModal>
</>
);
};
Expand Down
74 changes: 43 additions & 31 deletions src/app/[pohid]/[chain]/[request]/Info.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,61 @@
"use client";

import { useState } from "react";
import Modal from "components/Modal";
import ActionButton from "components/ActionButton";
import RequestModal, { RequestModalHeader } from "components/RequestModal";
import InfoIcon from "icons/info.svg";
import Image from "next/image";
import { useState } from "react";

interface InfoProps {
nbRequests: number;
label: string;
}

export default function Info({ nbRequests, label }: InfoProps) {
const [isOpen, setIsOpen] = useState(false);
const [open, setOpen] = useState(false);

return (
<>
<span
className="flex cursor-pointer gap-x-[4px] text-slate-500 hover:text-slate-700"
onClick={() => setIsOpen(true)}
>
{label}&nbsp;
<InfoIcon className="h-6 w-6 stroke-slate-500 stroke-2 hover:stroke-slate-700" />
</span>
<Modal
formal
className="flex flex-col p-8"
open={isOpen}
onClose={() => setIsOpen(false)}
<button
type="button"
onClick={() => setOpen(true)}
className="inline-flex items-center gap-2 text-sm font-normal text-peach hover:opacity-80"
>
<Image
alt="poh id"
src="/logo/pohid.svg"
className="mx-auto mb-8"
height={128}
width={128}
{label}
<InfoIcon className="h-4 w-4 shrink-0 stroke-current stroke-2" />
</button>
<RequestModal open={open} onClose={() => setOpen(false)}>
<RequestModalHeader
title={
<>
What is a <span className="text-peach">PoH ID</span>?
</>
}
description={
<div className="flex flex-col gap-4 text-center">
<p>
A PoH ID is a unique, non-transferable identity for a verified
human on Proof of Humanity.
</p>
<p>
For a first registration, it is normally derived from the
registering wallet address. The identifier stays attached to the
human and can be reclaimed from a different wallet when the
protocol&apos;s recovery conditions are met.
</p>
<p>
This POH ID had {nbRequests} requests claimed in this chain.
</p>
</div>
}
/>
<p className="text-primaryText">
The Proof of Humanity ID is a soulbound ID. It corresponds to each
unique human registered on Proof of Humanity.
</p>
<p className="text-primaryText">
This POH ID had <strong>{nbRequests} requests</strong> claimed in this
chain
</p>
</Modal>
<div className="mt-8 flex justify-center">
<ActionButton
label="Got it"
onClick={() => setOpen(false)}
className="w-full sm:w-[170px]"
/>
</div>
</RequestModal>
</>
);
}
2 changes: 1 addition & 1 deletion src/app/[pohid]/[chain]/[request]/RemoveVouch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default function RemoveVouch({
});

const trigger = resolveTxState([
{ active: isReconciling, message: "Syncing" },
{ active: isReconciling, message: "Waiting for indexer" },
{ active: !!disabled, message: tooltip },
{
active: userChainId !== chain.id,
Expand Down
2 changes: 1 addition & 1 deletion src/app/[pohid]/[chain]/[request]/RequestErrorState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function ChainDataUnavailableCard({ chainName }: { chainName: string }) {
elsewhere in the app may also be incomplete or out of date. Nothing
on-chain is affected &mdash; please try again in a few minutes.
</div>
<RetryButton className="btn-main mt-6 px-4 py-2 normal-case" />
<RetryButton className="btn-primary mt-6 px-4 py-2 normal-case" />
</CardShell>
);
}
Expand Down
Loading