4. 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/wrapper-react NPM package.

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,
    PageKyb,
    PrivacyConsentScopeEnglishMap,
    PrivacyConsentScopeParamKey,
    QuadAttribute,
    QuadClientEnvironment,
    QuadClientMintParamsReadyCallback,
    QuadMintParamsBigNumbers,
    QuadSupportedChainId,
    QuadrataReact,
    QuadrataReactConfigShared,
    QuadrataReactConfigUser,
} from '@quadrata/quadrata-react';

const QUAD_PASSPORT_ADDRESS = "0x185cc335175B1E7E29e04A321E1873932379a4a0"; // Testnet

export interface AttributeOnboardStatusDto {
    data: {
        type: 'attributes';
        onboardStatus:{
            [attributeName: string]: {
                status: 'READY' | 'IN_REVIEW' | 'NA' | 'NOT_APPLICABLE';
                onboardedAt?: number;
                mintedOnchain?: boolean;
            };
        };
        privacyStatus?: {
            [privacyPermission: string]: {
                status: 'ALLOWED' | 'REVOKED' | 'NEEDS_CONSENT';
                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) {
    let isConsentNeeded = false;

    if (privacyStatus) {
        for (const privacyScopeKey in privacyStatus) {
            const { status } = privacyStatus[privacyScopeKey];
            if (status !== 'ALLOWED') {
                isConsentNeeded = true;
                break;
            }
        }
    }

    return isConsentNeeded;
}

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

    return { attributesToClaim, isConsentNeeded };
}

// Component
export const MyComponent: React.FC<{ accessToken: string }> = ({
  accessToken,
}) => {
    // State
    const [attributes, setAttributes] = useState<QuadAttribute[]>([]);
    const [missingAttributes] = useState<QuadAttribute[]>([]);
    const [attributesToClaim, setAttributesToClaim] = useState<QuadAttribute[]>([]);
    const [bypassMint, setBypassMint] = useState<boolean>(false);
    const [fetchOnboardStatus, setfetchOnboardStatus] = useState(false);
    const [isError, setError] = useState(false);
    const [isLoading, setIsLoading] = useState(false);
    const [showWrapper, setShowWrapper] = useState(false);
    const [signature, setSignature] = useState<string>();
    const [signatureConsent, setSignatureConsent] = useState<string>();
    const [privacyScopes, setPrivacyScopes] = useState<Array<PrivacyConsentScopeParamKey> | undefined>(undefined);
    
    // Minting state
    const [mintComplete, setMintComplete] = useState(false);
    const [mintError, setMintError] = useState<string>();
    const [mintParams, setMintParams] = useState<QuadMintParamsBigNumbers>();

    // Hooks
    const { signMessageAsync } = useSignMessage();
    const { chain: { id: chainId } = { id: 0 } } = useNetwork();
    const { address: account, isDisconnected } = useAccount();
    
    // Wagmi hooks
    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,
        onError: (err: any) => {
            console.log('[Next Demo]: Mint error: ', err);
            setMintError(err);
        },
    });
    
    let configShared: QuadrataReactConfigShared = {
        _debug: true,
        accessToken: accessToken,
        apiUrl: process.env.NEXT_PUBLIC_QUADRATA_API_URL!,
        children: undefined,
        className: 'custom__class__name',
        contactEmail: 'support@quadrata.com',
        darkMode: isDarkMode,
        discordUrl: 'https://discord.gg/SR5Fc6BK',
        error: undefined,
        environment: QuadClientEnvironment.SANDBOX,
        protocolName: 'NewCo',
        showSocialButtons: false,
    };
    
    let configUser: QuadrataReactConfigUser = {
        ...configShared,
        account: account || '',
        attributes: attributes.concat(attributesToClaim),
        chainId: chainId,
        mintComplete: mintComplete,
        mintError: mintError,
        privacyScopes: privacyScopes,
        signature: signature,
        signatureConsent: signatureConsent,
        transactionHash: data?.hash,
    };
    
    useWaitForTransaction({
        hash: data?.hash,
        onSuccess() {
            // Setting mint to complete
            setMintComplete(true);
            // Resetting state
            setMintParams(undefined);
            setSignature(undefined);
        },
    });
    
    // Check which attributes to claim for a given wallet
    const apiAttributesOnboardStatus = async () => {
        try {
            setisLoading(true);
            const attrQuery = missingAttributes.map((attr) => attr.toLowerCase()).join(',');
            const privacyScopesQuery = privacyScopes ? privacyScopes?.join(',') : undefined;
            const response = await fetch(
                `${process.env.NEXT_PUBLIC_QUADRATA_API_URL}/api/v2/attributes/onboard_status?wallet=${account}&chainId=${chainId}&attributes=${attrQuery}${privacyScopesQuery ? `&privacyScopes=${privacyScopesQuery}` : ''}`,
                {
                    method: 'GET',
                    headers: {
                        'Content-Type': 'application/json',
                        Authorization: `Bearer ${accessToken}`,
                    },
                },
            );
            if (!response.ok) {
                throw new Error('Onboard status failed');
            }
            return (await response.json()) as AttributeOnboardStatusDto;
        } catch (error) {
            setError(true);
            throw error;
        } finally {
            setIsLoading(false);
        }
    };

    useQuery('QUAD_API_ONBOARD_STATUS', () => apiAttributesOnboardStatus(), {
        enabled: fetchOnboardStatus && attributes.length == 0,
        onSuccess: (response: AttributeOnboardStatusDto) => {
            const { attributesToClaim, isConsentNeeded } = parseOnboardStatusResponse(response, bypassMint);

            setAttributesToClaim(attributesToClaim);
            if (isConsentNeeded) {
                setPrivacyScopes(privacyScopes);
            } else {
                setPrivacyScopes(undefined);
            }
            setError(false);
            setIsLoading(false);
            setfetchOnboardStatus(false);
        },
        onError: (err) => {
            console.error(`/onboard_status error : ${err}`);
            setError(true);
            setIsLoading(false);
            throw new Error(`/onboard_status error : ${err}`);
        },
        retry: false,
    });

  // Handlers
   const handleOnApplicationEnd: QuadrataOnApplicationEndCallback = ({ status, error }) => {
        console.log('handleOnApplicationEnd:::status:::', status);
        console.log('handleOnApplicationEnd:::error:::', error);
    };
  
    const handleSign = async (message: string, isConsent: boolean) => {
        // User clicked the initial sign button
        // Signing the message and updating state.
        // will automatically navigate to the next step upon signature update
        if (account) {
            const signature = await signMessageAsync({ message });
            if (isConsent) {
                setSignatureConsent(signature);
            } else {
                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 state if present
            setSignature(undefined);
            setMintParams(undefined);
        }
    };
  
    const handleMintClick = () => {
        // Trying to mint passport
        write?.();
    };

    const handleMintParamsReady: QuadClientMintParamsReadyCallback = (mintParams) => {
        // Setting mint params to prepare write function
        setMintParams(mintParams);
    };
  
  // Onboarding user and/or business
  // QuadrataReact should only be displayed when the configs are ready
    return (
        <>
            {accessToken && account ? (
                <QuadrataReact
                    configBusiness={configShared}
                    configUser={configUser}
                    onApplicationEnd={handleOnApplicationEnd}
                    onHide={() => setShowWrapper(false)}
                    onMintClick={handleMintClick}
                    onMintParamsReady={handleMintParamsReady}
                    onPageChange={handlePageChange}
                    onSign={handleSign}
                ></QuadrataReact>
            ) : (
                <></>
            )}
        </>
    );
};

Last updated