Skip to main content

Privacy compliance

Privacy regulations like the General Data Protection Regulation (GDPR) and the California Consumer Privacy Act (CCPA) require consent before tracking visitors or collecting personal data. Checkout Kit passes visitor consent from your app to Shopify, which applies the right tracking and data collection rules during checkout. Collect consent in your app and pass it through the Storefront API.


When you create a cart, include a visitorConsent parameter in the Storefront API's @inContext directive. Shopify encodes the consent into the checkoutUrl and applies it during checkout.

The Storefront API's VisitorConsent input object has four fields:

  • analytics: Data collection to understand buyer behavior.
  • preferences: Storing visitor preferences for their shopping experience.
  • marketing: Promotional communications and advertising.
  • saleOfData: Sharing or selling personal data to third parties.

Each consent type is optional. If you omit a type, then Shopify treats it as not provided.


  • Checkout Kit installed in your app.
  • Storefront API access configured for your app.
  • Storefront API version 2025-10 or later, which is required for visitorConsent on the @inContext directive.

iOS provides App Tracking Transparency (ATT) and Android provides the User Messaging Platform (UMP). On React Native, libraries like react-native-tracking-transparency wrap ATT for iOS, but no comparable cross-platform wrapper exists for UMP on Android. For Android on React Native, use a custom consent UI instead. Use these frameworks to prompt buyers, then map their responses to Shopify's consent categories.

Collect consent using each platform's built-in framework:

Platform-native consent collection

import AppTrackingTransparency
import AdSupport

class ConsentManager {
func requestConsent() async -> VisitorConsent {
// Request iOS App Tracking Transparency permission
let status = await ATTrackingManager.requestTrackingAuthorization()
return mapATTToVisitorConsent(status: status)
}

private func mapATTToVisitorConsent(status: ATTrackingManager.AuthorizationStatus) -> VisitorConsent {
// Map ATT response to your specific consent requirements
// ATT covers cross-app tracking - you decide how this maps to your data practices

switch status {
case .authorized:
// Buyer allowed cross-app tracking
return VisitorConsent(
analytics: true, // Consider: Does this cover your analytics use?
preferences: true, // Consider: Are preferences affected by ATT?
marketing: true, // Consider: Does this cover your marketing practices?
saleOfData: true // Consider: Does this cover your data sharing?
)
case .denied, .restricted:
// Buyer denied cross-app tracking
return VisitorConsent(
analytics: true, // Consider: Can you still do first-party analytics?
preferences: true, // Consider: Are preferences still allowed?
marketing: false, // Consider: What marketing is still allowed?
saleOfData: false // Consider: What data sharing is affected?
)
case .notDetermined:
// Handle undetermined state based on your requirements
return getDefaultConsent()
@unknown default:
return getDefaultConsent()
}
}

private func getDefaultConsent() -> VisitorConsent {
// Define your default consent state
return VisitorConsent(
analytics: false,
preferences: true, // Usually considered essential
marketing: false,
saleOfData: false
)
}
}

struct VisitorConsent {
let analytics: Bool
let preferences: Bool
let marketing: Bool
let saleOfData: Bool

static let defaultConsent = VisitorConsent(
analytics: false,
preferences: true,
marketing: false,
saleOfData: false
)
}

// Usage example
let consentManager = ConsentManager()

func collectConsentAndCreateCart() async {
let consent = await consentManager.requestConsent()
createCartWithConsent(consent: consent)
}
import com.google.android.ump.*

class ConsentManager(private val context: Context) {

fun requestConsent(completion: (VisitorConsent) -> Unit) {
val consentInformation = UserMessagingPlatform.getConsentInformation(context)

consentInformation.requestConsentInfoUpdate(
ConsentRequestParameters.Builder().build(),
{
// Load consent form if needed
UserMessagingPlatform.loadConsentForm(context) { form, error ->
if (form != null) {
form.show(context as Activity) {
val consent = mapUMPToVisitorConsent(consentInformation)
completion(consent)
}
} else {
// Handle form load error
completion(getDefaultConsent())
}
}
},
{ error ->
// Handle consent update error
completion(getDefaultConsent())
}
)
}

private fun mapUMPToVisitorConsent(consentInfo: ConsentInformation): VisitorConsent {
// Map UMP response to your specific consent requirements
// UMP scope depends on what YOU configured in your AdMob setup

return when (consentInfo.consentStatus) {
ConsentInformation.ConsentStatus.OBTAINED -> {
// Buyer provided consent based on YOUR UMP configuration
VisitorConsent(
analytics = true, // Consider: What did you ask consent for?
preferences = true, // Consider: Did UMP cover preferences?
marketing = true, // Consider: What marketing was included?
saleOfData = true // Consider: Did you ask about data sales?
)
}
ConsentInformation.ConsentStatus.NOT_REQUIRED -> {
// Consent not required for buyer's location
VisitorConsent(
analytics = true,
preferences = true,
marketing = true,
saleOfData = true
)
}
else -> {
// Required but not obtained, or unknown status
getDefaultConsent()
}
}
}

private fun getDefaultConsent(): VisitorConsent {
return VisitorConsent(
analytics = false,
preferences = true, // Usually considered essential
marketing = false,
saleOfData = false
)
}
}

