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.
Anchor to How it worksHow it works
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.
Anchor to RequirementsRequirements
- Checkout Kit installed in your app.
- Storefront API access configured for your app.
- Storefront API version
2025-10or later, which is required forvisitorConsenton the@inContextdirective.
Anchor to Step 1: Collect visitor consentStep 1: Collect visitor consent
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.
Anchor to Use platform-native consent collectionUse platform-native consent collection
Collect consent using each platform's built-in framework:
Platform-native consent collection
Swift
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)
}Kotlin
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
)React Native
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);
};Anchor to Custom consent collectionCustom consent collection
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
Swift
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()
}
}Kotlin
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
}
}React Native
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>
);
}Anchor to Step 2: Pass consent in the cart mutationStep 2: Pass consent in the cart mutation
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
Pass the cart line items as variables:
Variables
The response includes a checkoutUrl with a _cs parameter that contains the encoded consent. Pass this URL to Checkout Kit:
Example response
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:
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.
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
Swift
import ShopifyCheckoutSheetKit
func presentCheckout(with checkoutUrl: URL) {
ShopifyCheckoutSheetKit.present(
checkout: checkoutUrl,
from: self,
delegate: self
)
}Kotlin
import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit
fun presentCheckout(checkoutUrl: String) {
ShopifyCheckoutSheetKit.present(checkoutUrl, this, checkoutEventProcessor)
}React Native
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.
Anchor to Preloaded checkoutsPreloaded checkouts
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.