Skip to main content
You will need a way of getting the user’s wallet address, in these examples we use Wagmi. You can see a complete example Next.js application here.

Installation

npm install @echoxyz/sonar-core @echoxyz/sonar-react

Configuration

To use the @echo-xyz/sonar-react library, you will need to wrap your app in a SonarProvider.
import { SonarProvider } from "@echoxyz/sonar-react";

const sonarConfig = {
  // can be found in the sale integration settings on the Echo founder
  // dashboard
  clientUUID: "<YOUR_OAUTH_CLIENT_UUID>",
  // must match a redirect URI that you have configured against your
  // OAuth client in the sale settings
  redirectURI: "<YOUR_REDIRECT_URI>",
};

function Provider() {
  return (
    <SonarProvider config={sonarConfig}>
      {/* Your application */}
    </SonarProvider>
  );
}

Implementing the OAuth flow

Sonar uses the 2-legged OAuth flow to allow users to authenticate with sonar and authorize your application to access their data. You will need buttons that allow users to login and logout from Sonar. Logging in will redirect the user to the Sonar site to login. If they haven’t yet authorized your application, they will be prompted to do so.
import { useSonarAuth } from "@echoxyz/sonar-react";

function AuthButton() {
  const { login, authenticated, logout } = useSonarAuth();

  const onClick = () => {
    if (authenticated) {
      logout();
      return;
    }
    login();
  };

  return (
    <button onClick={onClick}>
      {authenticated ? "Disconnect from Sonar" : "Sign in with Sonar"}
    </button>
  );
}
Sonar will redirect the user back to your application at your redirectURI with code and state querystring parameters. You will need a page at your redirectURI that will handle the OAuth callback.
import { useEffect, useRef, useState } from "react";
import { useSonarAuth } from "@echoxyz/sonar-react";

export default function OAuthCallback() {
  const { authenticated, completeOAuth, ready } = useSonarAuth();
  const oauthCompletionTriggered = useRef(false);
  const [oauthError, setOAuthError] = useState<string | null>(null);

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

  // complete the oauth flow and exchange the code for an access token
  useEffect(() => {
    const processOAuthCallback = async () => {
      // the user is already authenticated, nothing to do
      if (!ready || authenticated || !code || !state) {
        return;
      }

      // ensuring the oauth completion isn't called multiple times
      // since subsequent ones are expected to fail
      if (oauthCompletionTriggered.current) {
        return;
      }
      oauthCompletionTriggered.current = true;

      try {
        await completeOAuth({ code, state });
      } catch (err) {
        setOAuthError(err instanceof Error ? err.message : null);
      }
    };

    processOAuthCallback();
  }, [authenticated, completeOAuth, code, ready, state]);

  // redirect away from this page once the OAuth flow is complete
  useEffect(() => {
    if (!ready || !authenticated) {
      return;
    }
    window.location.href = "/";
  }, [authenticated, ready]);

  // note you should also add a timeout to handle the case
  // where it takes too long for `authenticated` to be true
  if (oauthError) {
    return (
      <p>{oauthError}</p>
    );
  }

  return (
    <p>Connecting to Echo...</p>
  );
}

Fetching state of a user’s Sonar entity

You can fetch the state of a user’s Sonar entity with the useSonarEntity hook. The entity needs to be looked up by wallet address, so you will need a way of retrieving this from the user. In this example we use the wagmi library to get the address from the user’s connected wallet.
import { useAccount } from "wagmi";
import { useSonarEntity } from "@echoxyz/sonar-react";

const SonarEntityPanel = () => {
  const { address, isConnected } = useAccount();
  const { authenticated, loading, entity, error } = useSonarEntity({
    // from the sale integration settings on the Echo founder dashboard
    saleUUID: "YOUR_SALE_UUID",
    walletAddress: address,
  });

  if (!address || !authenticated) {
    return <p>Connect your wallet and Sonar account to continue</p>;
  }

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>Error: {error.message}</p>;
  }

  // in this case the user has authenticated with Sonar,
  // but has not yet linked the currently connected wallet
  // to any of their entities on the Sonar site
  if (!entity) {
    return (
      <p>No entity found for this wallet. Please link your wallet{" "}
      on <a href="https://app.echo.xyz/sonar"> Sonar</a> to continue.</p>
    );
  }

  return (
    <div>
      <span>{entity.Label}</span>
      <span>{entity.EntitySetupState}</span>
      <span>{entity.EntitySaleEligibility}</span>
    </div>
  );
};
Reference docs for the useSonarEntity hook. See the example app SonarEntity for more detail on how to interpret the state and display it to the user.

Running pre-purchase checks

Once the sale is live, you can use the useSonarPurchase hook to run pre-purchase checks. Reference docs for the useSonarPurchase hook. If the entity is not ready to purchase, the reason why will be returned in the failureReason field. If a liveness check is required, the livenessCheckURL field will be returned and you should redirect the user to this URL to complete the liveness check.
import { PrePurchaseFailureReason } from "@echoxyz/sonar-core";
import {
  useSonarPurchase,
} from "@echoxyz/sonar-react";

