NERO
Guides

Login + Create a Wallet

Authenticate users via Google or GitHub OAuth and generate an MPC wallet — complete React walkthrough

Login + Create a Wallet

This guide walks through the full OAuth login flow: provider setup, social login buttons, callback handling, wallet generation, and displaying the wallet address.

Prerequisites

  • @nerochain/mpc-sdk installed (Installation)
  • A NERO project with an API key and OAuth providers configured (Dashboard)

Step 1 — Wrap your app with the provider

The NeroMpcAuthProvider initializes the SDK and makes all hooks available to child components. Place it at the root of your component tree.

import { NeroMpcAuthProvider } from "@nerochain/mpc-sdk/react";
import type { SDKConfig } from "@nerochain/mpc-sdk";

const config: SDKConfig = {
  backendUrl: import.meta.env.VITE_NERO_AUTH_URL,
  apiKey: import.meta.env.VITE_NERO_API_KEY,
  protocol: "dkls",
  uiConfig: {
    appName: "My App",
  },
};

function Root() {
  return (
    <NeroMpcAuthProvider config={config} autoConnect>
      <App />
    </NeroMpcAuthProvider>
  );
}

autoConnect tells the SDK to restore an existing session on page load. If a user previously logged in and their tokens are still valid, they skip straight to the authenticated state.

Step 2 — Add login buttons

useNeroConnect exposes a connect() method that redirects the browser to the OAuth provider's authorization page. Before redirecting, store the provider name so the callback handler knows which provider to use.

import { useNeroConnect } from "@nerochain/mpc-sdk/react";
import { useCallback } from "react";

type Provider = "google" | "github";

function LoginButtons() {
  const { connect } = useNeroConnect();

  const handleLogin = useCallback(
    async (provider: Provider) => {
      try {
        localStorage.setItem("nero:oauth_provider", provider);
        await connect(provider, window.location.origin + "/");
      } catch {
        localStorage.removeItem("nero:oauth_provider");
      }
    },
    [connect],
  );

  return (
    <div>
      <button onClick={() => handleLogin("google")}>Login with Google</button>
      <button onClick={() => handleLogin("github")}>Login with GitHub</button>
    </div>
  );
}

The second argument to connect() is the redirect URI. The OAuth provider will send the user back to this URL with code and state query parameters.

Step 3 — Handle the OAuth callback

After the provider redirects back, you need to detect the code and state URL parameters, exchange them for a session, and optionally trigger wallet generation.

Mount this component at the root level so it runs on every page load:

import {
  useNeroConnect,
  useNeroMpcAuth,
  useNeroWallet,
} from "@nerochain/mpc-sdk/react";
import { useEffect, useRef, useState } from "react";

function OAuthCallbackHandler() {
  const { sdk, isLoading } = useNeroMpcAuth();
  const { handleCallback } = useNeroConnect();
  const { generateWallet } = useNeroWallet();
  const [status, setStatus] = useState<string | null>(null);
  const processing = useRef(false);

  useEffect(() => {
    if (isLoading || !sdk || processing.current) return;

    const params = new URLSearchParams(window.location.search);
    const code = params.get("code");
    const state = params.get("state");
    if (!code || !state) return;

    processing.current = true;
    const provider = localStorage.getItem("nero:oauth_provider") ?? "google";
    localStorage.removeItem("nero:oauth_provider");

    window.history.replaceState({}, "", window.location.pathname);

    (async () => {
      try {
        setStatus("Completing login...");
        const result = await handleCallback(
          provider as Parameters<typeof handleCallback>[0],
          code,
          state,
          window.location.origin + "/",
        );

        if (result.requiresDKG) {
          setStatus("Generating wallet (DKG)...");
          await generateWallet();
        }

        setStatus(null);
      } catch (err) {
        setStatus(
          `Error: ${err instanceof Error ? err.message : String(err)}`,
        );
        setTimeout(() => setStatus(null), 8000);
      }
    })();
  }, [sdk, isLoading, handleCallback, generateWallet]);

  if (!status) return null;

  return (
    <div
      style={{
        position: "fixed",
        top: 16,
        left: "50%",
        transform: "translateX(-50%)",
        zIndex: 50,
        padding: "8px 16px",
        borderRadius: 8,
        background: status.startsWith("Error:") ? "#fee" : "#eef",
        color: status.startsWith("Error:") ? "#900" : "#006",
        fontSize: 14,
      }}
    >
      {status}
    </div>
  );
}

Key details:

  • processing ref prevents double-execution in React StrictMode
  • window.history.replaceState strips the OAuth params from the URL so a page refresh does not re-trigger the flow
  • requiresDKG is true for first-time users who need a new MPC wallet
  • The redirect URI passed to handleCallback must match the one used in connect()

Step 4 — Display wallet status and trigger generation

After authentication, check whether the user already has a wallet. If not, show a button to generate one. generateWallet() runs the DKLS distributed key generation protocol between the browser and the NERO backend.

import {
  useNeroConnect,
  useNeroUser,
  useNeroWallet,
} from "@nerochain/mpc-sdk/react";
import { useCallback, useState } from "react";

type Provider = "google" | "github";

