Introduction

Privacy has moved from legal checkbox to competitive advantage. Users check privacy labels before downloading apps. A “Data Not Collected” label builds trust. A wall of data collection categories raises concerns.

Both Apple and Google now require detailed privacy disclosures. Get them wrong and your app update gets rejected. Get them right and users feel confident sharing their data with you.

This guide covers privacy requirements for iOS App Store and Google Play Store, along with implementation patterns for transparent data handling that satisfies regulators and builds user trust.

Platform Privacy Requirements

Apple App Store Privacy Labels

Apple requires privacy labels for all apps, displaying what data you collect and how it is used.

DATA TYPES:
├── Contact Info (name, email, phone, address)
├── Health & Fitness (health, fitness)
├── Financial Info (payment info, credit info)
├── Location (precise, coarse)
├── Sensitive Info (racial, political, religious, sexual orientation)
├── Contacts (contacts list)
├── User Content (emails, texts, photos, videos, gameplay, customer support)
├── Browsing History
├── Search History
├── Identifiers (user ID, device ID)
├── Purchases
├── Usage Data (product interaction, advertising data)
├── Diagnostics (crash data, performance data)
└── Other Data

USAGE PURPOSES:
├── Third-Party Advertising
├── Developer's Advertising or Marketing
├── Analytics
├── Product Personalization
├── App Functionality
└── Other Purposes

DATA LINKAGE:
├── Linked to User Identity
└── Not Linked to User Identity

TRACKING:
├── Used to Track Users
└── Not Used to Track Users

Google Play Data Safety

Google’s Data Safety section requires similar but differently structured disclosures.

DATA COLLECTION:
├── Collected (app collects this data)
└── Shared (app shares this data with third parties)

DATA TYPES:
├── Location (approximate, precise)
├── Personal Info (name, email, user IDs, address, phone, other)
├── Financial Info (purchase history, credit info, other)
├── Health and Fitness (health, fitness)
├── Messages (emails, SMS, other)
├── Photos and Videos
├── Audio Files (voice recordings, music, other)
├── Files and Docs
├── Calendar
├── Contacts
├── App Activity (interactions, search history, installed apps, other)
├── Web Browsing
├── App Info and Performance (crash logs, diagnostics, other)
└── Device or Other IDs

DATA HANDLING:
├── Data encrypted in transit
├── Users can request data deletion
└── Data deletion when account deleted

Auditing Your Data Collection

Data Inventory Template

interface DataInventory {
  dataType: string;
  collectionPoint: string;      // Where in app
  purpose: DataPurpose[];
  storage: StorageDetails;
  sharing: SharingDetails;
  retention: RetentionPolicy;
  userControl: UserControlOptions;
}

interface StorageDetails {
  location: 'device' | 'server' | 'both';
  encrypted: boolean;
  encryptionMethod?: string;
}

interface SharingDetails {
  sharedWithThirdParties: boolean;
  thirdParties?: ThirdPartyDetails[];
  purpose?: string;
}

interface ThirdPartyDetails {
  name: string;
  purpose: string;
  dataTypes: string[];
  privacyPolicyUrl: string;
}

// Example inventory
const dataInventory: DataInventory[] = [
  {
    dataType: "Email Address",
    collectionPoint: "Registration, checkout",
    purpose: ["app_functionality", "marketing"],
    storage: {
      location: "server",
      encrypted: true,
      encryptionMethod: "AES-256"
    },
    sharing: {
      sharedWithThirdParties: true,
      thirdParties: [
        {
          name: "SendGrid",
          purpose: "Transactional emails",
          dataTypes: ["email"],
          privacyPolicyUrl: "https://sendgrid.com/privacy"
        }
      ]
    },
    retention: {
      period: "account_lifetime",
      deletionProcess: "automated_on_account_deletion"
    },
    userControl: {
      canView: true,
      canEdit: true,
      canDelete: true,
      canExport: true
    }
  },
  {
    dataType: "Precise Location",
    collectionPoint: "Store finder, delivery address",
    purpose: ["app_functionality"],
    storage: {
      location: "device",
      encrypted: false
    },
    sharing: {
      sharedWithThirdParties: false
    },
    retention: {
      period: "session_only",
      deletionProcess: "automatic_on_session_end"
    },
    userControl: {
      canView: false,
      canEdit: false,
      canDelete: true, // Via OS settings
      canExport: false
    }
  }
];

SDK and Third-Party Audit

// Audit all SDKs for data collection
struct SDKPrivacyAudit {
    let sdkName: String
    let version: String
    let dataCollected: [DataType]
    let dataPurposes: [DataPurpose]
    let requiresConsent: Bool
    let canBeDisabled: Bool
    let privacyDocumentation: URL

