Skip to main content

Mobile Apps (SDK Reference)

This appendix contains the full SDK source used in the Mobile Apps (React Native Guide). Copy the files into src/sdk/ in your React Native project to replicate the behavior demonstrated in our setup guide. Each component is shown without its styles to make reuse easier, followed by the corresponding style modules.

Directory structure

src/sdk/
├── ConsentManager.ts
├── OptOutBanner.tsx
├── OptOutBanner.styles.ts
├── index.ts
├── types.ts
└── useConsent.ts

types.ts

src/sdk/types.ts
/**
* Polaris CMP SDK Types
*
* Types for the Consent Management Platform SDK
*/

/**
* SDK Configuration
*/
export interface SDKConfig {
/** API endpoint URL */
endpointUrl: string;
/** Provider type */
provider: 'shopify' | 'email';
/** Shop domain/identifier */
shop: string;
/** TrueVault privacy center ID */
privacyCenterId: string;
/** Shopify customer ID (for shopify provider) */
customerId?: string;
/** Customer email (for email provider) */
customerEmail?: string;
/** Optional client ID (UUID v4) */
clientId?: string;
}

/**
* Consent preferences matching the API shape
*/
export interface ConsentData {
consentAnalytics: boolean | null;
consentAdvertising: boolean | null;
consentPersonalization: boolean | null;
consentTargetedAdvertising: boolean | null;
optedOut: boolean | null;
}

/**
* API Response from GET /api/v1/cmp/consent
*/
export interface ConsentApiResponse {
jwt: string;
consentAnalytics: boolean | null;
consentAdvertising: boolean | null;
consentPersonalization: boolean | null;
consentTargetedAdvertising: boolean | null;
optedOut: boolean | null;
implicit: boolean | null;
isExisting: boolean;
/** Request ID if an opt-out was previously submitted */
submittedOptOutRequestId?: string;
}

/**
* API Response from POST /api/v1/cmp/consent
*/
export interface ConsentPostApiResponse {
/** Request ID if an opt-out was submitted */
submittedOptOutRequestId?: string;
}

/**
* API Request body for POST /api/v1/cmp/consent
*/
export interface ConsentApiRequest {
jwt: string;
consentAnalytics?: boolean;
consentAdvertising?: boolean;
consentPersonalization?: boolean;
consentTargetedAdvertising?: boolean;
optedOut?: boolean;
}

/**
* Full consent state including metadata
*/
export interface ConsentState {
hasConsent: boolean;
consent: ConsentData | null;
jwt: string | null;
implicit: boolean | null;
isLoading: boolean;
error: string | null;
config: SDKConfig;
/** Request ID if an opt-out was submitted */
submittedOptOutRequestId: string | null;
}

export type ConsentListener = (state: ConsentState) => void;

/**
* Configuration for OptOutBanner component
*/
export interface OptOutBannerConfig {
/** URL for the "Privacy Policy" link in the description text */
privacyPolicyUrl?: string;
/** Hide the prominent "Opt Out" button (keeps the link in the text) */
hideOptOutButton?: boolean;
}

/**
* Default SDK configuration
*/
export const DEFAULT_CONFIG: Partial<SDKConfig> = {
endpointUrl: 'https://consent.truevault.com/api/v1/cmp/consent',
provider: 'shopify',
shop: /* Replace with your Shopify store domain */ '',
privacyCenterId: /* Replace with your TrueVault privacy center ID */ ''
};

ConsentManager.ts

src/sdk/ConsentManager.ts
import {
ConsentData,
ConsentState,
ConsentListener,
ConsentApiResponse,
ConsentPostApiResponse,
ConsentApiRequest,
SDKConfig,
DEFAULT_CONFIG,
} from './types';

