4. Full Example

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

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { parseUnits } from 'viem';

// wagmi
import {
  useAccount,
  useSignMessage,
  useSimulateContract,
  useWaitForTransactionReceipt,
  useWriteContract
} 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';
import { AttributeStatus, QuadrataOnApplicationEndCallback } from '@quadrata/core-react';

// NOTE: find contract addresses at https://docs.quadrata.com/integration/additional-information/smart-contracts
const QUAD_PASSPORT_ADDRESS = '0x185cc335175B1E7E29e04A321E1873932379a4a0'; // Testnet

const QUADRATA_API_URL = 'https://int.quadrata.com/api'; // sandbox api

export interface AttributeOnboardStatusDto {
    data: {
        type: 'attributes';
        onboardStatus:{
            [attributeName: string]: {
                status: AttributeStatus;
                onboardedAt?: number;
                mintedOnchain?: boolean;
            };
        };
        offeringStatus?: {
            [attributeName: string]: {
                status: AttributeStatus;
                verifiedAt?: number;
            };
        };
        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;
}

function apiFetchAttributesOnboardStatus(args: {
    accessToken: string;
    account: string;
    attributes: Array<QuadAttribute>;
    chainId?: number;
    privacyScopes?: Array<PrivacyConsentScopeParamKey>;
}): Promise<AttributeOnboardStatusDto> {
    const {
        accessToken,
        account,
        attributes,
        chainId,
        privacyScopes
    } = args;
    const attrQuery = attributes.map((attr) => attr.toLowerCase()).join(',');
    const privacyScopesQuery = privacyScopes ? privacyScopes?.join(',') : undefined;
    const queryStringParameters: Record<string, any> = {
        wallet: account,
        attributes: attrQuery
    };
    if (chainId) {
        queryStringParameters.chainId = chainId;
    }
    if (privacyScopesQuery) {
        queryStringParameters.privacyScopes = privacyScopesQuery;
    }
    const queryString = new URLSearchParams(queryStringParameters);
    return fetch(
        `${QUADRATA_API_URL}/v2/attributes/onboard_status?${queryString.toString()}`,
        {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${accessToken}`,
            },
            cache: 'no-cache',
        }
    ).then((response) => {;
        if (!response.ok) {
            throw new Error('Onboard status failed');
        }
        return response.json();
    }) as Promise<AttributeOnboardStatusDto>;
};

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 attributeName of attributesToAttest) {
            if (!attributesToClaim.includes(attributeName)) {
                attributesToClaim.push(attributeName);
            }
        }
    }
    return { attributesToClaim, isConsentNeeded };
}

// Component
export const Quadrata: React.FC<{ accessToken: string }> = ({
    accessToken
}) => {
    // Config options
    const bypassMint: boolean = false;
    const requiredAttributes = [QuadAttribute.DID, QuadAttribute.AML];
    const requiredPrivacyScopes = ['FN','LN','EM','DOB'] as Array<PrivacyConsentScopeParamKey>;

    // State
    const [attributesToClaim, setAttributesToClaim] = useState<QuadAttribute[] | undefined>(undefined);
    const [isError, setError] = useState(false);
    const [isLoading, setIsLoading] = useState(false);
    const [privacyScopes, setPrivacyScopes] = useState<Array<PrivacyConsentScopeParamKey> | undefined>(undefined);
    const [showModal, setShowModal] = useState<boolean>(false);
    const [signature, setSignature] = useState<string>();
    const [signatureConsent, setSignatureConsent] = useState<string>();
  
    // Minting
    const [mintComplete, setMintComplete] = useState(false);
    const [mintError, setMintError] = useState<string>();
    const [mintParams, setMintParams] = useState<QuadMintParamsBigNumbers>();

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

    const contractConfig = useMemo(() => {
        if (!mintParams) {
            return undefined;
        }
        return {
            abi: QUAD_PASSPORT_ABI,
            args: [mintParams.account, mintParams.params[0], mintParams.signaturesIssuers[0]],
            address: QUAD_PASSPORT_ADDRESS as `0x${string}`,
            value: (mintParams?.fee || parseUnits('0', 18)) as bigint, // note: assumes same as ether with 18 decimals
            functionName: 'setAttributesIssuer',
        };
    }, [mintParams]);

    useSimulateContract(contractConfig);

    const {
        data: transactionHash,
        writeContract,
        error: writeContractError
    } = useWriteContract();
    
    useWaitForTransactionReceipt({
        hash: transactionHash,
    });

    useEffect(() => {
        if (transactionHash) {
            // Setting mint to complete
            setMintComplete(true);
            // Resetting state
            setMintParams(undefined);
            setSignature(undefined);
        }
    }, [transactionHash]);
    
    useEffect(() => {
        if (writeContractError) {
            console.log('[Quadrata Integration]: Mint error: ', writeContractError);
            setMintError(writeContractError.message);
        }
    }, [writeContractError]);
    
    // Check which attributes to claim for a given wallet
    const { error: onboardStatusError, data: onboardStatusData } = useQuery({
        queryKey: ['QUAD_API_ONBOARD_STATUS', account, requiredAttributes, chainId, bypassMint, requiredPrivacyScopes],
        queryFn: () => {
            setIsLoading(true);
            return apiFetchAttributesOnboardStatus({
                accessToken,
                account,
                attributes: requiredAttributes,
                privacyScopes: requiredPrivacyScopes,
                chainId: !bypassMint && chainId ? chainId : undefined,
            }).catch((err) => {
                setError(true);
                throw err;
            });
        },
        enabled: !!account,
        gcTime: 0,
    });
    useEffect(() => {
        if (onboardStatusError) {
            console.error(`/onboard_status error : ${onboardStatusError}`);
            setError(true);
            setIsLoading(false);
            throw new Error(`/onboard_status error : ${onboardStatusError}`);
        }
        if (onboardStatusData) {
            const { attributesToClaim, isConsentNeeded } = parseOnboardStatusResponse(onboardStatusData, bypassMint);
            setAttributesToClaim(attributesToClaim);
            if (isConsentNeeded) {
                setPrivacyScopes(requiredPrivacyScopes);
            } else {
                setPrivacyScopes(undefined);
            }
            setError(false);
            setIsLoading(false);
        }
    }, [onboardStatusData, onboardStatusError]);

    // 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 = useCallback(() => {
        if (writeContract && contractConfig) {
            // Trying to mint passport
            writeContract(contractConfig);
        }
    }, [writeContract, contractConfig]);

    const handleMintParamsReady: QuadClientMintParamsReadyCallback = (mintParams) => {
        // Setting mint params to prepare write function
        setMintParams(mintParams);
    };

    // QuadrataReact should only be displayed if the wallet is connected
    if (!account) {
        return <p>Please connect your wallet</p>;
    }

    if (isError) {
        return <p>Error initializing onboard status</p>;
    }

    // QuadrataReact should only be displayed if attributes to claim or privacy scopes has data
    if (isConnecting || isLoading || !attributesToClaim) {
        return <p>Loading...</p>
    }
    if (attributesToClaim.length === 0 
        && (!privacyScopes || privacyScopes.length === 0)
    ) {
        return <p>Onboarding Completed</p>;
    }

    if (!showModal) {
        // Button to launch the quadrata application
        return (
            <button
                type="button"
                onClick={() => setShowModal(true)}
            >
                Launch Quadrata
            </button>
        );
    }

    // Quadrata all-in-one configuration
    const configShared: QuadrataReactConfigShared = {
        _debug: true,
        accessToken: accessToken,
        apiUrl: `${QUADRATA_API_URL}/v1`,
        children: undefined,
        className: 'custom__class__name',
        contactEmail: '[email protected]',
        darkMode: false,
        discordUrl: 'https://discord.gg/SR5Fc6BK',
        error: undefined,
        environment: QuadClientEnvironment.SANDBOX,
        protocolName: 'NewCo',  // Your company name goes here
        showSocialButtons: false,
    };
    const configUser: QuadrataReactConfigUser = {
        ...configShared,
        account: account || '',
        attributes: attributesToClaim,
        bypassMint: bypassMint,
        chainId: chainId,
        countriesUnavailable: undefined, // ['US','COUNTRY','CODE','LIST']
        mintComplete: mintComplete,
        mintError: mintError,
        offeringId: undefined, // set to unique id for accreditation re-attestation
        privacyScopes: privacyScopes,
        signature: signature,
        signatureConsent: signatureConsent,
        transactionHash: transactionHash,
    };

    // 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={() => setShowModal(false)}
                    onMintClick={handleMintClick}
                    onMintParamsReady={handleMintParamsReady}
                    onPageChange={handlePageChange}
                    onSign={handleSign}
                ></QuadrataReact>
            ) : (
                <></>
            )}
        </>
    );
};

Last updated