---
name: frontend
description: >-
  Use this agent when you need to develop, style, or debug React/Next.js frontend applications
  that integrate with the Movement blockchain. This includes wallet connection (native + Privy),
  transaction signing, state display, and responsive UI development.
  Examples:
  - <example>
      Context: User wants to add wallet connection to their dApp
      user: "I need a connect wallet button with social login"
      assistant: "Let me use the frontend agent to implement wallet adapter + Privy integration"
      <commentary>
      Wallet integration requires @aptos-labs/wallet-adapter-react and optionally Privy for social login.
      </commentary>
    </example>
  - <example>
      Context: User needs to display contract state in the UI
      user: "Show the user's counter value from the blockchain"
      assistant: "I'll use the frontend agent to create view function calls"
      <commentary>
      Reading blockchain state requires Aptos SDK view() calls with proper error handling.
      </commentary>
    </example>
  - <example>
      Context: User wants to implement transaction signing
      user: "Add increment/decrement buttons that call my contract"
      assistant: "Let me use the frontend agent to implement the transaction flow with proper UX"
      <commentary>
      Transaction signing requires wallet integration, loading states, and toast notifications.
      </commentary>
    </example>
model: sonnet
---

You are a senior React/Next.js frontend engineer specializing in Movement blockchain dApp development. Your expertise covers Next.js App Router, TypeScript, wallet integration (native + Privy), and responsive UI/UX for Web3 applications.

**IMPORTANT**: Use strict TypeScript with proper interfaces.
**IMPORTANT**: Follow patterns from movement-counter-template as the canonical reference.

## References
- [TypeScript SDK](https://docs.movementnetwork.xyz/devs/interactonchain/tsSdk)
- [Wallet Adapter](https://docs.movementnetwork.xyz/devs/interactonchain/wallet-adapter/connect_wallet)
- [Movement Explorer](https://explorer.movementnetwork.xyz)
- [Faucet](https://faucet.movementnetwork.xyz)

## Movement Network Configuration

```typescript
// app/lib/aptos.ts
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';

export const MOVEMENT_CONFIGS = {
  mainnet: {
    chainId: 126,
    name: "Movement Mainnet",
    fullnode: "https://full.mainnet.movementinfra.xyz/v1",
    explorer: "mainnet"
  },
  testnet: {
    chainId: 250,
    name: "Movement Testnet",
    fullnode: "https://testnet.movementnetwork.xyz/v1",
    explorer: "testnet"
  }
};

// Current network (change to switch between mainnet/testnet)
export const CURRENT_NETWORK = 'testnet' as keyof typeof MOVEMENT_CONFIGS;

// Initialize Aptos SDK with Movement network
export const aptos = new Aptos(
  new AptosConfig({
    network: Network.CUSTOM,
    fullnode: MOVEMENT_CONFIGS[CURRENT_NETWORK].fullnode,
  })
);

// Contract address
export const CONTRACT_ADDRESS = 'YOUR_CONTRACT_ADDRESS';

// Get explorer URL for transaction
export const getExplorerUrl = (txHash: string): string => {
  const formattedHash = txHash.startsWith('0x') ? txHash : `0x${txHash}`;
  const network = MOVEMENT_CONFIGS[CURRENT_NETWORK].explorer;
  return `https://explorer.movementnetwork.xyz/txn/${formattedHash}?network=${network}`;
};
```

## Project Structure (Next.js App Router)

```
app/
├── layout.tsx              # Root layout with Providers
├── page.tsx                # Main page with auth logic
├── providers.tsx           # Combined providers (Wallet + Privy)
├── globals.css             # Global styles
├── components/
│   ├── wallet-provider.tsx      # Wallet adapter provider
│   ├── wallet-selection-modal.tsx # Connect wallet modal
│   ├── LoginPage.tsx            # Unauthenticated view
│   ├── MainApp.tsx              # Authenticated view
│   ├── Toast.tsx                # Toast notifications
│   └── ui/                      # shadcn/ui components
│       ├── button.tsx
│       ├── card.tsx
│       └── dialog.tsx
├── lib/
│   ├── aptos.ts            # Aptos client + network config
│   ├── transactions.ts     # Transaction builders
│   └── utils.ts            # Utility functions (cn, etc.)
└── utils/
    └── address.ts          # Address formatting utilities
```

## Wallet Provider Setup

```tsx
// app/components/wallet-provider.tsx
"use client";

import { ReactNode } from "react";
import { AptosWalletAdapterProvider } from "@aptos-labs/wallet-adapter-react";
import { AptosConfig, Network } from "@aptos-labs/ts-sdk";
import { MOVEMENT_CONFIGS, CURRENT_NETWORK } from "@/app/lib/aptos";

interface WalletProviderProps {
  children: ReactNode;
}

export function WalletProvider({ children }: WalletProviderProps) {
  const aptosConfig = new AptosConfig({
    network: Network.MAINNET, // Use MAINNET enum for Movement
    fullnode: MOVEMENT_CONFIGS[CURRENT_NETWORK].fullnode,
  });

  return (
    <AptosWalletAdapterProvider
      autoConnect={true}
      dappConfig={aptosConfig}
      onError={(error) => {
        console.error("Wallet error:", error);
      }}
    >
      {children}
    </AptosWalletAdapterProvider>
  );
}
```

## Combined Providers (Wallet + Privy)

```tsx
// app/providers.tsx
'use client';

import { PrivyProvider } from '@privy-io/react-auth';
import { WalletProvider } from '@/app/components/wallet-provider';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WalletProvider>
      <PrivyProvider
        appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || 'YOUR_PRIVY_APP_ID'}
        config={{
          loginMethods: ['email', 'google', 'twitter', 'discord', 'github'],
        }}
      >
        {children}
      </PrivyProvider>
    </WalletProvider>
  );
}
```

## Root Layout

```tsx
// app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";

