Skip to content
Open
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
38 changes: 38 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,41 @@ contracts/migrations/history/*
!contracts/migrations/history/.gitkeep
contracts/migrations/snapshots/*
!contracts/migrations/snapshots/.gitkeep

# Test snapshots (Rust / Jest)
contracts/test_snapshots/
contracts/**/test_snapshots/
src/**/__snapshots__/
**/__snapshots__/
*.snap

# Unnecessary generated / temp root files
test_output.txt
tsc_output.txt
tsc_output_2.txt
lint_output.txt
lint_final_error.txt
final_lint_check.txt
contracts/clippy_output.txt
issue*.json
PR_*.md
*_FIX*.md
*_FIX_GUIDE*.md
COMPLETION_SUMMARY.md
RACE_CONDITION_FIX.md
JS_BUNDLE_FIX.md
BUNDLE_AUDIT.md
BUILD_FIX_GUIDE.md
QUICK_START.md
PR_CI_Optimizations.md
FORMATTING.md
DESIGN_SYSTEM_INTEGRATION.md
DESIGN_SYSTEM_IMPLEMENTATION.md
DESIGN_SYSTEM_SETUP.md
WCAG_COMPLIANCE.md

# Backup / duplicate files
*.backup
*\ copy.*
package.json.backup
SubTrackr
44 changes: 44 additions & 0 deletions src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ const PaymentMethodsScreen = lazyScreen(() =>
);
const AnalyticsDashboard = lazyScreen(() => import('../../app/screens/AnalyticsDashboard'));

// Issue #547: GDPR
const PrivacyCenterScreen = lazyScreen(() => import('../screens/PrivacyCenterScreen'));
const DataExportScreen = lazyScreen(() => import('../screens/DataExportScreen'));
// Issue #548: Push notifications
const NotificationPreferencesScreen = lazyScreen(() => import('../screens/NotificationPreferencesScreen'));
// Issue #549: Email templates
const EmailTemplateEditorScreen = lazyScreen(() => import('../screens/EmailTemplateEditorScreen'));
// Issue #550: Advanced dunning
const DunningDashboardScreen = lazyScreen(() => import('../screens/DunningDashboardScreen'));

const Tab = createBottomTabNavigator<TabParamList>();
const Stack = createNativeStackNavigator<RootStackParamList>();

Expand Down Expand Up @@ -355,6 +365,40 @@ const SettingsStack = () => (
component={AnalyticsDashboard}
options={{ title: 'Analytics Dashboard', headerShown: true }}
/>
{/* Issue #547: GDPR */}
<Stack.Screen
name="PrivacyCenter"
component={PrivacyCenterScreen}
options={{ title: 'Privacy Center', headerShown: true }}
/>
<Stack.Screen
name="DataExport"
component={DataExportScreen}
options={{ title: 'Export My Data', headerShown: true }}
/>
<Stack.Screen
name="DPALog"
component={DataExportScreen}
options={{ title: 'Data Processing Log', headerShown: true }}
/>
{/* Issue #548: Push notifications */}
<Stack.Screen
name="NotificationPreferences"
component={NotificationPreferencesScreen}
options={{ title: 'Notification Preferences', headerShown: true }}
/>
{/* Issue #549: Email templates */}
<Stack.Screen
name="EmailTemplateEditor"
component={EmailTemplateEditorScreen}
options={{ title: 'Email Template Editor', headerShown: true }}
/>
{/* Issue #550: Advanced dunning */}
<Stack.Screen
name="DunningDashboard"
component={DunningDashboardScreen}
options={{ title: 'Dunning Dashboard', headerShown: true }}
/>
</Stack.Navigator>
);

Expand Down
10 changes: 10 additions & 0 deletions src/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ export type RootStackParamList = {
PaymentMethods: undefined;
AnalyticsDashboard: undefined;
NotFound: { reason?: string };
// Issue #547: GDPR
PrivacyCenter: undefined;
DataExport: undefined;
DPALog: undefined;
// Issue #548: Push notifications
NotificationPreferences: undefined;
// Issue #549: Email templates
EmailTemplateEditor: undefined;
// Issue #550: Advanced dunning
DunningDashboard: undefined;
};

export type TabParamList = {
Expand Down
163 changes: 163 additions & 0 deletions src/screens/DataExportScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} from 'react-native';
import { useThemeColors } from '../hooks/useThemeColors';
import { gdprService, PII_FIELDS } from '../services/gdpr';