data class VisitorConsent(
val analytics: Boolean,
val preferences: Boolean,
val marketing: Boolean,
val saleOfData: Boolean
)
import { ATTrackingManager } from 'react-native-tracking-transparency';

class ConsentManager {

async requestConsent() {
try {
// Request iOS ATT permission
const trackingStatus = await ATTrackingManager.requestTrackingAuthorization();
return this.mapPlatformToVisitorConsent(trackingStatus);

} catch (error) {
console.error('Consent request failed:', error);
return this.getDefaultConsent();
}
}

mapPlatformToVisitorConsent(platformStatus) {
// Map platform response to your specific consent requirements
// Consider what each platform permission actually covers for YOUR app

switch (platformStatus) {
case 'authorized':
// Buyer allowed tracking
return {
analytics: true, // Consider: Your analytics practices
preferences: true, // Consider: Preference handling
marketing: true, // Consider: Your marketing scope
saleOfData: true // Consider: Your data sharing
};

case 'denied':
case 'restricted':
// Buyer denied or restricted tracking
return {
analytics: true, // Consider: First-party analytics
preferences: true, // Consider: Essential preferences
marketing: false, // Consider: Limited marketing
saleOfData: false // Consider: No data sharing
};

case 'notDetermined':
default:
return this.getDefaultConsent();
}
}

getDefaultConsent() {
// Define your app's default consent state
return {
analytics: false,
preferences: true, // Usually essential for app function
marketing: false,
saleOfData: false
};
}
}

// Usage example
const consentManager = new ConsentManager();

const collectConsentAndCreateCart = async () => {
const consent = await consentManager.requestConsent();
createCartWithConsent(consent);
};

Only implement custom consent UIs if platform-native solutions don't meet your specific requirements, for example:

  • Your app needs more granular consent options than platform frameworks provide.
  • You're targeting enterprise/B2B scenarios with specific compliance requirements.
  • Platform-native solutions don't cover your specific use case or jurisdiction.
  • You need to collect consent outside platform advertising frameworks.

Custom consent collection

struct CustomConsentView: View {
@State private var analyticsConsent = false
@State private var preferencesConsent = true // Often essential
@State private var marketingConsent = false
@State private var saleOfDataConsent = false

var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Privacy Preferences")
.font(.headline)

Toggle("Analytics - Help us improve our services", isOn: $analyticsConsent)
Toggle("Preferences - Remember your shopping preferences", isOn: $preferencesConsent)
Toggle("Marketing - Receive promotional offers", isOn: $marketingConsent)
Toggle("Sale of data - Allow sharing data with partners", isOn: $saleOfDataConsent)

Button("Continue to Checkout") {
let consent = VisitorConsent(
analytics: analyticsConsent,
preferences: preferencesConsent,
marketing: marketingConsent,
saleOfData: saleOfDataConsent
)
createCartWithConsent(consent: consent)
}
}
.padding()
}
}
class CustomConsentFragment : Fragment() {
private var analyticsConsent = false
private var preferencesConsent = true // Often essential
private var marketingConsent = false
private var saleOfDataConsent = false

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_custom_consent, container, false)

view.findViewById<CheckBox>(R.id.analytics_checkbox).apply {
setOnCheckedChangeListener { _, isChecked -> analyticsConsent = isChecked }
}

view.findViewById<CheckBox>(R.id.preferences_checkbox).apply {
isChecked = true // Often essential
setOnCheckedChangeListener { _, isChecked -> preferencesConsent = isChecked }
}

view.findViewById<CheckBox>(R.id.marketing_checkbox).apply {
setOnCheckedChangeListener { _, isChecked -> marketingConsent = isChecked }
}

view.findViewById<CheckBox>(R.id.sale_of_data_checkbox).apply {
setOnCheckedChangeListener { _, isChecked -> saleOfDataConsent = isChecked }
}