class ConsentManager {
private state: ConsentState = {
hasConsent: false,
consent: null,
jwt: null,
implicit: null,
isLoading: false,
error: null,
config: { ...DEFAULT_CONFIG },
submittedOptOutRequestId: null,
};

private listeners: Set<ConsentListener> = new Set();

private debug(action: string, data: unknown): void {
const timestamp = new Date().toISOString();
console.log(`\n[CMP SDK] ${timestamp}`);
console.log(`[CMP SDK] Action: ${action}`);
console.log(`[CMP SDK] Endpoint: ${this.state.config.endpointUrl}`);
console.log(`[CMP SDK] Data:`, JSON.stringify(data, null, 2));
console.log('');
}

updateConfig(config: Partial<SDKConfig>): void {
this.setState({
...this.state,
config: { ...this.state.config, ...config },
});
this.debug('CONFIG_UPDATED', this.state.config);
}

getConfig(): SDKConfig {
return { ...this.state.config };
}

private buildQueryParams(): URLSearchParams {
const { config } = this.state;
const params = new URLSearchParams();

params.append('provider', config.provider);
params.append('shop', config.shop);
params.append('privacy_center_id', config.privacyCenterId);

if (config.provider === 'shopify' && config.customerId) {
params.append('logged_in_customer_id', config.customerId);
} else if (config.provider === 'email' && config.customerEmail) {
params.append('customer_email', config.customerEmail);
}

if (config.clientId) {
params.append('client_id', config.clientId);
}

return params;
}

async initialize(): Promise<void> {
this.setState({ ...this.state, isLoading: true, error: null });

const params = this.buildQueryParams();
const url = `${this.state.config.endpointUrl}?${params.toString()}`;

this.debug('FETCH_CONSENT_REQUEST', {
url,
params: Object.fromEntries(params),
});

try {
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
},
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const data: ConsentApiResponse = await response.json();

this.debug('FETCH_CONSENT_RESPONSE', data);

const consent: ConsentData = {
consentAnalytics: data.consentAnalytics,
consentAdvertising: data.consentAdvertising,
consentPersonalization: data.consentPersonalization,
consentTargetedAdvertising: data.consentTargetedAdvertising,
optedOut: data.optedOut,
};

this.setState({
...this.state,
hasConsent: !data.implicit,
consent,
jwt: data.jwt,
implicit: data.implicit,
isLoading: false,
error: null,
submittedOptOutRequestId: data.submittedOptOutRequestId ?? null,
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Failed to fetch consent';

this.debug('FETCH_CONSENT_ERROR', { error: errorMessage });

this.setState({
...this.state,
isLoading: false,
error: errorMessage,
});
}
}

async saveConsent(consent: ConsentData): Promise<void> {
if (!this.state.jwt) {
await this.initialize();
}

if (!this.state.jwt) {
throw new Error('No JWT available. Please initialize the SDK first.');
}

this.setState({ ...this.state, isLoading: true, error: null });

const requestBody: ConsentApiRequest = {
jwt: this.state.jwt,
};

if (consent.consentAnalytics !== null) {
requestBody.consentAnalytics = consent.consentAnalytics;
}
if (consent.consentAdvertising !== null) {
requestBody.consentAdvertising = consent.consentAdvertising;
}
if (consent.consentPersonalization !== null) {
requestBody.consentPersonalization = consent.consentPersonalization;
}
if (consent.consentTargetedAdvertising !== null) {
requestBody.consentTargetedAdvertising = consent.consentTargetedAdvertising;
}
if (consent.optedOut !== null) {
requestBody.optedOut = consent.optedOut;
}

this.debug('SAVE_CONSENT_REQUEST', {
url: this.state.config.endpointUrl,
body: requestBody,
});

try {
const response = await fetch(this.state.config.endpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(requestBody),
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const data: ConsentPostApiResponse = await response.json();

this.debug('SAVE_CONSENT_RESPONSE', data);

this.setState({
...this.state,
hasConsent: true,
consent,
implicit: false,
isLoading: false,
error: null,
submittedOptOutRequestId:
data.submittedOptOutRequestId ?? this.state.submittedOptOutRequestId,
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Failed to save consent';

this.debug('SAVE_CONSENT_ERROR', { error: errorMessage });

this.setState({
...this.state,
isLoading: false,
error: errorMessage,
});
throw error;
}
}

getState(): ConsentState {
return { ...this.state };
}

subscribe(listener: ConsentListener): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}

private setState(newState: ConsentState): void {
this.state = newState;
this.notifyListeners();
}

private notifyListeners(): void {
this.listeners.forEach(listener => {
listener(this.getState());
});
}
}

export const consentManager = new ConsentManager();

useConsent.ts

src/sdk/useConsent.ts
import { useState, useEffect } from 'react';
import { consentManager } from './ConsentManager';
import { ConsentState } from './types';

export const useConsent = (): ConsentState => {
const [state, setState] = useState<ConsentState>(consentManager.getState());

useEffect(() => {
const unsubscribe = consentManager.subscribe(setState);
setState(consentManager.getState());
return unsubscribe;
}, []);

return state;
};

OptOutBanner.tsx

src/sdk/OptOutBanner.tsx
import React, { useEffect, useRef } from 'react';
import {
View,
Text,
TouchableOpacity,
Animated,
Modal,
Linking,
} from 'react-native';
import { OptOutBannerConfig } from './types';
import { styles } from './OptOutBanner.styles';

interface OptOutBannerProps {
visible: boolean;
onOptOut: () => void;
onAccept: () => void;
onDismiss?: () => void;
config?: OptOutBannerConfig;
isOptedOut?: boolean;
requestId?: string | null;
}

export const OptOutBanner: React.FC<OptOutBannerProps> = ({
visible,
onOptOut,
onAccept,
onDismiss,
config,
isOptedOut,
requestId,
}) => {
const slideAnim = useRef(new Animated.Value(600)).current;

useEffect(() => {
if (visible) {
Animated.spring(slideAnim, {
toValue: 0,
useNativeDriver: true,
tension: 50,
friction: 8,
}).start();
} else {
Animated.timing(slideAnim, {
toValue: 600,
duration: 200,
useNativeDriver: true,
}).start();
}
}, [visible, slideAnim]);

const handleOptOutLink = () => {
onOptOut();
};

const handlePrivacyPolicyLink = () => {
if (config?.privacyPolicyUrl) {
Linking.openURL(config.privacyPolicyUrl);
}
};

if (!visible) {
return null;
}

const renderOptedOutContent = () => (
<>
<Text style={styles.title}>You've Opted Out</Text>
<Text style={styles.description}>
Your opt-out request has been received.
{requestId && <Text> Your request ID is {requestId}.</Text>}{' '}
Thank you for taking the time to submit your request.
</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button} onPress={onDismiss ?? onAccept}>
<Text style={styles.buttonText}>Close</Text>
</TouchableOpacity>
</View>
</>
);

const renderDefaultContent = () => (
<>
<Text style={styles.title}>We Take Privacy Seriously</Text>
<Text style={styles.description}>
We share data with our advertising partners to improve your
experience in our app. You may{' '}
<Text style={styles.link} onPress={handleOptOutLink}>
Opt Out
</Text>{' '}
of this sharing. See our{' '}
<Text style={styles.link} onPress={handlePrivacyPolicyLink}>
Privacy Policy
</Text>{' '}
for more information.
</Text>
<View style={styles.buttonContainer}>
{!config?.hideOptOutButton && (
<TouchableOpacity style={styles.button} onPress={onOptOut}>
<Text style={styles.buttonText}>Opt Out</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.button} onPress={onAccept}>
<Text style={styles.buttonText}>Close</Text>
</TouchableOpacity>
</View>
</>
);

return (
<Modal transparent visible={visible} animationType="none">
<View style={styles.overlay}>
<Animated.View
style={[
styles.banner,
{
transform: [{ translateY: slideAnim }],
},
]}>
{isOptedOut ? renderOptedOutContent() : renderDefaultContent()}
</Animated.View>
</View>
</Modal>
);
};

Opt-Out banner styles

src/sdk/OptOutBanner.styles.ts
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
banner: {
backgroundColor: 'rgba(255, 255, 255, 0.80)',
borderTopLeftRadius: 15,
borderTopRightRadius: 15,
padding: 24,
paddingBottom: 40,
shadowColor: '#000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 5,
},
title: {
color: '#000',
textAlign: 'center',
fontSize: 16,
fontWeight: '500',
lineHeight: 16,
letterSpacing: 0.14,
marginBottom: 8,
},
description: {
color: '#000',
textAlign: 'center',
fontSize: 12,
lineHeight: 20,
},
link: {
color: '#5C6AC4',
textDecorationLine: 'underline',
},
buttonContainer: {
flexDirection: 'row',
gap: 12,
marginTop: 12,
},
button: {
flex: 1,
paddingVertical: 8,
borderRadius: 100,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#FFFFFF',
borderWidth: 1,
borderColor: '#D1CED9',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 2,
},
buttonText: {
fontSize: 14,
fontWeight: '600',
color: '#666666',
},
});

index.ts

src/sdk/index.ts
export { consentManager } from './ConsentManager';
export { OptOutBanner } from './OptOutBanner';
export { useConsent } from './useConsent';
export type {
ConsentData,
ConsentState,
ConsentListener,
SDKConfig,
ConsentApiResponse,
ConsentApiRequest,
OptOutBannerConfig,
} from './types';
export { DEFAULT_CONFIG } from './types';

With these files in place, you can import the SDK via import { consentManager, useConsent, OptOutBanner } from './sdk'; and follow the usage steps in the main Mobile Apps (React Native Guide).