    enum DataType: String, CaseIterable {
        case deviceId = "Device ID"
        case advertisingId = "Advertising ID"
        case ipAddress = "IP Address"
        case location = "Location"
        case crashLogs = "Crash Logs"
        case usageData = "Usage Data"
        case userIdentifiers = "User Identifiers"
    }

    enum DataPurpose: String, CaseIterable {
        case analytics = "Analytics"
        case crashReporting = "Crash Reporting"
        case advertising = "Advertising"
        case attribution = "Attribution"
        case functionality = "App Functionality"
    }
}

// Common SDK audit results
let sdkAudits: [SDKPrivacyAudit] = [
    SDKPrivacyAudit(
        sdkName: "Firebase Analytics",
        version: "10.x",
        dataCollected: [.deviceId, .usageData],
        dataPurposes: [.analytics],
        requiresConsent: true, // Under GDPR
        canBeDisabled: true,
        privacyDocumentation: URL(string: "https://firebase.google.com/support/privacy")!
    ),
    SDKPrivacyAudit(
        sdkName: "Firebase Crashlytics",
        version: "10.x",
        dataCollected: [.deviceId, .crashLogs],
        dataPurposes: [.crashReporting],
        requiresConsent: false, // Legitimate interest
        canBeDisabled: true,
        privacyDocumentation: URL(string: "https://firebase.google.com/support/privacy")!
    ),
    SDKPrivacyAudit(
        sdkName: "Facebook SDK",
        version: "16.x",
        dataCollected: [.deviceId, .advertisingId, .usageData],
        dataPurposes: [.advertising, .attribution, .analytics],
        requiresConsent: true,
        canBeDisabled: true,
        privacyDocumentation: URL(string: "https://www.facebook.com/privacy/policy")!
    ),
    SDKPrivacyAudit(
        sdkName: "Google AdMob",
        version: "10.x",
        dataCollected: [.deviceId, .advertisingId, .location],
        dataPurposes: [.advertising],
        requiresConsent: true,
        canBeDisabled: true,
        privacyDocumentation: URL(string: "https://policies.google.com/privacy")!
    )
]

iOS App Tracking Transparency

import AppTrackingTransparency
import AdSupport

class TrackingConsentManager: ObservableObject {
    @Published var trackingStatus: ATTrackingManager.AuthorizationStatus = .notDetermined
    @Published var idfa: String?

    init() {
        trackingStatus = ATTrackingManager.trackingAuthorizationStatus
        if trackingStatus == .authorized {
            idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
        }
    }

    func requestTrackingPermission() async -> Bool {
        // Only request if not determined
        guard trackingStatus == .notDetermined else {
            return trackingStatus == .authorized
        }

        let status = await ATTrackingManager.requestTrackingAuthorization()

        await MainActor.run {
            trackingStatus = status
            if status == .authorized {
                idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
            }
        }

        return status == .authorized
    }

    var canTrack: Bool {
        trackingStatus == .authorized
    }
}

// Pre-permission prompt (shown before system dialog)
struct TrackingPermissionView: View {
    let onContinue: () -> Void