function LoginPage() {
  const { connect } = useNeroConnect();
  const { user, isAuthenticated } = useNeroUser();
  const { wallet, hasWallet, generateWallet, isGenerating, error } =
    useNeroWallet();
  const [genResult, setGenResult] = useState<unknown>(null);

  const handleLogin = useCallback(
    async (provider: Provider) => {
      try {
        localStorage.setItem("nero:oauth_provider", provider);
        await connect(provider, window.location.origin + "/");
      } catch {
        localStorage.removeItem("nero:oauth_provider");
      }
    },
    [connect],
  );

  const handleGenerate = async () => {
    try {
      const result = await generateWallet();
      setGenResult(result);
    } catch {
      // error state is surfaced via the hook's `error` property
    }
  };

  if (!isAuthenticated) {
    return (
      <div>
        <p>Login with a social provider:</p>
        <button onClick={() => handleLogin("google")}>Google</button>
        <button onClick={() => handleLogin("github")}>GitHub</button>
      </div>
    );
  }

  return (
    <div>
      <p>Logged in as {user?.displayName ?? user?.email ?? user?.id}</p>

      {hasWallet ? (
        <p>Wallet: {wallet?.eoaAddress}</p>
      ) : (
        <button onClick={handleGenerate} disabled={isGenerating}>
          {isGenerating ? "Generating wallet (DKG)..." : "Generate Wallet"}
        </button>
      )}

      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}

Complete example

Putting it all together with the provider, callback handler, and login page:

import { NeroMpcAuthProvider } from "@nerochain/mpc-sdk/react";
import type { SDKConfig } from "@nerochain/mpc-sdk";
import {
  useNeroConnect,
  useNeroMpcAuth,
  useNeroUser,
  useNeroWallet,
} from "@nerochain/mpc-sdk/react";
import { useCallback, useEffect, useRef, useState } from "react";

const config: SDKConfig = {
  backendUrl: import.meta.env.VITE_NERO_AUTH_URL,
  apiKey: import.meta.env.VITE_NERO_API_KEY,
  protocol: "dkls",
};

type Provider = "google" | "github";

function OAuthCallbackHandler() {
  const { sdk, isLoading } = useNeroMpcAuth();
  const { handleCallback } = useNeroConnect();
  const { generateWallet } = useNeroWallet();
  const [status, setStatus] = useState<string | null>(null);
  const processing = useRef(false);

  useEffect(() => {
    if (isLoading || !sdk || processing.current) return;

    const params = new URLSearchParams(window.location.search);
    const code = params.get("code");
    const state = params.get("state");
    if (!code || !state) return;

    processing.current = true;
    const provider = localStorage.getItem("nero:oauth_provider") ?? "google";
    localStorage.removeItem("nero:oauth_provider");
    window.history.replaceState({}, "", window.location.pathname);

    (async () => {
      try {
        setStatus("Completing login...");
        const result = await handleCallback(
          provider as Parameters<typeof handleCallback>[0],
          code,
          state,
          window.location.origin + "/",
        );
        if (result.requiresDKG) {
          setStatus("Generating wallet (DKG)...");
          await generateWallet();
        }
        setStatus(null);
      } catch (err) {
        setStatus(
          `Error: ${err instanceof Error ? err.message : String(err)}`,
        );
        setTimeout(() => setStatus(null), 8000);
      }
    })();
  }, [sdk, isLoading, handleCallback, generateWallet]);

  if (!status) return null;

  return (
    <div
      style={{
        position: "fixed",
        top: 16,
        left: "50%",
        transform: "translateX(-50%)",
        padding: "8px 16px",
        borderRadius: 8,
        background: status.startsWith("Error:") ? "#fee" : "#eef",
      }}
    >
      {status}
    </div>
  );
}

function LoginPage() {
  const { connect } = useNeroConnect();
  const { user, isAuthenticated } = useNeroUser();
  const { wallet, hasWallet, generateWallet, isGenerating, error } =
    useNeroWallet();

  const handleLogin = useCallback(
    async (provider: Provider) => {
      try {
        localStorage.setItem("nero:oauth_provider", provider);
        await connect(provider, window.location.origin + "/");
      } catch {
        localStorage.removeItem("nero:oauth_provider");
      }
    },
    [connect],
  );

  const handleGenerate = async () => {
    try {
      await generateWallet();
    } catch {
      // error surfaced via hook
    }
  };

  if (!isAuthenticated) {
    return (
      <div>
        <h1>Welcome</h1>
        <button onClick={() => handleLogin("google")}>
          Login with Google
        </button>
        <button onClick={() => handleLogin("github")}>
          Login with GitHub
        </button>
      </div>
    );
  }

  return (
    <div>
      <p>Hello, {user?.displayName ?? user?.email}</p>
      {hasWallet ? (
        <p>Wallet: {wallet?.eoaAddress}</p>
      ) : (
        <button onClick={handleGenerate} disabled={isGenerating}>
          {isGenerating ? "Generating..." : "Create Wallet"}
        </button>
      )}
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}

export default function App() {
  return (
    <NeroMpcAuthProvider config={config} autoConnect>
      <OAuthCallbackHandler />
      <LoginPage />
    </NeroMpcAuthProvider>
  );
}

What the user sees

StateUI
Not logged inLogin buttons for Google / GitHub
OAuth redirect in progressBrowser navigates to provider consent screen
Callback processingToast: "Completing login..."
First-time user (DKG)Toast: "Generating wallet (DKG)..." (takes 2-5 seconds)
Authenticated with walletGreeting + wallet address
Error (invalid code, network)Red toast with error message, auto-dismisses after 8 seconds

Flow summary

  1. connect(provider, redirectUri) redirects to the OAuth provider
  2. Provider redirects back with code and state query parameters
  3. handleCallback(provider, code, state, redirectUri) exchanges the code for a session
  4. If result.requiresDKG is true, call generateWallet() to run distributed key generation
  5. The wallet address is available via useNeroWallet().wallet.eoaAddress

On this page