6. Full Example

In this example we use rainbowkit and wagmi libraries to manage Web3 connectivity, you might use any other library.

All of our UI libraries support both Javascript and Typescript environments.

Types, interfaces, helper functions, and object maps can be found in and imported from the @quadrata/core-react and @quadrata/client-react NPM packages.

import React, { useState } from "react";
import { useQuery } from "react-query";
import { parseUnits } from 'viem';

// wagmi
import {
  useAccount,
  useContractWrite,
  useNetwork,
  usePrepareContractWrite,
  useSignMessage,
  useWaitForTransaction,
} from "wagmi";

// Quadrata
import QUAD_PASSPORT_ABI from "@quadrata/contracts/abis/QuadPassport.json";
import {
  Page,
  PrivacyConsentScopeParamKey,
  PrivacyConsentScopeParams,
  QuadClient,
  QuadAttribute,
  QuadClientConfig,
  QuadClientEnvironment,
  QuadMintParamsBigNumbers,
  QuadClientMintParamsReadyCallback,
} from "@quadrata/client-react";
import { QuadrataOnApplicationEndCallback } from '@quadrata/core-react';
import { CustomLoader } from "../CustomLoader";

const quadConfig: QuadClientConfig = {
  environment: QuadClientEnvironment.SANDBOX,
  protocolName: "NewCo, Inc",
};

const QUAD_PASSPORT_ADDRESS = "0x185cc335175B1E7E29e04A321E1873932379a4a0"; // Testnet

export interface AttributeOnboardStatusDto {
    data: {
        type: 'attributes';
        onboardStatus:{
            [attributeName: string]: {
                status: string;
                onboardedAt?: number;
                mintedOnchain?: boolean;
            };
        };
        offeringStatus?: {
            [attributeName: string]: {
                status: string;
                verifiedAt?: number;
            };
        };
        privacyStatus?: {
            [privacyPermission: string]: {
                status: string;
                allowedAt?: number;
                revokedAt?: number;
                revokedReason?: string;
            };
        };
    };
}

function getAttributesToClaim(onboardStatus: any, isBypassMint: boolean) {
    const attributesToClaim = [];
    for (const attributeName in onboardStatus) {
        const { status, mintedOnchain } = onboardStatus[attributeName];
        if (
            (status !== AttributeStatus.READY && status !== 'NOT_APPLICABLE') ||
            (!isBypassMint && !mintedOnchain && status === AttributeStatus.READY)
        ) {
            attributesToClaim.push(attributeName as QuadAttribute);
        }
    }
    return attributesToClaim;
}

function checkConsentNeeded(privacyStatus: any) {
    if (privacyStatus) {
        for (const privacyScopeKey in privacyStatus) {
            const { status } = privacyStatus[privacyScopeKey];
            if (status !== 'ALLOWED') {
                // if any permission is not allowed, all of the desired 
                // permissions need to be requested again
                return true;
            }
        }
    }
    return false;
}

export function parseOnboardStatusResponse(
    resp: AttributeOnboardStatusDto,
    isBypassMint: boolean = false
) {
    const { data: { onboardStatus, privacyStatus, offeringStatus } } = resp;

    const attributesToClaim = getAttributesToClaim(onboardStatus, isBypassMint);
    const isConsentNeeded = checkConsentNeeded(privacyStatus);

    if (offeringStatus) {
        // merge attribute to attest from offeringStatus into attributesToClaim
        const attributesToAttest = getAttributesToClaim(offeringStatus, true);
        for (const name of attributesToAttest) {
            if (!attributesToClaim.includes(name)) {
                attributesToClaim.push(name);
            }
        }
    }

    return { attributesToClaim, isConsentNeeded };
}

