diff --git a/examples/framework/nextjs-evm/src/app/Provider.tsx b/examples/framework/nextjs-evm/src/app/Provider.tsx index 9403f28..0469d8a 100644 --- a/examples/framework/nextjs-evm/src/app/Provider.tsx +++ b/examples/framework/nextjs-evm/src/app/Provider.tsx @@ -21,7 +21,7 @@ const config = createConfig( // Required App Info appName: "Sonar Next.js example app", appDescription: "Next.js app showing how to integrate with the Sonar API via backend OAuth.", - }) + }), ); const queryClient = new QueryClient(); diff --git a/examples/framework/nextjs-evm/src/app/actions/sonar.ts b/examples/framework/nextjs-evm/src/app/actions/sonar.ts index 4e53b13..7416dc8 100644 --- a/examples/framework/nextjs-evm/src/app/actions/sonar.ts +++ b/examples/framework/nextjs-evm/src/app/actions/sonar.ts @@ -18,7 +18,7 @@ export const getEntities = createSonarServerAction(undefined); + + const isCancellationStage = contractStage === CANCELLATION_STAGE; + + const cancel = async () => { + setLoading(true); + setError(undefined); + try { + await cancelBid(); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + const committedAmount = entityState?.currentBid?.amount; + const hasCommitment = committedAmount !== undefined && committedAmount > BigInt(0); + + return ( +
+ {entityStateError ? ( +
+

{entityStateError.message}

+
+ ) : hasCommitment ? ( + <> +
+

Current committed amount: {`${Number(committedAmount) / 1e6} USDC`}

+
+ + {!isCancellationStage && ( +
+

Contract not in Cancellation stage

+

+ The "Cancel Bid" button is only active when the contract is in the Cancellation stage. To test + this feature, use the founder's dashboard to open the cancellation state ( + openCancellation). See the{" "} + + contract docs + + . +

+
+ )} + + + + ) : committedAmount !== undefined ? ( +
+

No active commitment to cancel.

+
+ ) : ( +
+

Loading...

+
+ )} + + {awaitingTxReceipt && !txReceipt &&

Waiting for transaction receipt...

} + {txReceipt?.status === "success" && ( +

Cancellation successful — your funds have been refunded

+ )} + {txReceipt?.status === "reverted" &&

Cancellation reverted

} + {error &&

{error.message}

} + {awaitingTxReceiptError &&

{awaitingTxReceiptError.message}

} +
+ ); +} + +function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { + return ( +
+ +
+ ); +} + +export default CancelCard; diff --git a/examples/framework/nextjs-evm/src/app/components/sale/CommitCard.tsx b/examples/framework/nextjs-evm/src/app/components/sale/CommitCard.tsx index 261129b..4e5cd55 100644 --- a/examples/framework/nextjs-evm/src/app/components/sale/CommitCard.tsx +++ b/examples/framework/nextjs-evm/src/app/components/sale/CommitCard.tsx @@ -7,8 +7,10 @@ import { saleUUID, paymentTokenAddress } from "@/lib/config"; import { useSonarPurchase } from "../../hooks/use-sonar-purchase"; import { useSaleContract } from "../../hooks/use-sale-contract"; +const COMMITMENT_STAGE = 1; + function readinessConfig( - sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase + sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase, ) { const okConfig = (msg: string) => ({ fgCol: "text-green-800", @@ -39,11 +41,11 @@ function readinessConfig( return warningConfig("The connected wallet is not eligible for this sale. Connect a different wallet."); case PrePurchaseFailureReason.MAX_WALLETS_USED: return warningConfig( - "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet." + "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet.", ); case PrePurchaseFailureReason.WALLET_NOT_LINKED: return warningConfig( - "Wallet not linked — The connected wallet is not linked to your entity. Please link it first." + "Wallet not linked — The connected wallet is not linked to your entity. Please link it first.", ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -70,6 +72,7 @@ function CommitSection({ awaitingTxReceiptError, isWrongChain, usdcBalance, + contractStage, } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); @@ -87,6 +90,7 @@ function CommitSection({ const hasExistingCommitment = isEntityStateLoaded && currentTotalRaw > 0n; const hasInsufficientBalance = usdcBalance != null && isIncrementAmountValid && incrementRaw > usdcBalance; + const notInCommitmentStage = contractStage !== undefined && contractStage !== COMMITMENT_STAGE; const [showInput, setShowInput] = useState(true); @@ -127,10 +131,27 @@ function CommitSection({
{hasExistingCommitment && (

- Current commitment:{" "} - {currentTotalReadableStr} USDC + Current commitment: {currentTotalReadableStr} USDC

)} + {notInCommitmentStage && ( +
+

Contract not in Commitment stage

+

+ The Commit button is only active during the Commitment stage. Use the founder's dashboard to open the + commitment period (openCommitment). See the{" "} + + contract docs + + . +

+
+ )} {showInput ? ( <>
@@ -154,7 +175,7 @@ function CommitSection({ )}
+ + + +
@@ -163,8 +161,8 @@ export default function Home() {

Easy Company Token Sale

- {/* Countdown Banner */} - {!saleIsLive && ( + {/* Phase Banner */} + {salePhase === "presale" && (

Sale Starting Soon

@@ -173,20 +171,29 @@ export default function Home() {
)} - {saleIsLive && ( + {salePhase === "live" && (

The sale is now live!

)} + + {salePhase === "cancellation" && ( +
+
+

Cancellation Period

+

Commitments can be cancelled during this period

+
+
+ )}
{/* Registration Phase */} - {sonarConnected && !saleIsLive && ( + {sonarConnected && salePhase === "presale" && (

Check Your Eligibility

@@ -210,7 +217,7 @@ export default function Home() { )} {/* Sale Phase */} - {saleIsLive && ( + {salePhase === "live" && (
{sonarConnected && (
@@ -240,6 +247,29 @@ export default function Home() {
)} + + {/* Cancellation Phase */} + {salePhase === "cancellation" && ( +
+ {sonarConnected && ( +
+ + +
+

Your Entity Information

+ +
+ + {address && selectedEntity && ( +
+

Cancel Your Bid

+ +
+ )} +
+ )} +
+ )}
diff --git a/examples/framework/nextjs-svm/src/app/actions/sonar.ts b/examples/framework/nextjs-svm/src/app/actions/sonar.ts index 4e53b13..7416dc8 100644 --- a/examples/framework/nextjs-svm/src/app/actions/sonar.ts +++ b/examples/framework/nextjs-svm/src/app/actions/sonar.ts @@ -18,7 +18,7 @@ export const getEntities = createSonarServerAction(undefined); + const [cancelSuccess, setCancelSuccess] = useState(false); + + const isCancellationStage = contractStage === CANCELLATION_STAGE; + const hasCommitment = committedAmount !== undefined && committedAmount > 0n; + + const cancel = async () => { + setLoading(true); + setError(undefined); + try { + await cancelBid(); + setCancelSuccess(true); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + return ( +
+ {entityStateError ? ( +
+

{entityStateError.message}

+
+ ) : hasCommitment ? ( + <> +
+

Current committed amount: {`${Number(committedAmount) / 1e6} USDC`}

+
+ + {!isCancellationStage && ( +
+

Contract not in Cancellation stage

+

+ The "Cancel Bid" button is only active when the contract is in the Cancellation stage. To test + this feature, use the founder's dashboard to open the cancellation state ( + openCancellation). See the{" "} + + contract docs + + . +

+
+ )} + + + + ) : committedAmount !== undefined ? ( +
+

No active commitment to cancel.

+
+ ) : ( +
+

Loading...

+
+ )} + + {awaitingTxReceipt &&

Waiting for transaction confirmation...

} + {cancelSuccess &&

Cancellation successful — your funds have been refunded

} + {error &&

{error.message}

} +
+ ); +} + +function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: string }) { + return ( +
+ +
+ ); +} + +export default CancelCard; diff --git a/examples/framework/nextjs-svm/src/app/components/sale/CommitCard.tsx b/examples/framework/nextjs-svm/src/app/components/sale/CommitCard.tsx index 71372f6..8a90f08 100644 --- a/examples/framework/nextjs-svm/src/app/components/sale/CommitCard.tsx +++ b/examples/framework/nextjs-svm/src/app/components/sale/CommitCard.tsx @@ -7,8 +7,10 @@ import { saleUUID } from "@/lib/config"; import { useSonarPurchase } from "../../hooks/use-sonar-purchase"; import { useSaleContract } from "../../hooks/use-sale-contract"; +const COMMITMENT_STAGE = 1; + function readinessConfig( - sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase + sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase, ) { const okConfig = (msg: string) => ({ fgCol: "text-green-800", @@ -39,11 +41,11 @@ function readinessConfig( return warningConfig("The connected wallet is not eligible for this sale. Connect a different wallet."); case PrePurchaseFailureReason.MAX_WALLETS_USED: return warningConfig( - "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet." + "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet.", ); case PrePurchaseFailureReason.WALLET_NOT_LINKED: return warningConfig( - "Wallet not linked — The connected wallet is not linked to your entity. Please link it first." + "Wallet not linked — The connected wallet is not linked to your entity. Please link it first.", ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -68,6 +70,7 @@ function CommitSection({ entityStateError, awaitingTxReceipt, usdcBalance, + contractStage, } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); @@ -85,6 +88,7 @@ function CommitSection({ const hasExistingCommitment = isEntityStateLoaded && currentTotalRaw > 0n; const hasInsufficientBalance = usdcBalance !== undefined && isIncrementAmountValid && incrementRaw > usdcBalance; + const notInCommitmentStage = contractStage !== undefined && contractStage !== COMMITMENT_STAGE; const [showInput, setShowInput] = useState(true); @@ -116,10 +120,27 @@ function CommitSection({
{hasExistingCommitment && (

- Current commitment:{" "} - {currentTotalReadableStr} USDC + Current commitment: {currentTotalReadableStr} USDC

)} + {notInCommitmentStage && ( +
+

Contract not in Commitment stage

+

+ The Commit button is only active during the Commitment stage. Use the founder's dashboard to open the + commitment period (openCommitment). See the{" "} + + contract docs + + . +

+
+ )} {showInput ? ( <>
@@ -143,7 +164,7 @@ function CommitSection({ )}
+ + + +
@@ -164,8 +162,8 @@ export default function Home() {

Easy Company Token Sale

- {/* Countdown Banner */} - {!saleIsLive && ( + {/* Phase Banner */} + {salePhase === "presale" && (

Sale Starting Soon

@@ -174,20 +172,29 @@ export default function Home() {
)} - {saleIsLive && ( + {salePhase === "live" && (

The sale is now live!

)} + + {salePhase === "cancellation" && ( +
+
+

Cancellation Period

+

Commitments can be cancelled during this period

+
+
+ )}
{/* Registration Phase */} - {sonarConnected && !saleIsLive && ( + {sonarConnected && salePhase === "presale" && (

Check Your Eligibility

@@ -211,7 +218,7 @@ export default function Home() { )} {/* Sale Phase */} - {saleIsLive && ( + {salePhase === "live" && (
{sonarConnected && (
@@ -241,6 +248,29 @@ export default function Home() {
)} + + {/* Cancellation Phase */} + {salePhase === "cancellation" && ( +
+ {sonarConnected && ( +
+ + +
+

Your Entity Information

+ +
+ + {address && selectedEntity && ( +
+

Cancel Your Bid

+ +
+ )} +
+ )} +
+ )}
diff --git a/examples/framework/nextjs-svm/src/lib/pkce-store.ts b/examples/framework/nextjs-svm/src/lib/pkce-store.ts index 06e7312..397a320 100644 --- a/examples/framework/nextjs-svm/src/lib/pkce-store.ts +++ b/examples/framework/nextjs-svm/src/lib/pkce-store.ts @@ -46,18 +46,18 @@ class InMemoryPKCEStore implements PKCEStore { } } -// Singleton instance - can be swapped for a different implementation -let pkceStoreInstance: PKCEStore | null = null; +// Use globalThis to survive Next.js dev-mode hot module reloads. +const g = globalThis as typeof globalThis & { __pkceStore?: PKCEStore }; /** * Get the PKCE store instance. * This factory function allows swapping implementations without changing call sites. */ export function getPKCEStore(): PKCEStore { - if (!pkceStoreInstance) { - pkceStoreInstance = new InMemoryPKCEStore(); + if (!g.__pkceStore) { + g.__pkceStore = new InMemoryPKCEStore(); } - return pkceStoreInstance; + return g.__pkceStore; } /** @@ -65,7 +65,7 @@ export function getPKCEStore(): PKCEStore { * Useful for swapping to a database-backed store. */ export function setPKCEStore(store: PKCEStore): void { - pkceStoreInstance = store; + g.__pkceStore = store; } const PKCE_TTL_MS = 10 * 60 * 1000; // 10 minutes diff --git a/examples/framework/react-evm/src/App.tsx b/examples/framework/react-evm/src/App.tsx index 58ca82b..09d9ded 100644 --- a/examples/framework/react-evm/src/App.tsx +++ b/examples/framework/react-evm/src/App.tsx @@ -16,4 +16,3 @@ export function App() { ); } - diff --git a/examples/framework/react-evm/src/Provider.tsx b/examples/framework/react-evm/src/Provider.tsx index e8361cb..efa998e 100644 --- a/examples/framework/react-evm/src/Provider.tsx +++ b/examples/framework/react-evm/src/Provider.tsx @@ -19,7 +19,7 @@ const config = createConfig( appName: "Sonar React example app", appDescription: "React app showing how to integrate with the Sonar API via the sonar-react and sonar-core libraries.", - }) + }), ); const queryClient = new QueryClient(); diff --git a/examples/framework/react-evm/src/components/sale/CancelCard.tsx b/examples/framework/react-evm/src/components/sale/CancelCard.tsx new file mode 100644 index 0000000..33b8d95 --- /dev/null +++ b/examples/framework/react-evm/src/components/sale/CancelCard.tsx @@ -0,0 +1,107 @@ +import { Hex } from "@echoxyz/sonar-core"; +import { useState } from "react"; +import { useSaleContract } from "../../hooks"; + +const CANCELLATION_STAGE = 2; + +function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { + const { + cancelBid, + contractStage, + entityState, + entityStateError, + awaitingTxReceipt, + txReceipt, + awaitingTxReceiptError, + } = useSaleContract(saleSpecificEntityID); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + + const isCancellationStage = contractStage === CANCELLATION_STAGE; + + const cancel = async () => { + setLoading(true); + setError(undefined); + try { + await cancelBid(); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + const committedAmount = entityState?.currentBid?.amount; + const hasCommitment = committedAmount !== undefined && committedAmount > BigInt(0); + + return ( +
+ {entityStateError ? ( +
+

{entityStateError.message}

+
+ ) : hasCommitment ? ( + <> +
+

Current committed amount: {`${Number(committedAmount) / 1e6} USDC`}

+
+ + {!isCancellationStage && ( +
+

Contract not in Cancellation stage

+

+ The "Cancel Bid" button is only active when the contract is in the Cancellation stage. To test + this feature, use the founder's dashboard to open the cancellation state ( + openCancellation). See the{" "} + + contract docs + + . +

+
+ )} + + + + ) : committedAmount !== undefined ? ( +
+

No active commitment to cancel.

+
+ ) : ( +
+

Loading...

+
+ )} + + {awaitingTxReceipt && !txReceipt &&

Waiting for transaction receipt...

} + {txReceipt?.status === "success" && ( +

Cancellation successful — your funds have been refunded

+ )} + {txReceipt?.status === "reverted" &&

Cancellation reverted

} + {error &&

{error.message}

} + {awaitingTxReceiptError &&

{awaitingTxReceiptError.message}

} +
+ ); +} + +function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { + return ( +
+ +
+ ); +} + +export default CancelCard; diff --git a/examples/framework/react-evm/src/components/sale/CommitCard.tsx b/examples/framework/react-evm/src/components/sale/CommitCard.tsx index 3e6307f..e4e1dab 100644 --- a/examples/framework/react-evm/src/components/sale/CommitCard.tsx +++ b/examples/framework/react-evm/src/components/sale/CommitCard.tsx @@ -8,8 +8,10 @@ import { } from "@echoxyz/sonar-react"; import { useSaleContract } from "../../hooks"; +const COMMITMENT_STAGE = 1; + function readinessConfig( - sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase + sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase, ) { const okConfig = (msg: string) => ({ fgCol: "text-green-800", @@ -40,11 +42,11 @@ function readinessConfig( return warningConfig("The connected wallet is not eligible for this sale. Connect a different wallet."); case PrePurchaseFailureReason.MAX_WALLETS_USED: return warningConfig( - "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet." + "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet.", ); case PrePurchaseFailureReason.WALLET_NOT_LINKED: return warningConfig( - "Wallet not linked — The connected wallet is not linked to your entity. Please link it first." + "Wallet not linked — The connected wallet is not linked to your entity. Please link it first.", ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -71,6 +73,7 @@ function CommitSection({ awaitingTxReceiptError, isWrongChain, usdcBalance, + contractStage, } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); @@ -88,6 +91,7 @@ function CommitSection({ const hasExistingCommitment = isEntityStateLoaded && currentTotalRaw > 0n; const hasInsufficientBalance = usdcBalance != null && isIncrementAmountValid && incrementRaw > usdcBalance; + const notInCommitmentStage = contractStage !== undefined && contractStage !== COMMITMENT_STAGE; const [showInput, setShowInput] = useState(true); @@ -128,10 +132,27 @@ function CommitSection({
{hasExistingCommitment && (

- Current commitment:{" "} - {currentTotalReadableStr} USDC + Current commitment: {currentTotalReadableStr} USDC

)} + {notInCommitmentStage && ( +
+

Contract not in Commitment stage

+

+ The Commit button is only active during the Commitment stage. Use the founder's dashboard to open the + commitment period (openCommitment). See the{" "} + + contract docs + + . +

+
+ )} {showInput ? ( <>
@@ -155,7 +176,7 @@ function CommitSection({ )}
+ + + +
@@ -167,8 +165,8 @@ export function Home() {

Easy Company Token Sale

- {/* Countdown Banner */} - {!saleIsLive && ( + {/* Phase Banner */} + {salePhase === "presale" && (

Sale Starting Soon

@@ -177,17 +175,26 @@ export function Home() {
)} - {saleIsLive && ( + {salePhase === "live" && (

The sale is now live!

)} + + {salePhase === "cancellation" && ( +
+
+

Cancellation Period

+

Commitments can be cancelled during this period

+
+
+ )}
{/* Registration Phase */} - {!saleIsLive && ( + {salePhase === "presale" && (
@@ -217,7 +224,7 @@ export function Home() { )} {/* Sale Phase */} - {saleIsLive && ( + {salePhase === "live" && (
{/* Connection Buttons */} @@ -248,6 +255,29 @@ export function Home() {
)} + + {/* Cancellation Phase */} + {salePhase === "cancellation" && ( +
+ {/* Connection Buttons */} + + + + {/* Entity Information */} +
+

Your Entity Information

+ +
+ + {/* Cancel Card */} + {address && selectedEntity && ( +
+

Cancel Your Bid

+ +
+ )} +
+ )} diff --git a/examples/framework/react-svm/src/App.tsx b/examples/framework/react-svm/src/App.tsx index 58ca82b..09d9ded 100644 --- a/examples/framework/react-svm/src/App.tsx +++ b/examples/framework/react-svm/src/App.tsx @@ -16,4 +16,3 @@ export function App() { ); } - diff --git a/examples/framework/react-svm/src/components/sale/CancelCard.tsx b/examples/framework/react-svm/src/components/sale/CancelCard.tsx new file mode 100644 index 0000000..17226aa --- /dev/null +++ b/examples/framework/react-svm/src/components/sale/CancelCard.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { useSaleContract } from "../../hooks/use-sale-contract"; + +const CANCELLATION_STAGE = 2; + +function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: string }) { + const { cancelBid, contractStage, committedAmount, entityStateError, awaitingTxReceipt } = + useSaleContract(saleSpecificEntityID); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + const [cancelSuccess, setCancelSuccess] = useState(false); + + const isCancellationStage = contractStage === CANCELLATION_STAGE; + const hasCommitment = committedAmount !== undefined && committedAmount > 0n; + + const cancel = async () => { + setLoading(true); + setError(undefined); + try { + await cancelBid(); + setCancelSuccess(true); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + return ( +
+ {entityStateError ? ( +
+

{entityStateError.message}

+
+ ) : hasCommitment ? ( + <> +
+

Current committed amount: {`${Number(committedAmount) / 1e6} USDC`}

+
+ + {!isCancellationStage && ( +
+

Contract not in Cancellation stage

+

+ The "Cancel Bid" button is only active when the contract is in the Cancellation stage. To test + this feature, use the founder's dashboard to open the cancellation state ( + openCancellation). See the{" "} + + contract docs + + . +

+
+ )} + + + + ) : committedAmount !== undefined ? ( +
+

No active commitment to cancel.

+
+ ) : ( +
+

Loading...

+
+ )} + + {awaitingTxReceipt &&

Waiting for transaction confirmation...

} + {cancelSuccess &&

Cancellation successful — your funds have been refunded

} + {error &&

{error.message}

} +
+ ); +} + +function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: string }) { + return ( +
+ +
+ ); +} + +export default CancelCard; diff --git a/examples/framework/react-svm/src/components/sale/CommitCard.tsx b/examples/framework/react-svm/src/components/sale/CommitCard.tsx index 09fd1ab..4c0d73a 100644 --- a/examples/framework/react-svm/src/components/sale/CommitCard.tsx +++ b/examples/framework/react-svm/src/components/sale/CommitCard.tsx @@ -8,8 +8,10 @@ import { } from "@echoxyz/sonar-react"; import { useSaleContract } from "../../hooks/use-sale-contract"; +const COMMITMENT_STAGE = 1; + function readinessConfig( - sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase + sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase, ) { const okConfig = (msg: string) => ({ fgCol: "text-green-800", @@ -40,11 +42,11 @@ function readinessConfig( return warningConfig("The connected wallet is not eligible for this sale. Connect a different wallet."); case PrePurchaseFailureReason.MAX_WALLETS_USED: return warningConfig( - "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet." + "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet.", ); case PrePurchaseFailureReason.WALLET_NOT_LINKED: return warningConfig( - "Wallet not linked — The connected wallet is not linked to your entity. Please link it first." + "Wallet not linked — The connected wallet is not linked to your entity. Please link it first.", ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -69,6 +71,7 @@ function CommitSection({ entityStateError, awaitingTxReceipt, usdcBalance, + contractStage, } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); @@ -86,6 +89,7 @@ function CommitSection({ const hasExistingCommitment = isEntityStateLoaded && currentTotalRaw > 0n; const hasInsufficientBalance = usdcBalance !== undefined && isIncrementAmountValid && incrementRaw > usdcBalance; + const notInCommitmentStage = contractStage !== undefined && contractStage !== COMMITMENT_STAGE; const [showInput, setShowInput] = useState(true); @@ -117,10 +121,27 @@ function CommitSection({
{hasExistingCommitment && (

- Current commitment:{" "} - {currentTotalReadableStr} USDC + Current commitment: {currentTotalReadableStr} USDC

)} + {notInCommitmentStage && ( +
+

Contract not in Commitment stage

+

+ The Commit button is only active during the Commitment stage. Use the founder's dashboard to open the + commitment period (openCommitment). See the{" "} + + contract docs + + . +

+
+ )} {showInput ? ( <>
@@ -144,7 +165,7 @@ function CommitSection({ )}
+ + + +
@@ -168,8 +166,8 @@ export function Home() {

Easy Company Token Sale

- {/* Countdown Banner */} - {!saleIsLive && ( + {/* Phase Banner */} + {salePhase === "presale" && (

Sale Starting Soon

@@ -178,17 +176,26 @@ export function Home() {
)} - {saleIsLive && ( + {salePhase === "live" && (

The sale is now live!

)} + + {salePhase === "cancellation" && ( +
+
+

Cancellation Period

+

Commitments can be cancelled during this period

+
+
+ )}
{/* Registration Phase */} - {!saleIsLive && ( + {salePhase === "presale" && (
@@ -218,7 +225,7 @@ export function Home() { )} {/* Sale Phase */} - {saleIsLive && ( + {salePhase === "live" && (
{/* Connection Buttons */} @@ -249,6 +256,29 @@ export function Home() {
)} + + {/* Cancellation Phase */} + {salePhase === "cancellation" && ( +
+ {/* Connection Buttons */} + + + + {/* Entity Information */} +
+

Your Entity Information

+ +
+ + {/* Cancel Card */} + {address && selectedEntity && ( +
+

Cancel Your Bid

+ +
+ )} +
+ )}