Skip to content

Commit d171f57

Browse files
authored
Merge pull request #661 from Young850/feature/issues-547-548-549-550
feat: implement issues #547-#550 (GDPR, push notifications, email templates, dunning)
2 parents 68d8e0e + 0c56d9e commit d171f57

16 files changed

Lines changed: 2923 additions & 29 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ DESIGN_SYSTEM_INTEGRATION.md
128128
DESIGN_SYSTEM_IMPLEMENTATION.md
129129
DESIGN_SYSTEM_SETUP.md
130130
WCAG_COMPLIANCE.md
131+
132+
# Backup / duplicate files
133+
*.backup
134+
*\ copy.*
135+
package.json.backup
136+
SubTrackr
131137
FORMATTING.md
132138
package.json.backup
133139
# Test run artifacts (NOT the committed contract insta snapshots under

src/navigation/AppNavigator.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ const PaymentMethodsScreen = lazyScreen(() =>
8383
);
8484
const AnalyticsDashboard = lazyScreen(() => import('../../app/screens/AnalyticsDashboard'));
8585

86+
// Issue #547: GDPR
87+
const PrivacyCenterScreen = lazyScreen(() => import('../screens/PrivacyCenterScreen'));
88+
const DataExportScreen = lazyScreen(() => import('../screens/DataExportScreen'));
89+
// Issue #548: Push notifications
90+
const NotificationPreferencesScreen = lazyScreen(() => import('../screens/NotificationPreferencesScreen'));
91+
// Issue #549: Email templates
92+
const EmailTemplateEditorScreen = lazyScreen(() => import('../screens/EmailTemplateEditorScreen'));
93+
// Issue #550: Advanced dunning
94+
const DunningDashboardScreen = lazyScreen(() => import('../screens/DunningDashboardScreen'));
95+
8696
const Tab = createBottomTabNavigator<TabParamList>();
8797
const Stack = createNativeStackNavigator<RootStackParamList>();
8898

@@ -385,6 +395,40 @@ const SettingsStack = () => (
385395
component={AnalyticsDashboard}
386396
options={{ title: 'Analytics Dashboard', headerShown: true }}
387397
/>
398+
{/* Issue #547: GDPR */}
399+
<Stack.Screen
400+
name="PrivacyCenter"
401+
component={PrivacyCenterScreen}
402+
options={{ title: 'Privacy Center', headerShown: true }}
403+
/>
404+
<Stack.Screen
405+
name="DataExport"
406+
component={DataExportScreen}
407+
options={{ title: 'Export My Data', headerShown: true }}
408+
/>
409+
<Stack.Screen
410+
name="DPALog"
411+
component={DataExportScreen}
412+
options={{ title: 'Data Processing Log', headerShown: true }}
413+
/>
414+
{/* Issue #548: Push notifications */}
415+
<Stack.Screen
416+
name="NotificationPreferences"
417+
component={NotificationPreferencesScreen}
418+
options={{ title: 'Notification Preferences', headerShown: true }}
419+
/>
420+
{/* Issue #549: Email templates */}
421+
<Stack.Screen
422+
name="EmailTemplateEditor"
423+
component={EmailTemplateEditorScreen}
424+
options={{ title: 'Email Template Editor', headerShown: true }}
425+
/>
426+
{/* Issue #550: Advanced dunning */}
427+
<Stack.Screen
428+
name="DunningDashboard"
429+
component={DunningDashboardScreen}
430+
options={{ title: 'Dunning Dashboard', headerShown: true }}
431+
/>
388432
</Stack.Navigator>
389433
);
390434

src/navigation/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ export type RootStackParamList = {
5454
AnalyticsDashboard: undefined;
5555
PartnerDashboard: undefined;
5656
NotFound: { reason?: string };
57+
// Issue #547: GDPR
58+
PrivacyCenter: undefined;
59+
DataExport: undefined;
60+
DPALog: undefined;
61+
// Issue #548: Push notifications
62+
NotificationPreferences: undefined;
63+
// Issue #549: Email templates
64+
EmailTemplateEditor: undefined;
65+
// Issue #550: Advanced dunning
66+
DunningDashboard: undefined;
5767
};
5868

