Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions backend/services/gdpr.ts
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);
Comment on lines +14 to +31
Copy link

Copilot AI Apr 22, 2026

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).

Suggested change
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 interface ExportedUserProfile {
id: string;
email: string | null;
registeredAt: string | null;
[key: string]: unknown;
}
export interface ExportedSubscription {
id: string;
name: string;
amount: number;
status: string;
[key: string]: unknown;
}
export interface ExportedBillingRecord {
id: string;
date: string;
amount: number;
status: string;
[key: string]: unknown;
}
export interface ExportedConsentLog {
type: string;
status: string;
date: string;
[key: string]: unknown;
}
export interface UserDataExport {
profile: ExportedUserProfile | null;
subscriptions: ExportedSubscription[];
billingHistory: ExportedBillingRecord[];
consentLogs: ExportedConsentLog[];
}
export interface UserDataExportQueries {
getUserProfile: (userId: string) => Promise<ExportedUserProfile | null>;
getUserSubscriptions: (userId: string) => Promise<ExportedSubscription[]>;
getUserBillingHistory: (userId: string) => Promise<ExportedBillingRecord[]>;
getUserConsentLogs: (userId: string) => Promise<ExportedConsentLog[]>;
}
export const exportUserData = async (
userId: string,
queries: UserDataExportQueries,
): Promise<UserDataExport> => {
console.log(`Exporting data for user: ${userId}`);
const [profile, subscriptions, billingHistory, consentLogs] = await Promise.all([
queries.getUserProfile(userId),
queries.getUserSubscriptions(userId),
queries.getUserBillingHistory(userId),
queries.getUserConsentLogs(userId),
]);
return {
profile,
subscriptions,
billingHistory,
consentLogs,
};

Copilot uses AI. Check for mistakes.
};

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
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateConsent() accepts Partial<UserConsent>, but UserConsent includes fields like timestamp and dataProcessing; spreading a partial can produce incomplete records (e.g., missing required booleans) and the returned type no longer matches UserConsent. Consider defining an explicit input type (e.g. ConsentPreferences) and validating/normalizing missing fields before storing/logging to ensure a consistent audit trail schema.

Copilot uses AI. Check for mistakes.
// Log consent change for audit trail
console.log(`Consent updated for ${userId}:`, newConsent);

// await ConsentAuditModel.create({ userId, ...newConsent });

return newConsent;
};
49 changes: 49 additions & 0 deletions docs/gdpr.md
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)).
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc links to security.md, but there is no docs/security.md (or similarly named file) in the repository, so the link is currently broken. Either add the referenced security policy doc or update the link to the correct existing location.

Suggested change
- Regular security audits (see [Security Policy](security.md)).
- Regular security audits (see the repository security policy).

Copilot uses AI. Check for mistakes.

---

For any privacy-related inquiries, contact us at privacy@subtrackr.example.com.
Copy link

Copilot AI Apr 22, 2026

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.

Suggested change
For any privacy-related inquiries, contact us at privacy@subtrackr.example.com.
For any privacy-related inquiries, contact us at privacy@subtrackr.app.

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import AnalyticsScreen from '../screens/AnalyticsScreen';
import GDPRSettingsScreen from '../screens/GDPRSettingsScreen';
import LanguageSettingsScreen from '../screens/LanguageSettingsScreen';
import SettingsScreen from '../screens/SettingsScreen';
import GDPRSettingsScreen from '../screens/GDPRSettingsScreen';
import { colors } from '../utils/constants';
import { RootStackParamList, TabParamList } from './types';

Expand Down
201 changes: 201 additions & 0 deletions src/screens/GDPRSettingsScreen.tsx
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
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deletion confirmation copy says the action will "anonymize your data", but the code calls gdprService.requestDeletion(true) (i.e., permanent deletion). This is a behavioral mismatch for a destructive flow. Align the parameter and UI copy so the user is clearly requesting either anonymization/soft-delete or an irreversible hard delete.