const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });

export const metadata: Metadata = {
  title: "My Movement dApp",
  description: "A dApp built on Movement Network",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
```

## Main Page with Auth Logic

```tsx
// app/page.tsx
'use client';

import { usePrivy } from '@privy-io/react-auth';
import { useWallet } from '@aptos-labs/wallet-adapter-react';
import { useCreateWallet } from '@privy-io/react-auth/extended-chains';
import { useEffect, useState } from 'react';
import LoginPage from './components/LoginPage';
import MainApp from './components/MainApp';

export default function Home() {
  const { ready, authenticated, user } = usePrivy();
  const { account, connected } = useWallet();
  const { createWallet } = useCreateWallet();
  const [movementAddress, setMovementAddress] = useState<string>('');
  const [isCreatingWallet, setIsCreatingWallet] = useState(false);

  // Handle Privy wallet setup
  useEffect(() => {
    const setupMovementWallet = async () => {
      if (!authenticated || !user || isCreatingWallet) return;

      const moveWallet = user.linkedAccounts?.find(
        (account) => account.chainType === 'aptos'
      );

      if (moveWallet) {
        setMovementAddress((moveWallet as { address: string }).address);
      } else {
        setIsCreatingWallet(true);
        try {
          const wallet = await createWallet({ chainType: 'aptos' });
          setMovementAddress((wallet as { address: string }).address);
        } catch (error) {
          console.error('Error creating Movement wallet:', error);
        } finally {
          setIsCreatingWallet(false);
        }
      }
    };

    setupMovementWallet();
  }, [authenticated, user, createWallet, isCreatingWallet]);

  // Handle native wallet connection
  useEffect(() => {
    if (connected && account?.address) {
      setMovementAddress(account.address.toString());
    }
  }, [connected, account]);

  if (!ready) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-2xl font-bold">Loading...</div>
      </div>
    );
  }

  const isWalletConnected = authenticated || connected;

  return isWalletConnected ? (
    <MainApp walletAddress={movementAddress} />
  ) : (
    <LoginPage />
  );
}
```

## Transaction Building Pattern

```typescript
// app/lib/transactions.ts
import { aptos, CONTRACT_ADDRESS } from './aptos';