view.findViewById<Button>(R.id.continue_button).setOnClickListener {
val consent = VisitorConsent(analyticsConsent, preferencesConsent, marketingConsent, saleOfDataConsent)
createCartWithConsent(consent)
}

return view
}
}
import React, { useState } from 'react';
import { View, Text, Switch, Button } from 'react-native';

function CustomConsentCollection({ onConsentCollected }) {
const [analyticsConsent, setAnalyticsConsent] = useState(false);
const [preferencesConsent, setPreferencesConsent] = useState(true); // Often essential
const [marketingConsent, setMarketingConsent] = useState(false);
const [saleOfDataConsent, setSaleOfDataConsent] = useState(false);

const handleContinue = () => {
onConsentCollected({
analytics: analyticsConsent,
preferences: preferencesConsent,
marketing: marketingConsent,
saleOfData: saleOfDataConsent
});
};

return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Privacy Preferences</Text>

<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 16 }}>
<Switch value={analyticsConsent} onValueChange={setAnalyticsConsent} />
<Text>Analytics - Help us improve our services</Text>
</View>

<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 12 }}>
<Switch value={preferencesConsent} onValueChange={setPreferencesConsent} />
<Text>Preferences - Remember your shopping preferences</Text>
</View>

<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 12 }}>
<Switch value={marketingConsent} onValueChange={setMarketingConsent} />
<Text>Marketing - Receive promotional offers</Text>
</View>

<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 12 }}>
<Switch value={saleOfDataConsent} onValueChange={setSaleOfDataConsent} />
<Text>Sale of data - Allow sharing data with partners</Text>
</View>

<Button title="Continue to Checkout" onPress={handleContinue} />
</View>
);
}

Add the @inContext directive with visitorConsent to your cartCreate mutation. Set each consent type to true or false based on the consent you collected:

GraphQL mutation with consent

mutation CreateCartWithConsent($lines: [CartLineInput!]!)
@inContext(visitorConsent: {
analytics: true,
preferences: true,
marketing: false,
saleOfData: false
}) {
cartCreate(input: {lines: $lines}) {
cart {
id
checkoutUrl
}
userErrors {
field
message
}
}
}

Pass the cart line items as variables:

Variables

{
"lines": [
{
"merchandiseId": "gid://shopify/ProductVariant/12345",
"quantity": 1
}
]
}

The response includes a checkoutUrl with a _cs parameter that contains the encoded consent. Pass this URL to Checkout Kit:

Example response

{
"data": {
"cartCreate": {
"cart": {
"id": "gid://shopify/Cart/hWN1xFGGsrQ4tpR9nSGZtlsv",
"checkoutUrl": "https://{shop}.myshopify.com/cart/c/hWN1xFGGsrQ4tpR9nSGZtlsv?_cs=3AmPs."
},
"userErrors": []
}
}
}

Anchor to Step 3: Present checkoutStep 3: Present checkout

The checkoutUrl from Step 2 includes a _cs parameter that contains the encoded consent. Pass the full URL to Checkout Kit without modifying it:

Caution

Pass the full URL to Checkout Kit. Don't strip or modify the _cs parameter appended to checkoutUrl in Step 2. Shopify reads it to apply the buyer's consent choices during checkout.

Present checkout

import ShopifyCheckoutSheetKit

func presentCheckout(with checkoutUrl: URL) {
ShopifyCheckoutSheetKit.present(
checkout: checkoutUrl,
from: self,
delegate: self
)
}
import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit

fun presentCheckout(checkoutUrl: String) {
ShopifyCheckoutSheetKit.present(checkoutUrl, this, checkoutEventProcessor)
}
import {useShopifyCheckoutSheet} from '@shopify/checkout-sheet-kit';

function CheckoutButton({ checkoutUrl }) {
const shopifyCheckout = useShopifyCheckoutSheet();

const handlePress = () => {
shopifyCheckout.present(checkoutUrl);
};

return (
<Button title="Complete Checkout" onPress={handlePress} />
);
}

If a buyer updates their consent, then request a new checkoutUrl from the Storefront API before presenting checkout. Each URL encodes the preferences from the mutation that created it, so stale URLs won't reflect the change.


A preloaded checkoutUrl encodes the consent from the time you called preload(). If a buyer updates their preferences after preloading, the cached checkout won't reflect the change. Either collect consent before you preload, or call ShopifyCheckoutSheetKit.invalidate() and request a new URL when preferences change.

For more on preloading, see Preload checkout.



Was this page helpful?