    var body: some View {
        VStack(spacing: 24) {
            Image(systemName: "hand.raised.fill")
                .font(.system(size: 60))
                .foregroundColor(.blue)

            Text("Help Us Improve Your Experience")
                .font(.title2.bold())

            VStack(alignment: .leading, spacing: 12) {
                BenefitRow(
                    icon: "gift",
                    text: "See relevant offers and deals"
                )
                BenefitRow(
                    icon: "chart.bar",
                    text: "Help us understand how to make the app better"
                )
                BenefitRow(
                    icon: "hand.thumbsup",
                    text: "Reduce repetitive ads"
                )
            }
            .padding()
            .background(Color(.systemGray6))
            .cornerRadius(12)

            Text("On the next screen, tap \"Allow\" to enable personalised experiences. You can change this anytime in Settings.")
                .font(.caption)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)

            Button(action: onContinue) {
                Text("Continue")
                    .font(.headline)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
        .padding()
    }
}
// Android: Comprehensive consent management
class ConsentManager(
    private val context: Context,
    private val analytics: AnalyticsManager
) {

    private val prefs = context.getSharedPreferences("consent", Context.MODE_PRIVATE)

    data class ConsentState(
        val analyticsConsent: Boolean = false,
        val marketingConsent: Boolean = false,
        val personalisationConsent: Boolean = false,
        val thirdPartyConsent: Boolean = false,
        val consentTimestamp: Long = 0,
        val consentVersion: Int = 0
    )

    private val _consentState = MutableStateFlow(loadConsentState())
    val consentState: StateFlow<ConsentState> = _consentState

    fun updateConsent(
        analyticsConsent: Boolean? = null,
        marketingConsent: Boolean? = null,
        personalisationConsent: Boolean? = null,
        thirdPartyConsent: Boolean? = null
    ) {
        val current = _consentState.value

        val updated = current.copy(
            analyticsConsent = analyticsConsent ?: current.analyticsConsent,
            marketingConsent = marketingConsent ?: current.marketingConsent,
            personalisationConsent = personalisationConsent ?: current.personalisationConsent,
            thirdPartyConsent = thirdPartyConsent ?: current.thirdPartyConsent,
            consentTimestamp = System.currentTimeMillis(),
            consentVersion = CURRENT_CONSENT_VERSION
        )

        saveConsentState(updated)
        _consentState.value = updated

        // Apply consent changes
        applyConsentSettings(updated)

        // Log consent change (without PII)
        analytics.trackConsentChange(
            analyticsConsent = updated.analyticsConsent,
            marketingConsent = updated.marketingConsent,
            consentVersion = CURRENT_CONSENT_VERSION
        )
    }

    private fun applyConsentSettings(consent: ConsentState) {
        // Firebase Analytics
        Firebase.analytics.setAnalyticsCollectionEnabled(consent.analyticsConsent)

        // Facebook SDK
        if (consent.thirdPartyConsent) {
            FacebookSdk.setAutoLogAppEventsEnabled(true)
            FacebookSdk.setAdvertiserIDCollectionEnabled(true)
        } else {
            FacebookSdk.setAutoLogAppEventsEnabled(false)
            FacebookSdk.setAdvertiserIDCollectionEnabled(false)
        }

        // AdMob consent
        val consentInfo = UserMessagingPlatform.getConsentInformation(context)
        if (!consent.thirdPartyConsent) {
            // Request non-personalised ads only
            val extras = Bundle().apply {
                putString("npa", "1")
            }
            // Apply to ad requests
        }
    }

    fun needsConsentRefresh(): Boolean {
        val state = _consentState.value

        // Refresh if consent version outdated
        if (state.consentVersion < CURRENT_CONSENT_VERSION) return true

        // Refresh annually
        val oneYearAgo = System.currentTimeMillis() - (365L * 24 * 60 * 60 * 1000)
        if (state.consentTimestamp < oneYearAgo) return true

        return false
    }

    companion object {
        private const val CURRENT_CONSENT_VERSION = 2
    }
}
struct ConsentSettingsView: View {
    @ObservedObject var consentManager: ConsentManager
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationStack {
            List {
                Section {
                    Text("We use your data to provide and improve our services. You can control how your data is used below.")
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }

                Section("Essential") {
                    ConsentRow(
                        title: "App Functionality",
                        description: "Required for the app to work. Cannot be disabled.",
                        isEnabled: .constant(true),
                        isRequired: true
                    )
                }

                Section("Optional") {
                    ConsentRow(
                        title: "Analytics",
                        description: "Help us understand how you use the app to make improvements.",
                        isEnabled: $consentManager.analyticsConsent
                    )

                    ConsentRow(
                        title: "Personalisation",
                        description: "Personalise your experience based on your preferences and usage.",
                        isEnabled: $consentManager.personalisationConsent
                    )

                    ConsentRow(
                        title: "Marketing",
                        description: "Receive news, offers, and updates via email and push notifications.",
                        isEnabled: $consentManager.marketingConsent
                    )

                    ConsentRow(
                        title: "Third-Party Partners",
                        description: "Share data with advertising and analytics partners for targeted ads.",
                        isEnabled: $consentManager.thirdPartyConsent
                    )
                }

                Section {
                    NavigationLink("View Privacy Policy") {
                        PrivacyPolicyView()
                    }

                    NavigationLink("View Data We Collect") {
                        DataCollectionDetailView()
                    }

                    Button("Download My Data") {
                        consentManager.requestDataExport()
                    }

                    Button("Delete My Data", role: .destructive) {
                        consentManager.requestDataDeletion()
                    }
                }
            }
            .navigationTitle("Privacy Settings")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Done") { dismiss() }
                }
            }
        }
    }
}

struct ConsentRow: View {
    let title: String
    let description: String
    @Binding var isEnabled: Bool
    var isRequired: Bool = false

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Toggle(isOn: $isEnabled) {
                Text(title)
                    .font(.headline)
            }
            .disabled(isRequired)