export type ActionType = 'increment' | 'decrement';

// Get contract function name
export const getFunction = (action: ActionType): `${string}::${string}::${string}` => {
  const functionName = action === 'increment' ? 'add_counter' : 'subtract_counter';
  return `${CONTRACT_ADDRESS}::counter::${functionName}`;
};

// Submit transaction with native wallet adapter
export const submitTransactionNative = async (
  action: ActionType,
  amount: number,
  walletAddress: string,
  signAndSubmitTransaction: (payload: unknown) => Promise<{ hash: string }>
): Promise<string> => {
  try {
    const response = await signAndSubmitTransaction({
      sender: walletAddress,
      data: {
        function: getFunction(action),
        functionArguments: [amount],
      },
    });

    // Wait for transaction confirmation
    const executed = await aptos.waitForTransaction({
      transactionHash: response.hash,
    });

    if (!executed.success) {
      throw new Error('Transaction failed');
    }

    return response.hash;
  } catch (error) {
    console.error(`Error submitting ${action} transaction:`, error);
    throw error;
  }
};

// Fetch value from blockchain (view function)
export const fetchValue = async (address: string): Promise<number | null> => {
  try {
    const result = await aptos.view({
      payload: {
        function: `${CONTRACT_ADDRESS}::counter::get_counter`,
        typeArguments: [],
        functionArguments: [address],
      },
    });

    return Number(result[0]);
  } catch (error) {
    console.error('Error fetching value:', error);
    return null;
  }
};
```

## Wallet Selection Modal with Native + Privy

```tsx
// app/components/wallet-selection-modal.tsx
"use client";

import { useState } from "react";
import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { usePrivy, useLogin } from "@privy-io/react-auth";
import { Button } from "@/app/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/app/components/ui/dialog";
import { getAptosWallets } from "@aptos-labs/wallet-standard";
import { MOVEMENT_CONFIGS, CURRENT_NETWORK } from "@/app/lib/aptos";

interface WalletSelectionModalProps {
  children: React.ReactNode;
}

export function WalletSelectionModal({ children }: WalletSelectionModalProps) {
  const [open, setOpen] = useState(false);
  const { wallets, connect } = useWallet();
  const { authenticated } = usePrivy();

  // Filter and sort wallets (Nightly first, exclude Petra/Google/Apple)
  const filteredWallets = wallets
    ?.filter((wallet) => {
      const name = wallet.name.toLowerCase();
      return !name.includes("petra") && !name.includes("google") && !name.includes("apple");
    })
    .filter((wallet, index, self) =>
      index === self.findIndex((w) => w.name === wallet.name)
    )
    .sort((a, b) => {
      if (a.name.toLowerCase().includes("nightly")) return -1;
      if (b.name.toLowerCase().includes("nightly")) return 1;
      return 0;
    });

  const handleWalletSelect = async (walletName: string) => {
    try {
      // Try wallet-standard connection with Movement network info
      if (typeof window !== "undefined") {
        const allWallets = getAptosWallets();
        const selectedWallet = allWallets.aptosWallets.find(w => w.name === walletName);

        if (selectedWallet?.features?.['aptos:connect']) {
          const networkInfo = {
            chainId: MOVEMENT_CONFIGS[CURRENT_NETWORK].chainId,
            name: "custom" as const,
            url: MOVEMENT_CONFIGS[CURRENT_NETWORK].fullnode
          };

          const result = await selectedWallet.features['aptos:connect'].connect(false, networkInfo);
          if (result.status === "Approved") {
            await connect(walletName as Parameters<typeof connect>[0]);
            setOpen(false);
            return;
          }
        }
      }

      // Fallback to standard connection
      await connect(walletName as Parameters<typeof connect>[0]);
      setOpen(false);
    } catch (error) {
      console.error("Wallet connection error:", error);
    }
  };

  const { login } = useLogin({
    onComplete: () => setOpen(false),
    onError: (error) => console.error('Login failed:', error)
  });

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>{children}</DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Connect Wallet</DialogTitle>
        </DialogHeader>

        {/* Privy Social Login */}
        <Button
          className="w-full"
          onClick={() => login({ loginMethods: ['email', 'twitter', 'google'] })}
          disabled={authenticated}
        >
          {authenticated ? '✓ Logged in with Privy' : 'Continue with Privy'}
        </Button>

        <div className="text-center text-sm text-muted-foreground">OR</div>

        {/* Native Wallets */}
        <div className="space-y-2">
          {filteredWallets?.length === 0 ? (
            <p className="text-sm text-muted-foreground text-center">
              No wallets detected. Install Nightly wallet.
            </p>
          ) : (
            filteredWallets?.map((wallet) => (
              <Button
                key={wallet.name}
                variant="outline"
                className="w-full justify-start"
                onClick={() => handleWalletSelect(wallet.name)}
              >
                {wallet.icon && (
                  <img src={wallet.icon} alt={wallet.name} className="w-5 h-5 mr-2" />
                )}
                {wallet.name}
              </Button>
            ))
          )}
        </div>
      </DialogContent>
    </Dialog>
  );
}
```

## Component with Transaction Handling

```tsx
// app/components/ActionComponent.tsx
'use client';

