This guide covers integrating Sonar using the @echoxyz/sonar-react library.
All authentication and API calls happen client-side, with tokens stored in browser storage.
For apps with a backend, consider the Frontend with Backend approach for improved security.
Sonar uses OAuth to allow users to authenticate 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.
Copy
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.
Copy
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> );}
You can fetch the state of a user’s Sonar entities with the useSonarEntities hook.
Copy
import { useSonarEntities } from "@echoxyz/sonar-react";const SonarEntityPanel = () => { const { authenticated, loading, entities, error } = useSonarEntities({ // from the sale integration settings on the Echo founder dashboard saleUUID: "YOUR_SALE_UUID", }); if (!authenticated) { return <p>Connect your Sonar account to continue</p>; } if (loading) { return <p>Loading...</p>; } if (error) { return <p>Error: {error.message}</p>; } if (!entities || entities.length === 0) { return ( <p>No entity found. Continue your setup{" "} on <a href="https://app.echo.xyz/sonar"> Sonar</a> to continue.</p> ); } return ( <div> {entities.map((entity) => ( <div key={entity.EntityID}> <span>{entity.Label}</span> <span>{entity.EntitySetupState}</span> <span>{entity.EntitySaleEligibility}</span> </div> ))} </div> );};
Reference docs for the useSonarEntity hook.See the example app EntityStateDescription
for more detail on how to interpret the state and display it to the user.
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.
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:
Use the generatePurchasePermit function to fetch a signed purchase permit from Sonar
Construct a transaction including the purchase permit and have the user sign it
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.
If you are using the generic SettlementSale contract,
you can find the ABI here.Then, let’s create a hook for managing the transaction lifecycle.
Copy
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 = () => { 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; }) => { if (!("MinPrice" in purchasePermitResp.PermitJSON)) { throw new Error("Invalid purchase permit response"); } const permit = purchasePermitResp.PermitJSON; // Your args might be different if you are not using the generic SettlementSale contract. // But the format of the purchase permit should be the same. const commitmentArgs = [ "PAYMENT_TOKEN_ADDRESS", { amount: amount, price: 0n, lockup: false }, { saleSpecificEntityID: permit.SaleSpecificEntityID, saleUUID: permit.SaleUUID, wallet: permit.Wallet, expiresAt: BigInt(permit.ExpiresAt), minAmount: BigInt(permit.MinAmount), maxAmount: BigInt(permit.MaxAmount), minPrice: BigInt(permit.MinPrice), maxPrice: BigInt(permit.MaxPrice), opensAt: BigInt(permit.OpensAt), closesAt: BigInt(permit.ClosesAt), payload: permit.Payload, }, purchasePermitResp.Signature, ] as const; // Assumes that the sender has already approved the sale contract to spend the token. // Note that the SettlementSale contract also provides the option of committing with an ERC20 permit (replaceBidWithPermit) const { request } = await simulateContract(config, { address: "YOUR_SALE_CONTRACT_ADDRESS", abi: YourSaleContractABI, functionName: "YOUR_PURCHASE_FUNCTION_NAME", // e.g. replaceBidWithApproval for the SettlementSale contract args: commitmentArgs, }); 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.
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 custom contracts guide for more details.
This is an optional feature for retrieving users past Echo private group investments.
Once authenticated, you can retrieve a user’s investment history using the useEntityInvestmentHistory hook.This shows all deals they’ve previously participated in, which can be useful if you are intending to give reputational boost/extra points if they have invested in a particulat deal.Reference docs for the useEntityInvestmentHistory hook.