            Text(description)
                .font(.caption)
                .foregroundColor(.secondary)

            if isRequired {
                Text("Required")
                    .font(.caption2)
                    .foregroundColor(.blue)
            }
        }
        .padding(.vertical, 4)
    }
}

Data Subject Rights Implementation

Right to Access and Export

class DataExportManager(
    private val api: ApiClient,
    private val localDatabase: AppDatabase
) {

    suspend fun exportUserData(): DataExportResult {
        return try {
            // Gather server data
            val serverData = api.requestDataExport()

            // Gather local data
            val localData = gatherLocalData()

            // Combine into export format
            val export = UserDataExport(
                exportDate = Instant.now().toString(),
                userId = getCurrentUserId(),
                serverData = serverData,
                localData = localData
            )

            // Generate downloadable file
            val file = generateExportFile(export)

            DataExportResult.Success(file)
        } catch (e: Exception) {
            DataExportResult.Error(e.message ?: "Export failed")
        }
    }

    private suspend fun gatherLocalData(): LocalDataExport {
        return LocalDataExport(
            preferences = exportPreferences(),
            searchHistory = localDatabase.searchHistoryDao().getAll(),
            recentlyViewed = localDatabase.recentItemsDao().getAll(),
            savedItems = localDatabase.savedItemsDao().getAll(),
            cachedData = listCachedData()
        )
    }

    private fun generateExportFile(export: UserDataExport): File {
        val json = Json.encodeToString(export)
        val file = File(context.cacheDir, "data_export_${System.currentTimeMillis()}.json")
        file.writeText(json)
        return file
    }
}

@Serializable
data class UserDataExport(
    val exportDate: String,
    val userId: String,
    val serverData: ServerDataExport,
    val localData: LocalDataExport
)

@Serializable
data class ServerDataExport(
    val profile: UserProfile,
    val orders: List<Order>,
    val savedAddresses: List<Address>,
    val paymentMethods: List<PaymentMethodInfo>, // Masked
    val communications: List<Communication>,
    val activityLog: List<ActivityEntry>
)

Right to Deletion

class DataDeletionManager {

    enum DeletionScope {
        case fullAccount      // Delete everything
        case specificData([DataCategory])  // Delete selected categories
    }

    enum DataCategory: String, CaseIterable {
        case profile = "Profile Information"
        case orders = "Order History"
        case searchHistory = "Search History"
        case savedItems = "Saved Items"
        case communications = "Communications"
        case analytics = "Analytics Data"
    }

    func requestDeletion(scope: DeletionScope) async throws -> DeletionRequest {
        // Create deletion request
        let request = DeletionRequest(
            id: UUID().uuidString,
            scope: scope,
            requestedAt: Date(),
            scheduledCompletionDate: Calendar.current.date(byAdding: .day, value: 30, to: Date())!
        )

        // Submit to server
        try await api.submitDeletionRequest(request)

        // Clear local data immediately
        await clearLocalData(scope: scope)

        return request
    }

    private func clearLocalData(scope: DeletionScope) async {
        switch scope {
        case .fullAccount:
            // Clear all local data
            UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
            try? KeychainHelper.clearAll()
            try? FileManager.default.removeItem(at: cacheDirectory)
            await localDatabase.clearAll()

        case .specificData(let categories):
            for category in categories {
                await clearCategory(category)
            }
        }
    }

    private func clearCategory(_ category: DataCategory) async {
        switch category {
        case .searchHistory:
            await localDatabase.searchHistory.deleteAll()
        case .savedItems:
            await localDatabase.savedItems.deleteAll()
        case .analytics:
            // Reset analytics identifiers
            Analytics.resetAnalyticsData()
        default:
            break
        }
    }
}

Australian Privacy Act Compliance

Australian-Specific Requirements

// Australian Privacy Principles (APPs) compliance
struct AustralianPrivacyCompliance {

    // APP 1: Open and transparent management
    static let privacyPolicyURL = URL(string: "https://yourapp.com.au/privacy")!

    // APP 5: Notification of collection
    struct CollectionNotice {
        let dataTypes: [String]
        let purposes: [String]
        let disclosures: [String]
        let accessRights: String
        let complaintProcess: String

