# Passkey Flow (/docs/cookbook/rise-wallet-quickstart/passkey-flow)



## Implementing Passkey Flow

The passkey flow demonstrates transaction signing where users approve each transaction via a wallet popup.

### Create the Passkey Page

```typescript title="src/app/passkey/page.tsx"
"use client";

import { useEffect, useState } from "react";
import { useChainId, useConnection, useReadContract } from "wagmi";
import { createWalletClient, custom, encodeFunctionData, parseEther } from "viem";
import { Navbar } from "@/components/Navbar";
import {
  SIMPLE_TOKEN_ADDRESS,
  SIMPLE_TOKEN_ABI,
  COUNTER_ADDRESS,
  COUNTER_ABI,
} from "@/constants";

export default function PasskeyPage() {
  const [mounted, setMounted] = useState(false);
  const [txHash, setTxHash] = useState<string>();
  const [pendingAction, setPendingAction] = useState<string>();

  const { address, connector } = useConnection();
  const chainId = useChainId();

  // Read token balance
  const { data: balance, refetch: refetchBalance } = useReadContract({
    address: SIMPLE_TOKEN_ADDRESS,
    abi: SIMPLE_TOKEN_ABI,
    functionName: "balanceOf",
    args: address ? [address] : undefined,
    query: { enabled: !!address },
  });

  // Read counter value
  const { data: count, refetch: refetchCount } = useReadContract({
    address: COUNTER_ADDRESS,
    abi: COUNTER_ABI,
    functionName: "count",
    query: { enabled: !!address },
  });

  useEffect(() => {
    setMounted(true);
  }, []);

  const executeTransaction = async (
    action: string,
    contractAddress: string,
    data: string
  ) => {
    if (!connector?.provider || !address) return;

    try {
      setPendingAction(action);
      setTxHash(undefined);

      const walletClient = createWalletClient({
        account: address,
        chain: { id: chainId } as any,
        transport: custom(connector.provider),
      });

      const hash = await walletClient.sendCallsSync({
        account: address,
        calls: [{ to: contractAddress as `0x${string}`, data: data as `0x${string}` }],
      });

      setTxHash(hash);

      // Refresh contract state
      refetchBalance();
      refetchCount();
    } catch (error) {
      console.error(`${action} failed:`, error);
    } finally {
      setPendingAction(undefined);
    }
  };

  const mintTokens = async () => {
    if (!address) return;
    const data = encodeFunctionData({
      abi: SIMPLE_TOKEN_ABI,
      functionName: "mint",
      args: [address, parseEther("1000")],
    });
    await executeTransaction("Minting", SIMPLE_TOKEN_ADDRESS, data);
  };

  const spendTokens = async () => {
    if (!address) return;
    const data = encodeFunctionData({
      abi: SIMPLE_TOKEN_ABI,
      functionName: "transfer",
      args: ["0x0000000000000000000000000000000000000000", parseEther("5")],
    });
    await executeTransaction("Spending", SIMPLE_TOKEN_ADDRESS, data);
  };

  const incrementCounter = async () => {
    const data = encodeFunctionData({
      abi: COUNTER_ABI,
      functionName: "increment",
    });
    await executeTransaction("Incrementing", COUNTER_ADDRESS, data);
  };

  if (!mounted) return null;

  if (!address) {
    return (
      <div className="min-h-screen bg-black text-white">
        <Navbar />
        <div className="max-w-4xl mx-auto px-4 py-16 text-center">
          <p className="text-xl text-gray-400">
            Connect your wallet to use passkey flow
          </p>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-black text-white">
      <Navbar />
      <div className="max-w-4xl mx-auto px-4 py-16">
        <h1 className="text-4xl font-bold mb-8 text-green-500">
          Passkey Flow
        </h1>
        <p className="text-gray-400 mb-8">
          Each transaction requires wallet approval via popup
        </p>

        <div className="grid md:grid-cols-2 gap-6 mb-8">
          <div className="p-6 bg-gray-900 rounded-xl border border-gray-800">
            <h3 className="text-sm text-gray-400 mb-2">STK Balance</h3>
            <p className="text-3xl font-bold text-green-500">
              {balance ? (Number(balance) / 1e18).toLocaleString() : "0"}
            </p>
          </div>

          <div className="p-6 bg-gray-900 rounded-xl border border-gray-800">
            <h3 className="text-sm text-gray-400 mb-2">Counter Value</h3>
            <p className="text-3xl font-bold text-green-500">
              {count?.toString() || "0"}
            </p>
          </div>
        </div>

        <div className="space-y-4">
          <button
            onClick={mintTokens}
            disabled={!!pendingAction}
            className="w-full px-6 py-4 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {pendingAction === "Minting" ? "Minting..." : "Mint 1000 STK"}
          </button>

          <button
            onClick={spendTokens}
            disabled={!!pendingAction}
            className="w-full px-6 py-4 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {pendingAction === "Spending" ? "Spending..." : "Spend 5 STK"}
          </button>

          <button
            onClick={incrementCounter}
            disabled={!!pendingAction}
            className="w-full px-6 py-4 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {pendingAction === "Incrementing"
              ? "Incrementing..."
              : "Increment Counter"}
          </button>
        </div>

        {txHash && (
          <div className="mt-8 p-4 bg-gray-900 rounded-lg border border-green-500">
            <p className="text-sm text-gray-400 mb-2">Transaction Hash:</p>
            <a
              href={`https://testnet.risescan.com/tx/${txHash}`}
              target="_blank"
              rel="noopener noreferrer"
              className="text-green-500 hover:underline break-all"
            >
              {txHash}
            </a>
          </div>
        )}
      </div>
    </div>
  );
}
```

## How It Works

**Transaction Flow:**

1. User clicks an action button (Mint, Spend, Increment)
2. `encodeFunctionData()` creates the contract call data
3. `walletClient.sendCallsSync()` triggers the wallet popup
4. User approves the transaction in the popup
5. Wallet broadcasts the transaction and polls for confirmation
6. Transaction hash is displayed with explorer link
7. Contract state is refreshed automatically

When a user triggers a transaction, they'll see the RISE Wallet popup:

<img alt="RISE Wallet Passkey Approval" src={__img0} placeholder="blur" />

Now move on to the [Session Key Flow](/docs/cookbook/rise-wallet-quickstart/session-key-flow) to implement background signing without popups.