// Component
export const MyComponent: React.FC<{ accessToken: string }> = ({
  accessToken,
}) => {
  // State
  const [isLoading, setIsLoading] = useState(false);
  const [signature, setSignature] = useState<string>();
  const [mintParams, setMintParams] = useState<QuadMintParamsBigNumbers>();
  const [mintComplete, setMintComplete] = useState(false);
  const [showClient, setShowClient] = useState(false);
  const [attributesToClaim, setAttributesToClaim] = useState<QuadAttribute[]>([]);
  const [privacyScopes, setPrivacyScopes] = useState<Array<PrivacyConsentScopeParamKey> | undefined>(undefined);
  const [isError, setError] = useState(false);

  // Hooks
  const { chain: { id: chainId } = { id: 0 } } = useNetwork();
  const { address: account, isConnecting, isDisconnected } = useAccount();
  const { signMessageAsync } = useSignMessage();

  // Required attributes for this protocol
  const requiredAttributes = [QuadAttribute.DID, QuadAttribute.AML];
  
  const requiredPrivacyScopes = [
    PrivacyConsentScopeParams.FN,
    PrivacyConsentScopeParams.LN,
    PrivacyConsentScopeParams.EM,
    PrivacyConsentScopeParams.DOB
  ];
  
  // set this to true if you want to bypass minting on chain
  const isBypassMint = false;
    
  // Check which attributes to claim for a given wallet
  const apiAttributesOnboardStatus = async () => {
    setIsLoading(true);
    const { NEXT_PUBLIC_QUADRATA_API_URL } = process.env;
    const attributes = requiredAttributes
      .map((attr) => attr.toLowerCase())
      .join(',');
    const privacyScopes = requiredPrivacyScopes.join(',');
    const url = `${NEXT_PUBLIC_QUADRATA_API_URL}/api/v2/attributes/onboard_status?wallet=${account}&chainId=${chainId}&attributes=${attributes}&privacyScopes=${privacyScopes}`;
    const headers = {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`,
    };
    
    const response = await fetch(url, { method: 'GET', headers });
    
    if (!response.ok) {
      setIsLoading(true);
      setError(true);
      throw new Error('Onboard status failed');
    }
    return (await response.json()) as AttributeOnboardStatusDto;
  };
    
  useQuery(
    'QUAD_API_ATTR_ONBOARD_STAUS', 
    () => apiAttributesOnboardStatus(), 
    {
      enabled: accessToken !== '',
      onSuccess: (resp: AttributeOnboardStatusDto) => {
        const { 
          attributesToClaim, 
          isConsentNeeded 
        } = parseOnboardStatusResponseForClient(resp, isBypassMint);
        
        setAttributesToClaim(attributesToClaim);
        setPrivacyScopes(
          isConsentNeeded
            ? requiredPrivacyScopes
            : []
        );
      },
      onError: (err) => {
          throw new Error(`/attributes/onboard_status error : ${err}`);
      },      
      retry: false,
  });

  // Claim Passport on-chain
  const { config } = usePrepareContractWrite({
    abi: QUAD_PASSPORT_ABI,
    args: mintParams
      ? [mintParams.account, mintParams.params[0], mintParams.signaturesIssuers[0]]
      : undefined,
    address: QUAD_PASSPORT_ADDRESS,
    enabled: Boolean(mintParams),
    value: (mintParams?.fee || parseUnits('0', 18)) as bigint, // note: assumes same as ether with 18 decimals
    functionName: 'setAttributesIssuer',
  });

  const { data, write } = useContractWrite(config);

  useWaitForTransaction({
    hash: data?.hash,
    onSuccess() {
      setMintComplete(true);
      setMintParams(undefined);
    },
  });

  // Handlers
  const handleOnApplicationEnd: QuadrataOnApplicationEndCallback = ({ status, error }) => {
    // Application has reached an end state: completion or error
    console.log('handleOnApplicationEnd:::status:::', status);
    console.log('handleOnApplicationEnd:::error:::', error);
  };

  const handleSign = async (message: string) => {
    // User clicked the initial sign button
    // Signing the message and updating state.
    // Will navigate to the next step upon signature update
    if (account) {
      const signature = await signMessageAsync({ message });
      setSignature(signature);
    }
  };

  const handlePageChange = (page: Page) => {
    if (page === Page.INTRO && signature) {
      // Intro page navigation will get triggered when a different wallet is detected,
      // Resetting previous signature if present.
      setSignature(undefined);
    }
  };
  
  const handleMintParamsReady: QuadClientMintParamsReadyCallback = (mintParams) => {
      // Setting mint params to prepare the write function
      setMintParams(mintParams);
  };

  const handleMintClick = async () => {
    // Prompting mint transaction
    write?.();
  };


  if (!account) {
    return <h1>Connect wallet</h1>;
  }

  if (isConnecting) {
    return <CustomLoader />;
  }

  if (!!attributesToClaim.length) {
    // User has all required attributes
    return <h1>Welcome!</h1>;
  }
  
  // User is missing at least one attribute,
  // Onboarding user
  return (
    <QuadClient
      attributes={attributesToClaim}
      accessToken={accessToken}
      account={account}
      chainId={chainId}
      config={quadConfig}     
      darkMode={false} 
      mintComplete={mintComplete}
      onApplicationEnd={handleOnApplicationEnd}
      onHide={() => setShowClient(false)}
      onMintClick={handleMintClick}
      onMintParamsReady={handleMintParamsReady}
      onPageChange={handlePageChange}
      onSign={handleSign}      
      signature={signature}
      transactionHash={data?.hash}
    >
      <CustomLoader />
    </QuadClient>
  );
};

Last updated