Supporting SKAdNetwork
SKAdNetwork is Apple's privacy-focused attribution framework for iOS app install campaigns. The Singular React Native SDK enables SKAdNetwork by default in Managed Mode, where Singular automatically updates conversion values based on your configured conversion model in the dashboard.
No Additional Configuration Required: If you're using the latest React Native SDK, SKAdNetwork works out of the box. No code changes or additional settings are needed for basic functionality.
Understanding SKAN Modes
Managed Mode (Default)
In Managed Mode, Singular automatically handles conversion value updates based on the conversion model you configure in your dashboard. This is the recommended approach for most apps as it requires minimal code and provides optimal conversion tracking.
- Automatic updates: Singular manages all conversion value updates based on user events and your configured model.
- Dashboard configuration: Design your conversion model in the Singular dashboard without code changes.
- Optimization: Benefit from Singular's expertise in maximizing conversion value updates within Apple's constraints.
- 24-hour window management: Singular handles SKAdNetwork's 24-hour update window intelligently to maximize data collection.
Manual Mode (Advanced)
Manual Mode gives you complete control over conversion value updates, allowing you to implement custom logic for determining when and how to update SKAN conversion values. Use this mode only if you have specific requirements that Managed Mode cannot fulfill.
Advanced Feature: Manual Mode requires careful implementation and understanding of Apple's SKAdNetwork constraints, including conversion value update windows and limitations. Most apps should use Managed Mode.
Disabling SKAdNetwork Support
SKAdNetwork tracking is enabled by default. To disable it, set the
skAdNetworkEnabled configuration property to false when
building
your SingularConfig object.
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
const config: SingularConfig = {
apikey: 'API_KEY',
secret: 'SECRET',
skAdNetworkEnabled: false // Disable SKAdNetwork
};
import { SingularConfig } from 'singular-react-native';
const config = new SingularConfig('API_KEY', 'SECRET')
.withSkAdNetworkEnabled(false); // Disable SKAdNetwork
Configuring Manual Mode
To implement custom conversion value logic, enable Manual Mode and use the provided SDK methods to update and monitor conversion values throughout your app's lifecycle.
Enable Manual Mode
Set the manualSkanConversionManagement configuration property
to true when building your SingularConfig object to take control of conversion
value updates.
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
const config: SingularConfig = {
apikey: 'API_KEY',
secret: 'SECRET',
manualSkanConversionManagement: true // Enable manual SKAN conversion mode
};
import { SingularConfig } from 'singular-react-native';
const config = new SingularConfig('API_KEY', 'SECRET')
.withManualSkanConversionManagement(); // Enable manual mode
Important: Manual update methods only work when
manualSkanConversionManagement is enabled. If Managed
Mode
is active, manual updates will be ignored.
Update Conversion Value (SKAN 2.0-3.0)
Use the skanUpdateConversionValue method to manually update
the SKAdNetwork conversion value based on your custom logic. The conversion
value must be an integer between 0 and 63.
Method Signature
static skanUpdateConversionValue(conversionValue)
Usage Example
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
import { Platform } from 'react-native';
// User completed signup - update conversion value to 7
async function onUserSignUp() {
if (Platform.OS === 'ios') {
// Track the sign-up event
NativeSingular.event('SignUp');
// Update SKAN conversion value
const success = await NativeSingular.skanUpdateConversionValue(7);
if (success) {
console.log('Conversion value updated successfully');
} else {
console.warn('Failed to update conversion value');
}
}
}
// User completed purchase - update based on purchase amount
async function onPurchaseComplete(purchaseAmount) {
if (Platform.OS === 'ios') {
// Track revenue event
NativeSingular.revenue('USD', purchaseAmount);
// Calculate conversion value based on purchase tier
const conversionValue = calculateConversionValue(purchaseAmount);
await NativeSingular.skanUpdateConversionValue(conversionValue);
}
}
function calculateConversionValue(amount) {
// Your custom logic to determine conversion value
if (amount >= 100) return 63; // High value
if (amount >= 50) return 40; // Medium value
if (amount >= 10) return 20; // Low value
return 10; // Minimal value
}
import { Singular } from 'singular-react-native';
import { Platform } from 'react-native';
// User completed signup - update conversion value to 7
async function onUserSignUp() {
if (Platform.OS === 'ios') {
// Track the sign-up event
Singular.event('SignUp');
// Update SKAN conversion value
const success = await Singular.skanUpdateConversionValue(7);
if (success) {
console.log('Conversion value updated successfully');
} else {
console.warn('Failed to update conversion value');
}
}
}
// User completed purchase - update based on purchase amount
async function onPurchaseComplete(purchaseAmount) {
if (Platform.OS === 'ios') {
// Track revenue event
Singular.revenue('USD', purchaseAmount);
// Calculate conversion value based on purchase tier
const conversionValue = calculateConversionValue(purchaseAmount);
await Singular.skanUpdateConversionValue(conversionValue);
}
}
function calculateConversionValue(amount) {
// Your custom logic to determine conversion value
if (amount >= 100) return 63; // High value
if (amount >= 50) return 40; // Medium value
if (amount >= 10) return 20; // Low value
return 10; // Minimal value
}
Update Conversion Values (SKAN 4.0)
For iOS 16.1+, use the skanUpdateConversionValues method
to
update SKAdNetwork 4.0 conversion values with fine value, coarse value,
and
lock parameters. This provides more granular control over conversion
value
updates.
Signature
static skanUpdateConversionValues(
conversionValue, // Fine value (0-63)
coarse, // Coarse value (0=low, 1=medium, 2=high)
lock // Lock status
)
Usage Example
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
import { Platform } from 'react-native';
async function updateSKAN4ConversionValue(fineValue, coarseValue, shouldLock) {
if (Platform.OS === 'ios') {
// Map coarse value string to number
const coarseMap = { low: 0, medium: 1, high: 2 };
// Update SKAdNetwork 4.0 conversion values
await NativeSingular.skanUpdateConversionValues(
fineValue,
coarseMap[coarseValue],
shouldLock
);
console.log(`SKAN 4.0 updated: fine=${fineValue}, coarse=${coarseValue}, lock=${shouldLock}`);
}
}
// Example: High-value user completes tutorial
async function onTutorialComplete() {
await updateSKAN4ConversionValue(15, 'medium', false);
}
// Example: Premium purchase - lock the value
async function onPremiumPurchase() {
await updateSKAN4ConversionValue(63, 'high', true);
}
import { Singular } from 'singular-react-native';
import { Platform } from 'react-native';
async function updateSKAN4ConversionValue(fineValue, coarseValue, shouldLock) {
if (Platform.OS === 'ios') {
// Map coarse value string to number
const coarseMap = { low: 0, medium: 1, high: 2 };
// Update SKAdNetwork 4.0 conversion values
Singular.skanUpdateConversionValues(
fineValue,
coarseMap[coarseValue],
shouldLock
);
console.log(`SKAN 4.0 updated: fine=${fineValue}, coarse=${coarseValue}, lock=${shouldLock}`);
}
}
// Example: High-value user completes tutorial
async function onTutorialComplete() {
await updateSKAN4ConversionValue(15, 'medium', false);
}
// Example: Premium purchase - lock the value
async function onPremiumPurchase() {
await updateSKAN4ConversionValue(63, 'high', true);
}
Get Current Conversion Value
Retrieve the current conversion value tracked by the Singular SDK. This is useful for implementing conditional logic based on the current state and works in both Managed and Manual modes.
Signature
static skanGetConversionValue()
Usage Example
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
import { Platform } from 'react-native';
async function checkAndUpdateConversionValue() {
if (Platform.OS === 'ios') {
const currentValue = await NativeSingular.skanGetConversionValue();
if (currentValue !== null) {
console.log(`Current conversion value: ${currentValue}`);
// Only update if current value is below threshold
if (currentValue < 30) {
await NativeSingular.skanUpdateConversionValue(30);
console.log('Updated conversion value to 30');
}
} else {
console.warn('Conversion value not available');
}
}
}
import { Singular } from 'singular-react-native';
import { Platform } from 'react-native';
async function checkAndUpdateConversionValue() {
if (Platform.OS === 'ios') {
const currentValue = await Singular.skanGetConversionValue();
if (currentValue !== null) {
console.log(`Current conversion value: ${currentValue}`);
// Only update if current value is below threshold
if (currentValue < 30) {
await Singular.skanUpdateConversionValue(30);
console.log('Updated conversion value to 30');
}
} else {
console.warn('Conversion value not available');
}
}
}
Monitor Conversion Value Updates
Set up a handler to receive real-time notifications whenever the conversion value changes. This enables you to react to conversion value updates and log analytics or trigger other app behaviors.
Configuration
Configure the conversion value update handler using the
withConversionValueUpdatedHandler method when initializing
the
SDK.
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
import { NativeEventEmitter } from 'react-native';
const config: SingularConfig = {
apikey: 'API_KEY',
secret: 'SECRET'
};
NativeSingular.init(config);
const emitter = new NativeEventEmitter(NativeSingular);
// Listen for conversion value updates
emitter.addListener('ConversionValueUpdatedHandler', (conversionValue) => {
console.log(`Conversion value updated to: ${conversionValue}`);
// Log the update to your analytics
logConversionValueUpdate(conversionValue);
// Trigger app-specific behavior
if (conversionValue >= 50) {
unlockPremiumFeature();
}
});
function logConversionValueUpdate(value) {
// Your analytics logging logic
console.log(`Analytics: SKAN CV = ${value}`);
}
function unlockPremiumFeature() {
// Your custom logic
console.log('Premium feature unlocked based on high conversion value');
}
import { Singular, SingularConfig } from 'singular-react-native';
const config = new SingularConfig('API_KEY', 'SECRET')
.withConversionValueUpdatedHandler((conversionValue) => {
console.log(`Conversion value updated to: ${conversionValue}`);
// Log the update to your analytics
logConversionValueUpdate(conversionValue);
// Trigger app-specific behavior
if (conversionValue >= 50) {
unlockPremiumFeature();
}
});
Singular.init(config);
function logConversionValueUpdate(value) {
// Your analytics logging logic
console.log(`Analytics: SKAN CV = ${value}`);
}
function unlockPremiumFeature() {
// Your custom logic
console.log('Premium feature unlocked based on high conversion value');
}
Best Practice: Use the conversion value handler to maintain a synchronized view of the current conversion state across your app. This is especially useful for debugging and ensuring your custom logic works correctly.
Supporting App Tracking Transparency (ATT)
App Tracking Transparency (ATT) is Apple's privacy framework requiring user consent before accessing the device's IDFA (Identifier for Advertisers) and sharing user data. Implementing ATT correctly is critical for iOS attribution and maximizing the accuracy of your user acquisition campaigns.
Why ATT Matters for Attribution
Starting with iOS 14.5, apps must request user permission through the ATT framework before accessing the IDFA. While attribution is still possible without the IDFA using fingerprinting and probabilistic methods, having the IDFA significantly improves attribution accuracy and provides deterministic matching.
- Deterministic attribution: The IDFA enables precise, device-level attribution that connects ad impressions directly to installs.
- Ad network optimization: Ad networks can better optimize campaigns and provide more accurate reporting with IDFA access.
- User-level insights: Access to IDFA allows for more granular user behavior analysis and cohort tracking.
Recommendation: Singular strongly recommends implementing the ATT prompt and requesting user consent. Explain the benefits to users (personalized ads, better app experience) to maximize opt-in rates.
Implementation Requirements
For iOS 14.5+ (including iOS 18), use the ATTrackingManager framework to request user consent before accessing the IDFA for tracking. The Singular SDK supports ATT, allowing initialization before consent and delaying events until consent is granted or a timeout occurs.
Step 1: Add ATT Framework Configuration
Configure your iOS app to support the ATT framework by updating your Info.plist file with a user-facing usage description.
-
Open Info.plist: Navigate to your iOS project's
Info.plist file (located in
ios/[YourAppName]/Info.plist). -
Add usage description: Add the
NSUserTrackingUsageDescriptionkey with a clear explanation of why your app needs tracking permission.
<key>NSUserTrackingUsageDescription</key>
<string>This app uses tracking to provide personalized ads and improve your experience.</string>
Important: The usage description will be displayed to users in the ATT prompt. Make it clear, concise, and honest about how tracking benefits them.
Step 2: Install ATT Support Package
Install the React Native ATT support package to enable ATT functionality in your app.
npm install react-native-tracking-transparency --save
After installation, run pod install from your iOS directory
to link native dependencies.
cd ios && pod install && cd ..
Step 3: Configure SDK Wait Timeout
Configure the Singular SDK to wait for the user's ATT response before
initializing by setting the
waitForTrackingAuthorizationWithTimeoutInterval property.
This delay ensures the IDFA is captured if the user grants permission.
Critical: Always request ATT consent and retrieve the IDFA before the Singular SDK sends its first session. Failing to do so will permanently lose the IDFA for that device's attribution data.
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
const config: SingularConfig = {
apikey: 'API_KEY',
secret: 'SECRET',
waitForTrackingAuthorizationWithTimeoutInterval: 300 // Wait up to 5 minutes
};
import { SingularConfig } from 'singular-react-native';
const config = new SingularConfig('API_KEY', 'SECRET')
.withWaitForTrackingAuthorizationWithTimeoutInterval(300); // Wait up to 5 minutes
Recommended Value: Set the timeout to 300 seconds (5 minutes) if your app displays the ATT prompt. This provides sufficient time for the user to see and respond to the prompt without creating a poor user experience if the prompt is delayed or not shown.
Step 4: Request ATT Consent
Implement the ATT request flow in your app, prompting users for tracking permission at an appropriate time in your user experience.
// TurboModule direct API (React Native 0.76+ New Architecture)
import React, { useEffect } from 'react';
import { Platform } from 'react-native';
import NativeSingular from 'singular-react-native/js/NativeSingular';
import { requestTrackingPermission } from 'react-native-tracking-transparency';
export default function App() {
useEffect(() => {
initializeApp();
}, []);
async function initializeApp() {
if (Platform.OS === 'ios') {
// Request ATT authorization
const trackingStatus = await requestTrackingPermission();
// Log the user's response
console.log('ATT Status:', trackingStatus);
// Possible values: 'authorized', 'denied', 'restricted', 'unavailable'
}
// Initialize Singular SDK (configured with wait timeout)
const config: SingularConfig = {
apikey: 'API_KEY',
secret: 'SECRET',
waitForTrackingAuthorizationWithTimeoutInterval: 300,
loggingEnabled: true,
};
NativeSingular.init(config);
}
return (
// Your app components go here
null
);
}
import React, { useEffect } from 'react';
import { Platform } from 'react-native';
import { Singular, SingularConfig } from 'singular-react-native';
import { requestTrackingPermission, getTrackingStatus } from 'react-native-tracking-transparency';
export default function App() {
useEffect(() => {
initializeApp();
}, []);
async function initializeApp() {
if (Platform.OS === 'ios') {
// Request ATT authorization
const trackingStatus = await requestTrackingPermission();
// Log the user's response
console.log('ATT Status:', trackingStatus);
// Possible values: 'authorized', 'denied', 'restricted', 'unavailable'
}
// Initialize Singular SDK (configured with wait timeout)
const config = new SingularConfig('API_KEY', 'SECRET')
.withWaitForTrackingAuthorizationWithTimeoutInterval(300)
.withLoggingEnabled();
Singular.init(config);
}
return (
// Your app components
);
}
ATT Best Practices
- Pre-prompt messaging: Show users a pre-ATT screen explaining why you need tracking permission and how it benefits them (better ads, improved experience). This can significantly increase opt-in rates.
- Timing matters: Show the ATT prompt at a natural moment in your app flow, not immediately on first launch. Let users experience your app first to build trust.
-
Timeout configuration: Set
waitForTrackingAuthorizationWithTimeoutIntervalto 30-300 seconds. Post-timeout, Singular proceeds with SKAN 4.0 attribution (no IDFA). - Test thoroughly: Test both authorized and denied scenarios to ensure your app functions correctly regardless of the user's choice.
- Respect user choice: Never repeatedly prompt users who have denied tracking or show aggressive messaging that pressures them to opt in.
-
Error handling: Check tracking status for
restricted(e.g., parental controls) orunavailablestates and log for analytics.
App Store Review: Apps that don't properly implement ATT or attempt to circumvent the framework may be rejected during App Store review. Ensure your implementation follows Apple's guidelines and respects user privacy choices.