        static let standard = CollectionNotice(
            dataTypes: [
                "Name and contact details",
                "Payment information",
                "Order history",
                "Device information"
            ],
            purposes: [
                "Processing your orders",
                "Communicating with you",
                "Improving our services",
                "Legal compliance"
            ],
            disclosures: [
                "Payment processors (Stripe)",
                "Delivery partners",
                "Analytics providers (with consent)"
            ],
            accessRights: "You can request access to your personal information at any time through the app or by contacting [email protected]",
            complaintProcess: "If you have a complaint about how we handle your information, contact us at [email protected]. If unsatisfied, you can lodge a complaint with the Office of the Australian Information Commissioner (OAIC)."
        )
    }

    // APP 6: Use and disclosure
    static func canUseDataFor(purpose: DataPurpose, consent: ConsentState) -> Bool {
        switch purpose {
        case .primaryPurpose:
            return true // Always allowed
        case .relatedSecondaryPurpose:
            return true // Usually allowed if reasonable
        case .marketing:
            return consent.marketingConsent
        case .thirdPartySharing:
            return consent.thirdPartyConsent
        }
    }

    // APP 11: Security
    static let securityMeasures = [
        "Data encrypted in transit (TLS 1.3)",
        "Data encrypted at rest (AES-256)",
        "Access controls and authentication",
        "Regular security audits",
        "Staff privacy training"
    ]

    // APP 12: Access
    static func handleAccessRequest(userId: String) async -> DataAccessResponse {
        // Must respond within 30 days
        let deadline = Calendar.current.date(byAdding: .day, value: 30, to: Date())!

        return DataAccessResponse(
            requestId: UUID().uuidString,
            userId: userId,
            requestedAt: Date(),
            responseDeadline: deadline,
            status: .processing
        )
    }

    // APP 13: Correction
    static func handleCorrectionRequest(
        userId: String,
        field: String,
        currentValue: String,
        requestedValue: String
    ) async -> CorrectionResponse {
        // Must respond within 30 days
        // Must correct if information is inaccurate, incomplete, misleading

        return CorrectionResponse(
            requestId: UUID().uuidString,
            field: field,
            status: .approved,
            completedAt: Date()
        )
    }
}

Privacy Label Generation

Automated Privacy Label Generator

// Generate privacy labels from data inventory
function generateApplePrivacyLabel(inventory: DataInventory[]): ApplePrivacyLabel {
  const label: ApplePrivacyLabel = {
    dataUsedToTrackYou: [],
    dataLinkedToYou: [],
    dataNotLinkedToYou: [],
    dataNotCollected: []
  };

  // Categorize each data type
  for (const item of inventory) {
    const category = mapToAppleCategory(item.dataType);

    if (item.purposes.includes('tracking') || item.purposes.includes('third_party_advertising')) {
      label.dataUsedToTrackYou.push({
        category,
        purposes: item.purposes.map(mapToApplePurpose)
      });
    } else if (isLinkedToUser(item)) {
      label.dataLinkedToYou.push({
        category,
        purposes: item.purposes.map(mapToApplePurpose)
      });
    } else {
      label.dataNotLinkedToYou.push({
        category,
        purposes: item.purposes.map(mapToApplePurpose)
      });
    }
  }

  return label;
}

function generateGoogleDataSafety(inventory: DataInventory[]): GoogleDataSafety {
  const safety: GoogleDataSafety = {
    dataCollected: [],
    dataShared: [],
    securityPractices: {
      dataEncryptedInTransit: true,
      dataCanBeDeleted: true
    }
  };

  for (const item of inventory) {
    const category = mapToGoogleCategory(item.dataType);

    // Data collected
    safety.dataCollected.push({
      category,
      optional: !item.required,
      purposes: item.purposes.map(mapToGooglePurpose)
    });

    // Data shared
    if (item.sharing.sharedWithThirdParties) {
      safety.dataShared.push({
        category,
        purposes: item.sharing.thirdParties?.map(tp => tp.purpose) ?? []
      });
    }
  }

  return safety;
}

Conclusion

Privacy transparency is now a competitive requirement. Users check privacy labels. Regulators audit compliance. Getting privacy right builds trust that translates to engagement and retention.

Key takeaways:

  1. Audit thoroughly - Know exactly what data you collect and why
  2. Minimise collection - Only collect what you need
  3. Be transparent - Clear privacy labels and in-app disclosures
  4. Implement controls - Let users manage their consent
  5. Respect rights - Enable access, correction, and deletion
  6. Stay compliant - Meet platform requirements and local laws

Start with a comprehensive data inventory. Map each data point to its purpose. Implement consent management that respects user choices. Build data export and deletion into your architecture from the start.

Privacy done right is not just compliance - it is a feature users appreciate.


Need help navigating privacy requirements for your Australian app? We have implemented privacy-compliant systems for apps handling sensitive user data. Contact us to discuss your compliance needs.