Copilot uses AI. Check for mistakes.
} 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.
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The screen text says users can manage "what notifications you receive", but there is no toggle wired to consent.notifications (only analytics and marketing). Either add a notifications consent row here or adjust the copy/store shape so the UI accurately reflects the available controls.

Suggested change
Manage how SubTrackr processes your data and what notifications you receive.
Manage how SubTrackr processes your data and whether you receive marketing communications.

Copilot uses AI. Check for mistakes.
</Text>

<View style={styles.row}>
<View style={styles.labelContainer}>
<Text style={styles.label}>Analytics</Text>
<Text style={styles.subLabel}>Help us improve by sharing anonymous usage data.</Text>
</View>
<Switch
value={consent.analytics}
onValueChange={(val) => setConsent({ analytics: val })}
/>
</View>

<View style={styles.row}>
<View style={styles.labelContainer}>
<Text style={styles.label}>Marketing Notifications</Text>
<Text style={styles.subLabel}>Receive updates about new features and offers.</Text>
</View>
<Switch
value={consent.marketing}
onValueChange={(val) => setConsent({ marketing: val })}
/>
Comment on lines +69 to +83
Copy link

Copilot AI Apr 22, 2026

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 uses AI. Check for mistakes.
</View>
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Your Data Rights</Text>

<TouchableOpacity
style={styles.button}
onPress={handleExport}
disabled={loading}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Export My Data (JSON)</Text>}
</TouchableOpacity>
<Text style={styles.infoText}>
Download a structured copy of your profile, subscriptions, and billing history.
</Text>

<TouchableOpacity
style={[styles.button, styles.deleteButton]}
onPress={handleDeleteAccount}
disabled={loading}
>
<Text style={styles.buttonText}>Delete My Account</Text>
</TouchableOpacity>
<Text style={styles.infoText}>
Exercise your "Right to be Forgotten". This will anonymize your personal information.
</Text>
</View>

<View style={styles.footer}>
<Text style={styles.footerText}>
SubTrackr stores your data on-chain via Stellar and encrypted in our secure databases.
For more information, see our Privacy Policy.
</Text>
</View>
</ScrollView>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F8F9FA',
},
section: {
padding: 20,
backgroundColor: '#FFF',
marginBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#EEE',
},
sectionTitle: {
fontSize: 18,
fontWeight: '700',
color: '#1A1A1A',
marginBottom: 8,
},
description: {
fontSize: 14,
color: '#666',
marginBottom: 20,
lineHeight: 20,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
labelContainer: {
flex: 1,
paddingRight: 10,
},
label: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
subLabel: {
fontSize: 12,
color: '#888',
marginTop: 2,
},
button: {
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginTop: 10,
},
deleteButton: {
backgroundColor: '#FF3B30',
marginTop: 30,
Comment on lines +123 to +176
Copy link

Copilot AI Apr 22, 2026

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.

Copilot uses AI. Check for mistakes.
},
buttonText: {
color: '#FFF',
fontSize: 16,
fontWeight: '600',
},
infoText: {
fontSize: 12,
color: '#999',
marginTop: 8,
textAlign: 'center',
},
footer: {
padding: 30,
alignItems: 'center',
},
footerText: {
fontSize: 12,
color: '#BBB',
textAlign: 'center',
lineHeight: 18,
},
});

export default GDPRSettingsScreen;
4 changes: 4 additions & 0 deletions src/screens/SettingsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors, spacing, typography, borderRadius } from '../utils/constants';
import { useWalletStore } from '../store';
import { Card } from '../components/common/Card';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../navigation/types';

const APP_VERSION = '1.0.0';
interface Settings {
Expand All @@ -23,6 +26,7 @@ interface Settings {
const SETTINGS_KEY = '@subtrackr_settings';

const SettingsScreen: React.FC = () => {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { address, network, disconnect } = useWalletStore();
const [settings, setSettings] = useState<Settings>({
notificationsEnabled: true,
Expand Down
Loading
Loading