5969
export type TabParamList = {

src/screens/DataExportScreen.tsx

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import React, { useState } from 'react';
2+
import {
3+
View,
4+
Text,
5+
StyleSheet,
6+
ScrollView,
7+
TouchableOpacity,
8+
Alert,
9+
ActivityIndicator,
10+
} from 'react-native';
11+
import { useThemeColors } from '../hooks/useThemeColors';
12+
import { gdprService, PII_FIELDS } from '../services/gdpr';
13+
14+
const DataExportScreen = () => {
15+
const colors = useThemeColors();
16+
const styles = React.useMemo(() => createStyles(colors), [colors]);
17+
const [loading, setLoading] = useState(false);
18+
const [lastExport, setLastExport] = useState<{ url: string; timestamp: string } | null>(null);
19+
20+
const handleExport = async () => {
21+
setLoading(true);
22+
try {
23+
const result = await gdprService.exportData();
24+
setLastExport({ url: result.url, timestamp: result.timestamp });
25+
await gdprService.downloadData(result);
26+
} catch {
27+
Alert.alert('Error', 'Could not prepare your data export. Please try again later.');
28+
} finally {
29+
setLoading(false);
30+
}
31+
};
32+
33+
const categoryGroups = Array.from(
34+
PII_FIELDS.reduce((acc, field) => {
35+
const list = acc.get(field.category) ?? [];
36+
list.push(field);
37+
acc.set(field.category, list);
38+
return acc;
39+
}, new Map<string, typeof PII_FIELDS>())
40+
);
41+
42+
return (
43+
<ScrollView
44+
style={styles.container}
45+
contentContainerStyle={styles.content}
46+
accessibilityLabel="Data Export screen">
47+
<Text style={styles.title}>Export Your Data</Text>
48+
<Text style={styles.subtitle}>
49+
Under GDPR Article 20, you have the right to receive a machine-readable copy of all personal
50+
data we hold about you.
51+
</Text>
52+
53+
{/* Data categories included */}
54+
<View style={styles.card}>
55+
<Text style={styles.cardTitle}>What's included</Text>
56+
{categoryGroups.map(([category, fields]) => (
57+
<View key={category} style={styles.categoryRow}>
58+
<Text style={styles.categoryLabel}>{category.charAt(0).toUpperCase() + category.slice(1)}</Text>
59+
<Text style={styles.categoryFields}>{fields.map((f) => f.field).join(', ')}</Text>
60+
</View>
61+
))}
62+
</View>
63+
64+
{/* Export info */}
65+
<View style={styles.card}>
66+
<Text style={styles.cardTitle}>Export details</Text>
67+
<View style={styles.infoRow}>
68+
<Text style={styles.infoLabel}>Format</Text>
69+
<Text style={styles.infoValue}>JSON (machine-readable)</Text>
70+
</View>
71+
<View style={styles.infoRow}>
72+
<Text style={styles.infoLabel}>Encryption</Text>
73+
<Text style={styles.infoValue}>AES-256, fields annotated</Text>
74+
</View>
75+
<View style={styles.infoRow}>
76+
<Text style={styles.infoLabel}>Delivery</Text>
77+
<Text style={styles.infoValue}>Sent to registered email</Text>
78+
</View>
79+
<View style={styles.infoRow}>
80+
<Text style={styles.infoLabel}>Processing time</Text>
81+
<Text style={styles.infoValue}>Within 72 hours</Text>
82+
</View>
83+
</View>
84+
85+
{lastExport && (
86+
<View style={styles.card}>
87+
<Text style={styles.cardTitle}>Last export</Text>
88+
<Text style={styles.infoValue}>
89+
Generated: {new Date(lastExport.timestamp).toLocaleString()}
90+
</Text>
91+
<Text style={styles.infoValue} numberOfLines={1}>
92+
URL: {lastExport.url}
93+
</Text>
94+
</View>
95+
)}
96+
97+
<TouchableOpacity
98+
style={[styles.exportBtn, loading && styles.exportBtnDisabled]}
99+
onPress={handleExport}
100+
disabled={loading}
101+
accessibilityRole="button"
102+
accessibilityLabel="Request data export"
103+
accessibilityState={{ disabled: loading, busy: loading }}>
104+
{loading ? (
105+
<ActivityIndicator color={colors.onPrimary} />
106+
) : (
107+
<Text style={styles.exportBtnText}>📦 Request Data Export</Text>
108+
)}
109+
</TouchableOpacity>
110+
111+
<Text style={styles.footer}>
112+
Your export request is logged in the Data Processing Activity register as required by GDPR
113+
Article 30.
114+
</Text>
115+
</ScrollView>
116+
);
117+
};
118+
119+
function createStyles(colors: ReturnType<typeof useThemeColors>) {
120+
return StyleSheet.create({
121+
container: { flex: 1, backgroundColor: colors.background.primary },
122+
content: { padding: 16, paddingBottom: 40 },
123+
title: { fontSize: 24, fontWeight: '700', color: colors.text.primary, marginBottom: 8 },
124+
subtitle: { fontSize: 14, color: colors.textSecondary, marginBottom: 20, lineHeight: 20 },
125+
card: {
126+
backgroundColor: colors.background.card,
127+
borderRadius: 12,
128+
padding: 16,
129+
marginBottom: 16,
130+
borderWidth: 1,
131+
borderColor: colors.border.default,
132+
},
133+
cardTitle: { fontSize: 16, fontWeight: '700', color: colors.text.primary, marginBottom: 12 },
134+
categoryRow: {
135+
paddingVertical: 8,
136+
borderBottomWidth: 1,
137+
borderBottomColor: colors.border.default,
138+
},
139+
categoryLabel: { fontSize: 13, fontWeight: '600', color: colors.text.primary, textTransform: 'capitalize' },
140+
categoryFields: { fontSize: 12, color: colors.textSecondary, marginTop: 2 },
141+
infoRow: {
142+
flexDirection: 'row',
143+
justifyContent: 'space-between',
144+
paddingVertical: 8,
145+
borderBottomWidth: 1,
146+
borderBottomColor: colors.border.default,
147+
},
148+
infoLabel: { fontSize: 13, color: colors.textSecondary },
149+
infoValue: { fontSize: 13, color: colors.text.primary, maxWidth: '60%', textAlign: 'right' },
150+
exportBtn: {
151+
backgroundColor: colors.primary,
152+
padding: 16,
153+
borderRadius: 10,
154+
alignItems: 'center',
155+
marginBottom: 16,
156+
},
157+
exportBtnDisabled: { opacity: 0.6 },
158+
exportBtnText: { color: colors.onPrimary, fontSize: 16, fontWeight: '700' },
159+
footer: { fontSize: 12, color: colors.textSecondary, textAlign: 'center', lineHeight: 18 },
160+
});
161+
}
162+
163+
export default DataExportScreen;

0 commit comments

Comments
 (0)