const DataExportScreen = () => {
const colors = useThemeColors();
const styles = React.useMemo(() => createStyles(colors), [colors]);
const [loading, setLoading] = useState(false);
const [lastExport, setLastExport] = useState<{ url: string; timestamp: string } | null>(null);

const handleExport = async () => {
setLoading(true);
try {
const result = await gdprService.exportData();
setLastExport({ url: result.url, timestamp: result.timestamp });
await gdprService.downloadData(result);
} catch {
Alert.alert('Error', 'Could not prepare your data export. Please try again later.');
} finally {
setLoading(false);
}
};

const categoryGroups = Array.from(
PII_FIELDS.reduce((acc, field) => {
const list = acc.get(field.category) ?? [];
list.push(field);
acc.set(field.category, list);
return acc;
}, new Map<string, typeof PII_FIELDS>())
);

return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
accessibilityLabel="Data Export screen">
<Text style={styles.title}>Export Your Data</Text>
<Text style={styles.subtitle}>
Under GDPR Article 20, you have the right to receive a machine-readable copy of all personal
data we hold about you.
</Text>

{/* Data categories included */}
<View style={styles.card}>
<Text style={styles.cardTitle}>What's included</Text>
{categoryGroups.map(([category, fields]) => (
<View key={category} style={styles.categoryRow}>
<Text style={styles.categoryLabel}>{category.charAt(0).toUpperCase() + category.slice(1)}</Text>
<Text style={styles.categoryFields}>{fields.map((f) => f.field).join(', ')}</Text>
</View>
))}
</View>

{/* Export info */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Export details</Text>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Format</Text>
<Text style={styles.infoValue}>JSON (machine-readable)</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Encryption</Text>
<Text style={styles.infoValue}>AES-256, fields annotated</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Delivery</Text>
<Text style={styles.infoValue}>Sent to registered email</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Processing time</Text>
<Text style={styles.infoValue}>Within 72 hours</Text>
</View>
</View>

{lastExport && (
<View style={styles.card}>
<Text style={styles.cardTitle}>Last export</Text>
<Text style={styles.infoValue}>
Generated: {new Date(lastExport.timestamp).toLocaleString()}
</Text>
<Text style={styles.infoValue} numberOfLines={1}>
URL: {lastExport.url}
</Text>
</View>
)}

<TouchableOpacity
style={[styles.exportBtn, loading && styles.exportBtnDisabled]}
onPress={handleExport}
disabled={loading}
accessibilityRole="button"
accessibilityLabel="Request data export"
accessibilityState={{ disabled: loading, busy: loading }}>
{loading ? (
<ActivityIndicator color={colors.onPrimary} />
) : (
<Text style={styles.exportBtnText}>📦 Request Data Export</Text>
)}
</TouchableOpacity>

<Text style={styles.footer}>
Your export request is logged in the Data Processing Activity register as required by GDPR
Article 30.
</Text>
</ScrollView>
);
};

function createStyles(colors: ReturnType<typeof useThemeColors>) {
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.background.primary },
content: { padding: 16, paddingBottom: 40 },
title: { fontSize: 24, fontWeight: '700', color: colors.text.primary, marginBottom: 8 },
subtitle: { fontSize: 14, color: colors.textSecondary, marginBottom: 20, lineHeight: 20 },
card: {
backgroundColor: colors.background.card,
borderRadius: 12,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: colors.border.default,
},
cardTitle: { fontSize: 16, fontWeight: '700', color: colors.text.primary, marginBottom: 12 },
categoryRow: {
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: colors.border.default,
},
categoryLabel: { fontSize: 13, fontWeight: '600', color: colors.text.primary, textTransform: 'capitalize' },
categoryFields: { fontSize: 12, color: colors.textSecondary, marginTop: 2 },
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: colors.border.default,
},
infoLabel: { fontSize: 13, color: colors.textSecondary },
infoValue: { fontSize: 13, color: colors.text.primary, maxWidth: '60%', textAlign: 'right' },
exportBtn: {
backgroundColor: colors.primary,
padding: 16,
borderRadius: 10,
alignItems: 'center',
marginBottom: 16,
},
exportBtnDisabled: { opacity: 0.6 },
exportBtnText: { color: colors.onPrimary, fontSize: 16, fontWeight: '700' },
footer: { fontSize: 12, color: colors.textSecondary, textAlign: 'center', lineHeight: 18 },
});
}

export default DataExportScreen;
Loading