Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
31 changes: 30 additions & 1 deletion src/app/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import UnlockIcon from 'src/images/icons/unlock.svg';
import WithdrawIcon from 'src/images/icons/withdraw.svg';
import { shortenAddress } from 'src/utils/addresses';
import { usePageInvariant } from 'src/utils/navigation';
import { useIsMiniPay } from 'src/utils/useIsMiniPay';
import { StakingMode, useStakingMode } from 'src/utils/useStakingMode';
import useTabs from 'src/utils/useTabs';
import { Address } from 'viem';
Expand All @@ -52,7 +53,8 @@ import { useAccount } from 'wagmi';
export default function Page() {
const account = useAccount();
const address = account?.address;
usePageInvariant(!!address, '/');
const isMiniPay = useIsMiniPay();
usePageInvariant(!!address || isMiniPay, '/');

const { signingFor, isVoteSigner } = useVoteSignerToAccount(address);
const { balance: walletBalance } = useBalance(signingFor);
Expand Down Expand Up @@ -104,6 +106,7 @@ export default function Page() {
<LockButtons className="hidden md:flex" mode={mode} />
)}
</div>
{isMiniPay && totalLocked === 0n && <StakeCeloCta />}
{mode === 'CELO' ? (
<AccountStats
walletBalance={walletBalance}
Expand Down Expand Up @@ -405,6 +408,32 @@ function TableTabs({
);
}

function StakeCeloCta() {
const showTxModal = useTransactionModal();

return (
<div className="space-y-3 border border-taupe-300 bg-white px-3 py-4 md:px-5 md:py-5">
<h3 className="font-serif text-xl sm:text-2xl">Stake your CELO</h3>
<p className="text-sm sm:text-base">
Earn ~2% annually in exchange for agreeing to a three-day lockup. Staked CELO also gives you
access to participate in Celo Governance decisions so you can help shape the future of Celo,
the network powering MiniPay.
</p>
<SolidButton
onClick={() =>
showTxModal(TransactionFlowType.StakeStCELO, { action: StakeActionType.Stake })
}
Comment on lines +434 to +436
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Open CELO lock flow from the MiniPay stake CTA

The new StakeCeloCta button opens TransactionFlowType.StakeStCELO, but Governance visibility in MiniPay is gated by lockedBalance > 0n in src/components/nav/NavBar.tsx. In the MiniPay scenario this means a user with no locked CELO can follow this CTA, complete staking, and still never satisfy the nav gate (because stCELO deposit does not increase LockedGold), so the CTA fails to unlock the Governance access it promises.

Useful? React with 👍 / 👎.

className="bg-purple-300 text-white"
>
<div className="flex items-center space-x-1.5">
<Image src={StakingIcon} width={12} height={12} alt="" />
<span>Stake</span>
</div>
</SolidButton>
</div>
);
}

function AccountPageSkeleton() {
return (
<>
Expand Down
18 changes: 17 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';
import { useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo } from 'react';
import { Fade } from 'src/components/animation/Fade';
import { SkeletonBlock } from 'src/components/animation/Skeleton';
import { SolidButton } from 'src/components/buttons/SolidButton';
Expand All @@ -17,6 +18,21 @@ import { useIsMiniPay } from 'src/utils/useIsMiniPay';
import { useStakingMode } from 'src/utils/useStakingMode';

export default function Page() {
const isMiniPay = useIsMiniPay();
const router = useRouter();

useEffect(() => {
if (isMiniPay) {
router.replace('/account');
Comment on lines +25 to +26
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate MiniPay home redirect on wallet availability

This redirect is unconditional for MiniPay, so it also runs when no wallet address is connected (or before wagmi finishes hydrating). In that state, /account immediately redirects back to / via usePageInvariant(!!address, '/') in src/app/account/page.tsx, creating a //account navigation loop for MiniPay users and leaving no stable landing page. Redirecting only after account readiness (or when address is present) avoids that loop.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve minipay override when redirecting from home

The home redirect drops the current query string, so a session opened at /?minipay is immediately sent to /account without minipay, and useIsMiniPay() on the destination page falls back to window.ethereum?.isMiniPay. In a normal browser this disables the override after one navigation (and can bounce users back to / when no wallet is connected), which breaks the new query-param MiniPay emulation flow.

Useful? React with 👍 / 👎.

}
}, [isMiniPay, router]);

if (isMiniPay) return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid running validator query before MiniPay early return

MiniPay now redirects away from / and returns null, but this happens only after useValidatorGroups() runs, so each MiniPay home visit still triggers the full validator-group fetch path and its error-toast side effects (useToastError in src/features/validators/useValidatorGroups.ts) for data that is never rendered. This adds avoidable RPC load and can show irrelevant errors during redirect.

Useful? React with 👍 / 👎.


return <StakingPage />;
}

function StakingPage() {
const { groups, totalVotes } = useValidatorGroups();

return (
Expand Down
9 changes: 8 additions & 1 deletion src/components/nav/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useCallback } from 'react';
import { ChevronIcon } from 'src/components/icons/Chevron';
import { CeloGlyph } from 'src/components/logos/Celo';
import { DropdownMenu } from 'src/components/menus/Dropdown';
import { useLockedBalance } from 'src/features/account/hooks';
import Bridge from 'src/images/icons/bridge.svg';
import Dashboard from 'src/images/icons/dashboard.svg';
import Delegate from 'src/images/icons/delegate.svg';
Expand All @@ -18,7 +19,7 @@ import { useAccount } from 'wagmi';

const LINKS = (isWalletConnected?: boolean) => [
{ label: 'Staking', to: '/', icon: Staking },
{ label: 'Governance', to: '/governance', icon: Governance },
{ label: 'Governance', to: '/governance', icon: Governance, hideInMiniPayUntilStaked: true },
{ label: 'Delegate', to: '/delegate', icon: Delegate, hideInMiniPay: true },
{ label: 'Bridge', to: '/bridge', icon: Bridge, hideInMiniPay: true },
{ label: 'Names', to: 'https://names.celo.org', icon: ENS, hideInMiniPay: true },
Expand All @@ -30,6 +31,8 @@ export function NavBar({ collapsed }: { collapsed?: boolean }) {
const { address } = useAccount();
const trackEvent = useTrackEvent();
const isMiniPay = useIsMiniPay();
const { lockedBalance } = useLockedBalance(address);
const hasStaked = lockedBalance > 0n;
Comment on lines +34 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate locked-balance polling to MiniPay-only rendering

The navbar now calls useLockedBalance(address) unconditionally for every connected user, but hasStaked is only used for the MiniPay-specific governance filter. Because useLockedBalance enables periodic refetching (BALANCE_REFRESH_INTERVAL, 5s) and emits toast errors on failures, non-MiniPay users now incur continuous extra RPC reads and can see unrelated error toasts for a feature branch they never use.

Useful? React with 👍 / 👎.

Comment on lines +34 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compute MiniPay staking gate from signing account

The MiniPay Governance gate is based on useLockedBalance(address), which checks only the connected wallet, but governance eligibility in this codebase is resolved through vote-signer indirection (useVoteSignerToAccount in src/features/governance/hooks/useVotingStatus.ts and src/features/transactions/TransactionFlow.tsx). If a MiniPay wallet is a vote signer for another staked account, address can have 0n locked balance while the signing account still has voting power, so this nav filter hides Governance for a user who can otherwise participate.

Useful? React with 👍 / 👎.


const handleNavClick = useCallback(
(item: string) => {
Expand All @@ -43,6 +46,7 @@ export function NavBar({ collapsed }: { collapsed?: boolean }) {
<ul className="flex list-none items-center justify-center space-x-6 overflow-hidden">
{LINKS(!!address)
.filter((l) => !(isMiniPay && l.hideInMiniPay))
.filter((l) => !(isMiniPay && l.hideInMiniPayUntilStaked && !hasStaked))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate MiniPay governance on CELO stake, not stCELO balance

The new MiniPay Governance filter depends on hasStaked, but hasStaked is derived only from useStCELOBalance; MiniPay users are forced into CELO mode (src/utils/useStakingMode.tsx:23) and stake through the CELO flow, so they can complete staking while stCELOBalances.total remains 0n. In that path, Governance stays hidden indefinitely instead of reappearing after the user has staked.

Useful? React with 👍 / 👎.

.map((l) => {
const isSelected = l.to === pathname || (l.to !== '/' && pathname?.startsWith(l.to));

Expand Down Expand Up @@ -77,6 +81,8 @@ export function MobileNavDropdown({ className }: { className?: string }) {
const { address } = useAccount();
const trackEvent = useTrackEvent();
const isMiniPay = useIsMiniPay();
const { lockedBalance } = useLockedBalance(address);
const hasStaked = lockedBalance > 0n;

const handleNavClick = useCallback(
(item: string) => {
Expand All @@ -97,6 +103,7 @@ export function MobileNavDropdown({ className }: { className?: string }) {
menuClasses="space-y-8 py-6 px-8"
menuItems={LINKS(!!address)
.filter((l) => !(isMiniPay && l.hideInMiniPay))
.filter((l) => !(isMiniPay && l.hideInMiniPayUntilStaked && !hasStaked))
.map((l) => {
return (
<Link
Expand Down
3 changes: 2 additions & 1 deletion src/features/locking/useLockedStatus.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { lockedGoldABI } from '@celo/abis';
import { useQuery } from '@tanstack/react-query';
import { useToastError } from 'src/components/notifications/useToastError';
import { GCTime, StaleTime } from 'src/config/consts';
import { BALANCE_REFRESH_INTERVAL, GCTime, StaleTime } from 'src/config/consts';
import { Addresses } from 'src/config/contracts';
import { useAccountDetails } from 'src/features/account/hooks';
import { LockedStatus, PendingWithdrawal } from 'src/features/locking/types';
Expand All @@ -24,6 +24,7 @@ export function useLockedStatus(address?: Address) {
},
gcTime: GCTime.Short,
staleTime: StaleTime.Short,
refetchInterval: BALANCE_REFRESH_INTERVAL,
});

useToastError(error, 'Error fetching locked balances and withdrawals');
Expand Down
13 changes: 7 additions & 6 deletions src/features/transactions/TransactionFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export function TransactionFlow<FormDefaults extends {}>({
votingPower.isLoading
) {
Component = <SpinnerWithLabel className="py-20">Loading account data...</SpinnerWithLabel>;
} else if (confirmationDetails) {
Component = (
<TransactionConfirmation confirmation={confirmationDetails} closeModal={closeModal} />
);
} else if (!isRegistered && !isVoteSigner && !requiresStCelo && mode === 'CELO') {
Component = <AccountRegisterForm refetchAccountDetails={refetchAccountDetails} />;
} else if (
Expand All @@ -74,10 +78,11 @@ export function TransactionFlow<FormDefaults extends {}>({
!isVoteSigner &&
!willVoteAndHasVotingPower
) {
Component = <LockForm showTip={true} />;
header = 'Lock CELO';
Component = <LockForm showTip={true} onConfirmed={onConfirmed} />;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve original action after prerequisite lock completes

Passing onConfirmed into the prerequisite LockForm changes the flow for Stake, Delegate, Upvote, and Vote when the user has no locked CELO: after a successful lock, confirmationDetails is set and TransactionFlow switches to TransactionConfirmation (which closes/reloads) instead of returning to the originally requested form. This regresses the existing one-modal progression and forces users to restart the action (often losing context like proposal-specific voting intent).

Useful? React with 👍 / 👎.

} else if (requiresStCelo && stCELOBalances.total <= 0n) {
Component = <StakeStCeloForm showTip={true} />;
} else if (!confirmationDetails) {
} else {
const action = (defaultFormValues as any).action as string;
if (action) {
if (action === DelegateActionType.Transfer) {
Expand All @@ -92,10 +97,6 @@ export function TransactionFlow<FormDefaults extends {}>({
}

Component = <FormComponent defaultFormValues={defaultFormValues} onConfirmed={onConfirmed} />;
} else {
Component = (
<TransactionConfirmation confirmation={confirmationDetails} closeModal={closeModal} />
);
}

return (
Expand Down
Loading