-
Notifications
You must be signed in to change notification settings - Fork 104
feat(gdpr): implement data export, deletion, consent management, and GDPR settings UI #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /** | ||
| * GDPR Service - Backend implementation for Data Privacy rights. | ||
| * This service handles data exporting, deletion (Right to be Forgotten), | ||
| * and consent management. | ||
| */ | ||
|
|
||
| export interface UserConsent { | ||
| analytics: boolean; | ||
| notifications: boolean; | ||
| dataProcessing: boolean; | ||
| timestamp: string; | ||
| } | ||
|
|
||
| export const exportUserData = async (userId: string) => { | ||
| console.log(`Exporting data for user: ${userId}`); | ||
|
|
||
| // In a real scenario, this would query multiple tables/collections | ||
| const userData = { | ||
| profile: { id: userId, email: 'user@example.com', registeredAt: '2026-01-01' }, | ||
| subscriptions: [ | ||
| { id: 'sub_1', name: 'Netflix', amount: 15.99, status: 'active' } | ||
| ], | ||
| billingHistory: [ | ||
| { id: 'tx_1', date: '2026-04-20', amount: 15.99, status: 'completed' } | ||
| ], | ||
| consentLogs: [ | ||
| { type: 'analytics', status: 'granted', date: '2026-01-01' } | ||
| ], | ||
| }; | ||
|
|
||
| return JSON.stringify(userData, null, 2); | ||
| }; | ||
|
|
||
| export const deleteUserData = async (userId: string, permanent: boolean = false) => { | ||
| console.log(`Processing deletion for user: ${userId} (Permanent: ${permanent})`); | ||
|
|
||
| if (!permanent) { | ||
| // Soft delete / Anonymization | ||
| return anonymizeUserData(userId); | ||
| } | ||
|
|
||
| // Hard delete logic across all services | ||
| // await SubscriptionModel.deleteMany({ userId }); | ||
| // await ProfileModel.deleteOne({ userId }); | ||
|
|
||
| return { success: true, message: 'User data permanently deleted' }; | ||
| }; | ||
|
|
||
| export const anonymizeUserData = async (userId: string) => { | ||
| console.log(`Anonymizing data for user: ${userId}`); | ||
|
|
||
| // Replace sensitive identifiers with null/dummy values | ||
| const updates = { | ||
| email: `deleted-${Date.now()}@anonymized.invalid`, | ||
| name: 'Anonymized User', | ||
| address: null, | ||
| phone: null, | ||
| }; | ||
|
|
||
| // await ProfileModel.updateOne({ userId }, updates); | ||
|
|
||
| return { success: true, message: 'User data has been anonymized' }; | ||
| }; | ||
|
|
||
| export const updateConsent = async (userId: string, preferences: Partial<UserConsent>) => { | ||
| const newConsent = { | ||
| ...preferences, | ||
| timestamp: new Date().toISOString(), | ||
| }; | ||
|
|
||
|
Comment on lines
+65
to
+70
|
||
| // Log consent change for audit trail | ||
| console.log(`Consent updated for ${userId}:`, newConsent); | ||
|
|
||
| // await ConsentAuditModel.create({ userId, ...newConsent }); | ||
|
|
||
| return newConsent; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||
| # GDPR Compliance & Data Privacy | ||||||
|
|
||||||
| SubTrackr is designed with "Privacy by Design" principles to ensure user data is handled securely and transparently in compliance with the General Data Protection Regulation (GDPR). | ||||||
|
|
||||||
| ## 1. Data Collection & Processing | ||||||
|
|
||||||
| We collect only the minimum data necessary to provide our subscription management services: | ||||||
|
|
||||||
| - **Identity**: Wallet addresses (public keys), email (if social login is used). | ||||||
| - **Activity**: Subscription names, billing amounts, and recurring intervals. | ||||||
| - **On-chain**: Transactions recorded on the Stellar network (immutable by design). | ||||||
|
|
||||||
| ## 2. User Rights | ||||||
|
|
||||||
| ### Right of Access & Portability | ||||||
| Users can download a structured JSON file of their profile and activity data directly from the **GDPR Settings** screen in the app. | ||||||
|
|
||||||
| ### Right to be Forgotten (Deletion/Anonymization) | ||||||
| Users can request account deletion. Due to the immutable nature of blockchain, on-chain records remain, but we: | ||||||
| - Anonymize personal identifiers (names, emails) in our off-chain databases. | ||||||
| - Remove associations between the wallet address and the person's identity where possible. | ||||||
| - Soft-delete subscriptions to maintain system integrity for merchants while stopping all user tracking. | ||||||
|
|
||||||
| ### Right to Restrict Processing | ||||||
| Consent preferences for analytics and marketing can be toggled at any time in the app settings. | ||||||
|
|
||||||
| ## 3. Data Processing Agreement (DPA) | ||||||
|
|
||||||
| | Purpose | Data Category | Lawful Basis | | ||||||
| | :--- | :--- | :--- | | ||||||
| | Core Service | Wallet, Subscriptions | Contractual Necessity | | ||||||
| | Billing Alerts | Emails, Notifications | Legitimate Interest | | ||||||
| | App Improvement | Usage Analytics | Consent (Opt-in) | | ||||||
|
|
||||||
| ## 4. Retention Policy | ||||||
|
|
||||||
| - **Active Accounts**: Retained for the duration of the account lifespan. | ||||||
| - **Deactivated Accounts**: Anonymized immediately; logs deleted after 90 days. | ||||||
| - **On-chain Data**: Persists on the Stellar network indefinitely. | ||||||
|
|
||||||
| ## 5. Security Measures | ||||||
|
|
||||||
| - Data encryption at rest and in transit. | ||||||
| - Secure wallet-based authentication. | ||||||
| - Regular security audits (see [Security Policy](security.md)). | ||||||
|
||||||
| - Regular security audits (see [Security Policy](security.md)). | |
| - Regular security audits (see the repository security policy). |
Copilot
AI
Apr 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contact address uses the placeholder domain privacy@subtrackr.example.com, while the app links use @subtrackr.app (e.g. support@subtrackr.app). Please align this to the real privacy contact email/domain so users arenβt directed to a non-functional address.
| For any privacy-related inquiries, contact us at privacy@subtrackr.example.com. | |
| For any privacy-related inquiries, contact us at privacy@subtrackr.app. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,201 @@ | ||||||
| import React, { useState } from 'react'; | ||||||
| import { | ||||||
| View, | ||||||
| Text, | ||||||
| StyleSheet, | ||||||
| ScrollView, | ||||||
| TouchableOpacity, | ||||||
| Switch, | ||||||
| Alert, | ||||||
| ActivityIndicator, | ||||||
| } from 'react-native'; | ||||||
| import { useUserStore } from '../store/userStore'; | ||||||
| import { gdprService } from '../services/gdpr'; | ||||||
|
|
||||||
| const GDPRSettingsScreen = () => { | ||||||
| const { consent, setConsent } = useUserStore(); | ||||||
| const [loading, setLoading] = useState(false); | ||||||
|
|
||||||
| const handleExport = async () => { | ||||||
| setLoading(true); | ||||||
| try { | ||||||
| const result = await gdprService.exportData(); | ||||||
| gdprService.downloadData(result); | ||||||
| } catch (error) { | ||||||
| Alert.alert('Error', 'Could not prepare your data export. Please try again later.'); | ||||||
| } finally { | ||||||
| setLoading(false); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| const handleDeleteAccount = () => { | ||||||
| Alert.alert( | ||||||
| 'Permanent Deletion', | ||||||
| 'Are you sure you want to delete your account? This action will anonymize your data and revoke access to all subscriptions. It cannot be undone.', | ||||||
| [ | ||||||
| { text: 'Cancel', style: 'cancel' }, | ||||||
| { | ||||||
| text: 'Delete Everything', | ||||||
| style: 'destructive', | ||||||
| onPress: async () => { | ||||||
| setLoading(true); | ||||||
| try { | ||||||
| await gdprService.requestDeletion(true); | ||||||
| Alert.alert('Success', 'Your account has been queued for deletion.'); | ||||||
|
Comment on lines
+33
to
+44
|
||||||
| } catch (e) { | ||||||
| Alert.alert('Error', 'Deletion failed.'); | ||||||
| } finally { | ||||||
| setLoading(false); | ||||||
| } | ||||||
| } | ||||||
| }, | ||||||
| ] | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| return ( | ||||||
| <ScrollView style={styles.container}> | ||||||
| <View style={styles.section}> | ||||||
| <Text style={styles.sectionTitle}>Privacy & Consent</Text> | ||||||
| <Text style={styles.description}> | ||||||
| Manage how SubTrackr processes your data and what notifications you receive. | ||||||
|
||||||
| Manage how SubTrackr processes your data and what notifications you receive. | |
| Manage how SubTrackr processes your data and whether you receive marketing communications. |
Copilot
AI
Apr 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consent toggles currently only update local Zustand state via setConsent, but never call gdprService.updateConsent(). That means consent changes are not sent to the backend / audit trail as described in the PR, and will be lost to the server-side view of consent. Please persist these changes by invoking updateConsent (with error handling/rollback) when toggles change.
Copilot
AI
Apr 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This screen hard-codes colors (e.g. #F8F9FA, #007AFF) and leaves Switch un-themed, while other screens use the shared design tokens (colors, spacing, typography) and set trackColor/thumbColor (see SettingsScreen.tsx and SubscriptionDetailScreen.tsx). Using the shared constants here will keep theming consistent (including dark mode) and reduce future style drift.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exportUserData()currently constructs and returns hard-coded sample data (including an email address and subscription/billing records). Shipping this as-is risks leaking placeholder/incorrect PII and doesnβt actually export the requesting userβs data. Please replace the mocked payload with real queries (and keep the service returning structured data rather than a pre-stringified JSON blob so the API layer can choose the correct response headers/format).