function PrePurchaseStatusPanel({
  entityUUID,
  walletAddress,
}: {
  entityUUID: string;
  walletAddress: `0x${string}`;
}) {
  const sonarPurchaser = useSonarPurchase({
    saleUUID: "YOUR_SALE_UUID",
    entityUUID,
    walletAddress,
  });

  if (sonarPurchaser.loading) {
    return <p>Loading...</p>;
  }

  if (sonarPurchaser.error) {
    return <p>Error: {sonarPurchaser.error.message}</p>;
  }

  return (
    <div>
      <span>{readinessCfg.description}</span>

      {sonarPurchaser.readyToPurchase && (
        <span>Ready to purchase!</span>
      )}

      {!sonarPurchaser.readyToPurchase &&
          sonarPurchaser.failureReason ===
            PrePurchaseFailureReason.REQUIRES_LIVENESS && (
            <a href={sonarPurchaser.livenessCheckURL} target="_blank">
              Complete liveness check to purchase
            </a>
          )}
    </div>
  );
}
See the example app PurchasePanel for more detail on how to interpret the state and display it to the user.

Generating a purchase permit to send to the sale contract

If the entity is ready to purchase and you are running an onchain sale, you will need to:
  1. Use the generatePurchasePermit function to fetch a signed purchase permit from Sonar
  2. Construct a transaction including the purchase permit and have the user sign it
  3. Send the transaction to the sale contract
In this example we are going to use the wagmi library. First, you will need to get a copy of the sale contract’s ABI and include it in your project. There is an example ABI in the example app. Then, let’s create a hook for managing the transaction lifecycle.
import { GeneratePurchasePermitResponse } from "@echoxyz/sonar-core";
import { useCallback, useState } from "react";
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { useConfig } from "wagmi";
import { simulateContract } from "wagmi/actions";
import { YourSaleContractABI } from "./YourSaleContractABI";

export const useSaleContract = (walletAddress: `0x${string}`) => {
  const { writeContractAsync } = useWriteContract();
  const config = useConfig();

  const [txHash, setTxHash] = useState<`0x${string}` | undefined>(undefined);

  // Track the state of the transaction receipt
  const {
    data: txReceipt,
    isFetching: awaitingTxReceipt,
    error: awaitingTxReceiptError,
  } = useWaitForTransactionReceipt({
    hash: txHash,
  });

  // This function is used to construct the transaction,
  // simulate it, and then submit it after the user has signed it
  const commitWithPermit = useCallback(
    async ({
      purchasePermitResp,
      amount,
    }: {
      purchasePermitResp: GeneratePurchasePermitResponse;
      amount: bigint;
    }) => {
      // This example shows how to pass through BasicPermit's,
      // but you can see how to use AllocationPermit's in the example app.
      const { request } = await simulateContract(config, {
        address: "YOUR_SALE_CONTRACT_ADDRESS",
        abi: YourSaleContractABI,
        functionName: "YOUR_PURCHASE_FUNCTION_NAME",
        args: [
          amount,
          {
            permit: {
              entityID: purchasePermitResp.PermitJSON.EntityID,
              saleUUID: purchasePermitResp.PermitJSON.SaleUUID,
              wallet: purchasePermitResp.PermitJSON.Wallet,
              expiresAt: BigInt(purchasePermitResp.PermitJSON.ExpiresAt),
              payload: purchasePermitResp.PermitJSON.Payload,
            },
          },
          purchasePermitResp.Signature,
        ] as const,
      });

      setTxHash(
        await writeContractAsync(request, {
          onError: (error: Error) => {
            throw error;
          },
        })
      );
    },
    [writeContractAsync, config]
  );

  return {
    commitWithPermit,
    awaitingTxReceipt,
    txReceipt,
    awaitingTxReceiptError,
  };
};
Then in a component, we can use this hook and call the returned commitWithPermit function as part of a button click handler.
import { GeneratePurchasePermitResponse } from "@echoxyz/sonar-core";
import { useState } from "react";
import { useSaleContract } from "./useSaleContract";

function PurchasePanel({
  walletAddress,
  amount,
  generatePurchasePermit, // From the useSonarPurchase hook
}: {
  walletAddress: `0x${string}`;
  amount: bigint;
  generatePurchasePermit: () => Promise<GeneratePurchasePermitResponse>;
}) {
  const {
    commitWithPermit,
    awaitingTxReceipt,
    txReceipt,
    awaitingTxReceiptError,
  } = useSaleContract(walletAddress);

  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | undefined>(undefined);

  const purchase = async () => {
    setLoading(true);
    setError(undefined);
    try {
      const purchasePermitResp = await generatePurchasePermit();
      await commitWithPermit({
        purchasePermitResp: purchasePermitResp,
        amount: amount,
      });
    } catch (error) {
      setError(error as Error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <button
        disabled={loading || awaitingTxReceipt}
        onClick={purchase}
      >
        {loading || awaitingTxReceipt ? "Loading..." : "Purchase"}
      </button>

      {awaitingTxReceipt && !txReceipt && (
        <p>Waiting for transaction receipt...</p>
      )}
      {txReceipt?.status === "success" && (
        <p>Purchase successful</p>
      )}
      {txReceipt?.status === "reverted" && (
        <p>Purchase reverted</p>
      )}
      {error && <p>{error.message}</p>}
      {awaitingTxReceiptError && (
        <p>{awaitingTxReceiptError.message}</p>
      )}
    </div>
  );
}
If you are implementing your own contract, then you must validate that the purchase permit has been signed by Sonar and that the permit data is valid. See the smart contracts integratino guide for more details.
I