5. Privacy Data Permissions

Configure the onboarding flow to ask for permissions to share privacy data with your dApp.

The privacy permissions flow easily allows users to consent to sharing their privacy data with your dApp in a few simple, secure steps.

When a user clicks to "Allow" Privacy Permissions, a request for a signature is presented, which the user must sign with their connected wallet in order to authorize consent for Privacy Data sharing.

In order to use this feature, application owners must have a working implementation of the Quadrata Client. A full example and integration steps can be found here: 6. Full Example

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.

To enable the Privacy Permission flow in your dApp, you need to implement the following steps:

import {
    ...
    PrivacyConsentScopeParamKey,
    PrivacyConsentScopeParams
} from '@quadrata/client-react';

2. Set the Privacy Permissions requested

Set the privacyScopes that will be requested from the user for consent. These need to be passed into QuadClient as a component prop.

const privacyScopes: PrivacyConsentScopeParamKey[] = [
    PrivacyConsentScopeParams.ADR,
    PrivacyConsentScopeParams.DOB,
    PrivacyConsentScopeParams.EM,
    PrivacyConsentScopeParams.FN,
    PrivacyConsentScopeParams.LN
];

Adding this property to your component will enable the Privacy Permission flow and will prompt the onboarding individual for consent to share their Privacy Data.

You can find a full list of Privacy Permission Parameters that you can add to your privacyScopes on the "Request Privacy Data"."List of Privacy Data" page.

Add the state related code to your dApp to manage the privacy consent signature along with the normal onboarding signature.

const [signature, setSignature] = useState<string>();
const [signatureConsent, setSignatureConsent] = useState<string>();

Update the handleSign method to manage the user consent signature that will be available once the user signs, granting access to their Privacy Data.

A boolean flag is passed as the second attribute to the QuadClient onSign event handler. This boolean represents whether or not the signature is for privacy consent or not.

When your dApp receives the consent signature, it needs to be passed back in as a component prop to QuadClient. To do this, update the state so the component re-renders.

This works the same way for onboarding signatures to ensure wallet ownership

const handleSign = async (message: string, isConsent: boolean) => {
    // User clicked the 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);
        }
    }
};

4. Determine If Privacy Permissions Flow Is Needed

If a user has already allowed access to the Privacy Data that you are requesting, you may not want to put them into the onboarding flow a second time (unless there are unclaimed attributes).

To help you make this decision, the API Onboard Status endpoint has been modified to accept optional privacyScopes as a query parameter. If provided, the response payload will contain which permissions have already been allowed, and which permissions you need to ask the user for.

// Quadrata Client
import {
    QuadAttribute,
    PrivacyConsentScopeParamKey,
    PrivacyConsentScopeParams
} from '@quadrata/client-react';

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;
}

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 };
}

// Check which attributes to claim for a given wallet
const apiAttributesOnboardStatus = async () => {
    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) {
        throw new Error('Onboard status failed');
    }
    return (await response.json()) as AttributeOnboardStatusDto;
};

const response = await apiAttributesOnboardStatus();

// isConsentNeeded is a boolean indicating if the consent flow is needed
const { isConsentNeeded } = parseOnboardStatusResponseForClient(resp, isBypassMint);

let privacyScopesToRequest = [];
if (isConsentNeeded) {
    // if isConsentNeeded is true, all required privacy scopes should be passed in
    privacyScopesToRequest = requiredPrivacyScopes;
}

See the Full Example for a full integration example

Privacy Permission Integration Example

import {
    ...
    PrivacyConsentScopeParamKey,
    PrivacyConsentScopeParams
} from '@quadrata/client-react';

// QuadClient config
const quadConfig: QuadClientConfig = {
    _debug: true, // Set to 'false' for production environment
    apiUrl: process.env.NEXT_PUBLIC_QUADRATA_API_URL!,
    environment: QuadClientEnvironment.SANDBOX, // set to QuadClientEnvironment.PRODUCTION for production environment
    protocolName: 'NewCo', // Replace with your company name
};

// Privacy permissions being requested for user consent
// Update these to your dApp's requirements
// PrivacyConsentScopeParamKey[] is an array of available params that your dApp 
// is requesting from the user
const privacyScopes: PrivacyConsentScopeParamKey[] = [
    PrivacyConsentScopeParams.ADR,
    PrivacyConsentScopeParams.DOB,
    PrivacyConsentScopeParams.EM,
    PrivacyConsentScopeParams.FN,
    PrivacyConsentScopeParams.LN
];

// Component
export const MyComponent: React.FC<{ accessToken: string }> = ({ accessToken }) => {
    // State
    const [signature, setSignature] = useState<string>();
    const [signatureConsent, setSignatureConsent] = useState<string>();
    
    // Hooks
    // In this example we use rainbowkit and wagmi libraries to manage Web3 
    // connectivity. You might use any other library.
    const { address: account, isDisconnected } = useAccount();
      
    const handleSign = async (message: string, isConsent: boolean) => {
        // User clicked the sign or allow 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) {
                // Sets the user consent signature
                setSignatureConsent(signature);
            } else {
                // Sets the user wallet signature for normal Onboarding
                setSignature(signature);
            }
        }
    };
    
    // Requesting user PII
    return (
        <QuadClient
            ...
            accessToken={accessToken}
            account={account}
            config={quadConfig}
            onHide={onHide}
            onSign={handleSign}
            privacyScopes={privacyScopes || undefined}
            signature={siganture}
            signatureConsent={signatureConsent}
        >
            <CustomLoader />
        </QuadClient>
    );
};

Last updated