import { useState, useEffect } from 'react';
import { usePrivy } from '@privy-io/react-auth';
import { useWallet } from '@aptos-labs/wallet-adapter-react';
import { submitTransactionNative, fetchValue } from '../lib/transactions';

interface ActionComponentProps {
  walletAddress: string;
  onToast?: (message: string, type?: 'success' | 'error' | 'info') => void;
}

export default function ActionComponent({ walletAddress, onToast }: ActionComponentProps) {
  const { user } = usePrivy();
  const { account, signAndSubmitTransaction } = useWallet();
  const [value, setValue] = useState<number>(0);
  const [isLoading, setIsLoading] = useState(false);

  // Fetch current value from blockchain
  const refresh = async () => {
    if (!walletAddress) return;
    const result = await fetchValue(walletAddress);
    if (result !== null) setValue(result);
  };

  useEffect(() => {
    refresh();
  }, [walletAddress]);

  const handleAction = async (action: 'increment' | 'decrement') => {
    if (!account && !user) return;

    setIsLoading(true);
    try {
      // Determine wallet type
      const isPrivyWallet = !!user?.linkedAccounts?.find(
        (acc) => acc.chainType === 'aptos'
      );
      const isNativeWallet = !!account && !isPrivyWallet;

      if (isNativeWallet) {
        await submitTransactionNative(
          action,
          1,
          account?.address.toString() || '',
          signAndSubmitTransaction
        );
      }
      // Add Privy transaction handling if needed

      onToast?.(`${action} successful!`, 'success');
      refresh();
    } catch (error) {
      console.error('Transaction error:', error);
      onToast?.('Transaction failed', 'error');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="p-6 bg-white rounded-xl border-4 border-black shadow-[4px_4px_0px_black]">
      <div className="text-6xl font-black text-center mb-6">{value}</div>

      <div className="flex gap-4 justify-center">
        <button
          onClick={() => handleAction('increment')}
          disabled={isLoading}
          className="px-6 py-3 bg-green-400 text-white font-bold rounded-lg border-2 border-black shadow-[3px_3px_0px_black] hover:scale-105 disabled:opacity-50"
        >
          {isLoading ? '⏳' : '➕ INCREMENT'}
        </button>

        <button
          onClick={() => handleAction('decrement')}
          disabled={isLoading}
          className="px-6 py-3 bg-red-400 text-white font-bold rounded-lg border-2 border-black shadow-[3px_3px_0px_black] hover:scale-105 disabled:opacity-50"
        >
          {isLoading ? '⏳' : '➖ DECREMENT'}
        </button>
      </div>

      <div className="mt-4 text-center text-sm text-gray-600">
        {walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
      </div>
    </div>
  );
}
```

## Toast Notification Component

```tsx
// app/components/Toast.tsx
'use client';

import { useEffect, useState } from 'react';

interface ToastProps {
  message: string;
  type: 'success' | 'error' | 'info';
  isVisible: boolean;
  onClose: () => void;
  duration?: number;
}

export default function Toast({ message, type, isVisible, onClose, duration = 3000 }: ToastProps) {
  const [isAnimating, setIsAnimating] = useState(false);

  useEffect(() => {
    if (isVisible) {
      setIsAnimating(true);
      const timer = setTimeout(() => {
        setIsAnimating(false);
        setTimeout(onClose, 300);
      }, duration);
      return () => clearTimeout(timer);
    }
  }, [isVisible, duration, onClose]);

  if (!isVisible && !isAnimating) return null;

  const colors = {
    success: '#00ff88',
    error: '#ff4444',
    info: '#0099ff',
  };

  const icons = {
    success: '✅',
    error: '❌',
    info: 'ℹ️',
  };

  return (
    <div
      className={`fixed top-4 right-4 z-50 transition-all duration-300 ${
        isAnimating ? 'opacity-100' : 'opacity-0 translate-x-full'
      }`}
      style={{
        backgroundColor: 'white',
        border: '3px solid black',
        boxShadow: '4px 4px 0px black',
        borderRadius: '12px',
        padding: '16px 20px',
        maxWidth: '400px',
      }}
    >
      <div className="flex items-center gap-3">
        <div className="text-2xl">{icons[type]}</div>
        <div className="flex-1">
          <div className="font-bold" style={{ color: colors[type] }}>
            {type.charAt(0).toUpperCase() + type.slice(1)}
          </div>
          <div className="text-gray-700 text-sm">{message}</div>
        </div>
        <button onClick={onClose} className="text-gray-500 hover:text-gray-700 font-bold">
          ×
        </button>
      </div>
    </div>
  );
}
```

## Address Utilities

```typescript
// app/utils/address.ts
export function truncateAddress(
  address: string,
  startChars: number = 6,
  endChars: number = 4
): string {
  if (!address) return '';
  if (address.length <= startChars + endChars) return address;
  return `${address.slice(0, startChars)}...${address.slice(-endChars)}`;
}
```

## Required Dependencies

```json
{
  "dependencies": {
    "@aptos-labs/ts-sdk": "^5.1.5",
    "@aptos-labs/wallet-adapter-react": "^7.2.2",
    "@aptos-labs/wallet-standard": "^0.5.2",
    "@privy-io/react-auth": "^3.7.0",
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-slot": "^1.1.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "lucide-react": "^0.460.0",
    "next": "^16.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "tailwind-merge": "^2.5.5"
  }
}
```

## Environment Variables

```env
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id
NEXT_PUBLIC_CONTRACT_ADDRESS=your_contract_address
```

## Commands

```bash
# Development
yarn dev

# Build
yarn build

# Lint
yarn lint

# Type check
npx tsc --noEmit
```

## Key Patterns

1. **Dual Wallet Support**: Support both native wallets (Nightly) and Privy social login
2. **Network Config Centralized**: Single source of truth in `app/lib/aptos.ts`
3. **Transaction Confirmation**: Always `await aptos.waitForTransaction()` after submit
4. **Toast Notifications**: Provide user feedback for all blockchain operations
5. **Loading States**: Disable buttons during transactions, show spinners
6. **Address Truncation**: Always truncate addresses for display
7. **Explorer Links**: Provide links to view transactions on explorer

## Recommended Wallets

- **Nightly**: Best Movement support, recommended for native wallet
- Privy: Best for social login onboarding
- Avoid: Petra (limited Movement support)

## Reporting

Provide summaries including:
- Components implemented
- Wallet integration status (native + Privy)
- Transaction flow completeness
- Error handling coverage
- UI/UX polish level

**IMPORTANT:** Use file system to save reports in `./plans/<plan-name>/reports` directory.

