@tantainnovative/ndpr-toolkit 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/eslint.config.mjs +16 -0
  2. package/next.config.js +15 -0
  3. package/next.config.ts +62 -0
  4. package/package.json +1 -1
  5. package/packages/ndpr-toolkit/README.md +467 -0
  6. package/packages/ndpr-toolkit/dist/components/breach/BreachNotificationManager.d.ts +62 -0
  7. package/packages/ndpr-toolkit/dist/components/breach/BreachReportForm.d.ts +66 -0
  8. package/packages/ndpr-toolkit/dist/components/breach/BreachRiskAssessment.d.ts +50 -0
  9. package/packages/ndpr-toolkit/dist/components/breach/RegulatoryReportGenerator.d.ts +94 -0
  10. package/packages/ndpr-toolkit/dist/components/consent/ConsentBanner.d.ts +79 -0
  11. package/packages/ndpr-toolkit/dist/components/consent/ConsentManager.d.ts +73 -0
  12. package/packages/ndpr-toolkit/dist/components/consent/ConsentStorage.d.ts +41 -0
  13. package/packages/ndpr-toolkit/dist/components/dpia/DPIAQuestionnaire.d.ts +70 -0
  14. package/packages/ndpr-toolkit/dist/components/dpia/DPIAReport.d.ts +40 -0
  15. package/packages/ndpr-toolkit/dist/components/dpia/StepIndicator.d.ts +64 -0
  16. package/packages/ndpr-toolkit/dist/components/dsr/DSRDashboard.d.ts +58 -0
  17. package/packages/ndpr-toolkit/dist/components/dsr/DSRRequestForm.d.ts +74 -0
  18. package/packages/ndpr-toolkit/dist/components/dsr/DSRTracker.d.ts +56 -0
  19. package/packages/ndpr-toolkit/dist/components/policy/PolicyExporter.d.ts +65 -0
  20. package/packages/ndpr-toolkit/dist/components/policy/PolicyGenerator.d.ts +54 -0
  21. package/packages/ndpr-toolkit/dist/components/policy/PolicyPreview.d.ts +71 -0
  22. package/packages/ndpr-toolkit/dist/hooks/useBreach.d.ts +97 -0
  23. package/packages/ndpr-toolkit/dist/hooks/useConsent.d.ts +63 -0
  24. package/packages/ndpr-toolkit/dist/hooks/useDPIA.d.ts +92 -0
  25. package/packages/ndpr-toolkit/dist/hooks/useDSR.d.ts +72 -0
  26. package/packages/ndpr-toolkit/dist/hooks/usePrivacyPolicy.d.ts +87 -0
  27. package/packages/ndpr-toolkit/dist/index.d.ts +31 -0
  28. package/packages/ndpr-toolkit/dist/index.esm.js +2 -0
  29. package/packages/ndpr-toolkit/dist/index.esm.js.map +1 -0
  30. package/packages/ndpr-toolkit/dist/index.js +2 -0
  31. package/packages/ndpr-toolkit/dist/index.js.map +1 -0
  32. package/packages/ndpr-toolkit/dist/setupTests.d.ts +2 -0
  33. package/packages/ndpr-toolkit/dist/types/breach.d.ts +239 -0
  34. package/packages/ndpr-toolkit/dist/types/consent.d.ts +95 -0
  35. package/packages/ndpr-toolkit/dist/types/dpia.d.ts +196 -0
  36. package/packages/ndpr-toolkit/dist/types/dsr.d.ts +162 -0
  37. package/packages/ndpr-toolkit/dist/types/privacy.d.ts +204 -0
  38. package/packages/ndpr-toolkit/dist/utils/breach.d.ts +14 -0
  39. package/packages/ndpr-toolkit/dist/utils/consent.d.ts +10 -0
  40. package/packages/ndpr-toolkit/dist/utils/dpia.d.ts +12 -0
  41. package/packages/ndpr-toolkit/dist/utils/dsr.d.ts +11 -0
  42. package/packages/ndpr-toolkit/dist/utils/privacy.d.ts +12 -0
  43. package/packages/ndpr-toolkit/package-lock.json +8197 -0
  44. package/packages/ndpr-toolkit/package.json +71 -0
  45. package/packages/ndpr-toolkit/rollup.config.js +34 -0
  46. package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
  47. package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
  48. package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
  49. package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
  50. package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
  51. package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
  52. package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
  53. package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
  54. package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
  55. package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
  56. package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
  57. package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
  58. package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
  59. package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
  60. package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
  61. package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
  62. package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
  63. package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
  64. package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
  65. package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
  66. package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
  67. package/packages/ndpr-toolkit/src/index.ts +44 -0
  68. package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
  69. package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
  70. package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
  71. package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
  72. package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
  73. package/packages/ndpr-toolkit/src/types/index.ts +42 -0
  74. package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
  75. package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
  76. package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
  77. package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
  78. package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
  79. package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
  80. package/packages/ndpr-toolkit/tsconfig.json +23 -0
  81. package/postcss.config.mjs +5 -0
  82. package/src/app/accessibility.css +70 -0
  83. package/src/app/favicon.ico +0 -0
  84. package/src/app/globals.css +123 -0
  85. package/src/app/layout.tsx +37 -0
  86. package/src/app/ndpr-demos/breach/page.tsx +354 -0
  87. package/src/app/ndpr-demos/consent/page.tsx +366 -0
  88. package/src/app/ndpr-demos/dpia/page.tsx +495 -0
  89. package/src/app/ndpr-demos/dsr/page.tsx +280 -0
  90. package/src/app/ndpr-demos/page.tsx +73 -0
  91. package/src/app/ndpr-demos/policy/page.tsx +771 -0
  92. package/src/app/page.tsx +452 -0
  93. package/src/components/ErrorBoundary.tsx +90 -0
  94. package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
  95. package/src/components/consent/ConsentBanner.tsx +193 -0
  96. package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +530 -0
  97. package/src/components/dpia/DPIAQuestionnaire.tsx +523 -0
  98. package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
  99. package/src/components/privacy-policy/data.ts +98 -0
  100. package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
  101. package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
  102. package/src/components/privacy-policy/shared/FormField.tsx +79 -0
  103. package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
  104. package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +361 -0
  105. package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
  106. package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
  107. package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
  108. package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +226 -0
  109. package/src/components/ui/Badge.tsx +46 -0
  110. package/src/components/ui/Button.tsx +59 -0
  111. package/src/components/ui/Card.tsx +92 -0
  112. package/src/components/ui/Checkbox.tsx +57 -0
  113. package/src/components/ui/FormField.tsx +50 -0
  114. package/src/components/ui/Input.tsx +38 -0
  115. package/src/components/ui/Loading.tsx +201 -0
  116. package/src/components/ui/Select.tsx +42 -0
  117. package/src/components/ui/TextArea.tsx +38 -0
  118. package/src/components/ui/label.tsx +24 -0
  119. package/src/components/ui/switch.tsx +31 -0
  120. package/src/components/ui/tabs.tsx +66 -0
  121. package/src/hooks/useConsent.ts +70 -0
  122. package/src/hooks/useLoadingState.ts +85 -0
  123. package/src/lib/consentService.ts +144 -0
  124. package/src/lib/dpiaQuestions.ts +188 -0
  125. package/src/lib/requestService.ts +79 -0
  126. package/src/lib/sanitize.ts +108 -0
  127. package/src/lib/storage.ts +222 -0
  128. package/src/lib/utils.ts +6 -0
  129. package/src/types/html-to-docx.d.ts +30 -0
  130. package/src/types/index.ts +77 -0
  131. package/tailwind.config.ts +65 -0
  132. package/tsconfig.json +41 -0
@@ -0,0 +1,409 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { BreachReport, BreachCategory, RiskAssessment, NotificationRequirement, RegulatoryNotification } from '../types/breach';
3
+ import { calculateBreachSeverity } from '../utils/breach';
4
+
5
+ interface UseBreachOptions {
6
+ /**
7
+ * Available breach categories
8
+ */
9
+ categories: BreachCategory[];
10
+
11
+ /**
12
+ * Initial breach reports
13
+ */
14
+ initialReports?: BreachReport[];
15
+
16
+ /**
17
+ * Storage key for breach data
18
+ * @default "ndpr_breach_data"
19
+ */
20
+ storageKey?: string;
21
+
22
+ /**
23
+ * Whether to use local storage to persist breach data
24
+ * @default true
25
+ */
26
+ useLocalStorage?: boolean;
27
+
28
+ /**
29
+ * Callback function called when a breach is reported
30
+ */
31
+ onReport?: (report: BreachReport) => void;
32
+
33
+ /**
34
+ * Callback function called when a risk assessment is completed
35
+ */
36
+ onAssessment?: (assessment: RiskAssessment) => void;
37
+
38
+ /**
39
+ * Callback function called when a notification is sent
40
+ */
41
+ onNotification?: (notification: RegulatoryNotification) => void;
42
+ }
43
+
44
+ interface UseBreachReturn {
45
+ /**
46
+ * All breach reports
47
+ */
48
+ reports: BreachReport[];
49
+
50
+ /**
51
+ * All risk assessments
52
+ */
53
+ assessments: RiskAssessment[];
54
+
55
+ /**
56
+ * All regulatory notifications
57
+ */
58
+ notifications: RegulatoryNotification[];
59
+
60
+ /**
61
+ * Submit a new breach report
62
+ */
63
+ reportBreach: (reportData: Omit<BreachReport, 'id' | 'reportedAt'>) => BreachReport;
64
+
65
+ /**
66
+ * Update an existing breach report
67
+ */
68
+ updateReport: (id: string, updates: Partial<BreachReport>) => BreachReport | null;
69
+
70
+ /**
71
+ * Get a breach report by ID
72
+ */
73
+ getReport: (id: string) => BreachReport | null;
74
+
75
+ /**
76
+ * Conduct a risk assessment for a breach
77
+ */
78
+ assessRisk: (breachId: string, assessmentData: Omit<RiskAssessment, 'id' | 'breachId' | 'assessedAt'>) => RiskAssessment;
79
+
80
+ /**
81
+ * Get a risk assessment for a breach
82
+ */
83
+ getAssessment: (breachId: string) => RiskAssessment | null;
84
+
85
+ /**
86
+ * Calculate notification requirements based on a risk assessment
87
+ */
88
+ calculateNotificationRequirements: (breachId: string) => NotificationRequirement | null;
89
+
90
+ /**
91
+ * Send a regulatory notification
92
+ */
93
+ sendNotification: (breachId: string, notificationData: Omit<RegulatoryNotification, 'id' | 'breachId' | 'sentAt'>) => RegulatoryNotification;
94
+
95
+ /**
96
+ * Get a regulatory notification for a breach
97
+ */
98
+ getNotification: (breachId: string) => RegulatoryNotification | null;
99
+
100
+ /**
101
+ * Get breaches that require notification within the next X hours
102
+ */
103
+ getBreachesRequiringNotification: (hoursThreshold?: number) => Array<{
104
+ report: BreachReport;
105
+ assessment: RiskAssessment;
106
+ requirements: NotificationRequirement;
107
+ hoursRemaining: number;
108
+ }>;
109
+
110
+ /**
111
+ * Clear all breach data
112
+ */
113
+ clearBreachData: () => void;
114
+ }
115
+
116
+ /**
117
+ * Hook for managing data breach notifications in compliance with NDPR
118
+ */
119
+ export function useBreach({
120
+ categories,
121
+ initialReports = [],
122
+ storageKey = "ndpr_breach_data",
123
+ useLocalStorage = true,
124
+ onReport,
125
+ onAssessment,
126
+ onNotification
127
+ }: UseBreachOptions): UseBreachReturn {
128
+ const [reports, setReports] = useState<BreachReport[]>(initialReports);
129
+ const [assessments, setAssessments] = useState<RiskAssessment[]>([]);
130
+ const [notifications, setNotifications] = useState<RegulatoryNotification[]>([]);
131
+
132
+ // Load breach data from storage on mount
133
+ useEffect(() => {
134
+ if (useLocalStorage && typeof window !== 'undefined') {
135
+ try {
136
+ const savedData = localStorage.getItem(storageKey);
137
+ if (savedData) {
138
+ const { reports, assessments, notifications } = JSON.parse(savedData);
139
+ setReports(reports || []);
140
+ setAssessments(assessments || []);
141
+ setNotifications(notifications || []);
142
+ }
143
+ } catch (error) {
144
+ console.error('Error loading breach data:', error);
145
+ }
146
+ }
147
+ }, [storageKey, useLocalStorage]);
148
+
149
+ // Save breach data to storage when it changes
150
+ useEffect(() => {
151
+ if (useLocalStorage && typeof window !== 'undefined') {
152
+ try {
153
+ localStorage.setItem(storageKey, JSON.stringify({
154
+ reports,
155
+ assessments,
156
+ notifications
157
+ }));
158
+ } catch (error) {
159
+ console.error('Error saving breach data:', error);
160
+ }
161
+ }
162
+ }, [reports, assessments, notifications, storageKey, useLocalStorage]);
163
+
164
+ // Generate a unique ID
165
+ const generateId = (prefix: string): string => {
166
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
167
+ };
168
+
169
+ // Submit a new breach report
170
+ const reportBreach = (reportData: Omit<BreachReport, 'id' | 'reportedAt'>): BreachReport => {
171
+ const newReport: BreachReport = {
172
+ id: generateId('breach'),
173
+ reportedAt: Date.now(),
174
+ ...reportData
175
+ };
176
+
177
+ setReports(prevReports => [...prevReports, newReport]);
178
+
179
+ if (onReport) {
180
+ onReport(newReport);
181
+ }
182
+
183
+ return newReport;
184
+ };
185
+
186
+ // Update an existing breach report
187
+ const updateReport = (id: string, updates: Partial<BreachReport>): BreachReport | null => {
188
+ let updatedReport: BreachReport | null = null;
189
+
190
+ setReports(prevReports => {
191
+ const index = prevReports.findIndex(report => report.id === id);
192
+
193
+ if (index === -1) {
194
+ return prevReports;
195
+ }
196
+
197
+ const report = prevReports[index];
198
+ updatedReport = {
199
+ ...report,
200
+ ...updates
201
+ };
202
+
203
+ const newReports = [...prevReports];
204
+ newReports[index] = updatedReport as BreachReport;
205
+
206
+ return newReports;
207
+ });
208
+
209
+ return updatedReport;
210
+ };
211
+
212
+ // Get a breach report by ID
213
+ const getReport = (id: string): BreachReport | null => {
214
+ return reports.find(report => report.id === id) || null;
215
+ };
216
+
217
+ // Conduct a risk assessment for a breach
218
+ const assessRisk = (breachId: string, assessmentData: Omit<RiskAssessment, 'id' | 'breachId' | 'assessedAt'>): RiskAssessment => {
219
+ // Check if an assessment already exists for this breach
220
+ const existingAssessment = assessments.find(assessment => assessment.breachId === breachId);
221
+
222
+ if (existingAssessment) {
223
+ // Update the existing assessment
224
+ const updatedAssessment: RiskAssessment = {
225
+ ...existingAssessment,
226
+ ...assessmentData,
227
+ assessedAt: Date.now()
228
+ };
229
+
230
+ setAssessments(prevAssessments =>
231
+ prevAssessments.map(assessment =>
232
+ assessment.id === existingAssessment.id ? updatedAssessment : assessment
233
+ )
234
+ );
235
+
236
+ if (onAssessment) {
237
+ onAssessment(updatedAssessment);
238
+ }
239
+
240
+ return updatedAssessment;
241
+ } else {
242
+ // Create a new assessment
243
+ const newAssessment: RiskAssessment = {
244
+ id: generateId('assessment'),
245
+ breachId,
246
+ assessedAt: Date.now(),
247
+ ...assessmentData
248
+ };
249
+
250
+ setAssessments(prevAssessments => [...prevAssessments, newAssessment]);
251
+
252
+ if (onAssessment) {
253
+ onAssessment(newAssessment);
254
+ }
255
+
256
+ return newAssessment;
257
+ }
258
+ };
259
+
260
+ // Get a risk assessment for a breach
261
+ const getAssessment = (breachId: string): RiskAssessment | null => {
262
+ return assessments.find(assessment => assessment.breachId === breachId) || null;
263
+ };
264
+
265
+ // Calculate notification requirements based on a risk assessment
266
+ const calculateNotificationRequirements = (breachId: string): NotificationRequirement | null => {
267
+ const report = getReport(breachId);
268
+ const assessment = getAssessment(breachId);
269
+
270
+ if (!report) {
271
+ return null;
272
+ }
273
+
274
+ const { severityLevel, notificationRequired, timeframeHours, justification } = calculateBreachSeverity(report, assessment || undefined);
275
+
276
+ // Calculate the deadline (72 hours from discovery under NDPR)
277
+ const deadline = report.discoveredAt + (timeframeHours * 60 * 60 * 1000);
278
+
279
+ return {
280
+ nitdaNotificationRequired: notificationRequired,
281
+ nitdaNotificationDeadline: deadline,
282
+ dataSubjectNotificationRequired: severityLevel === 'high' || severityLevel === 'critical',
283
+ justification
284
+ };
285
+ };
286
+
287
+ // Send a regulatory notification
288
+ const sendNotification = (breachId: string, notificationData: Omit<RegulatoryNotification, 'id' | 'breachId' | 'sentAt'>): RegulatoryNotification => {
289
+ // Check if a notification already exists for this breach
290
+ const existingNotification = notifications.find(notification => notification.breachId === breachId);
291
+
292
+ if (existingNotification) {
293
+ // Update the existing notification
294
+ const updatedNotification: RegulatoryNotification = {
295
+ ...existingNotification,
296
+ ...notificationData,
297
+ sentAt: Date.now()
298
+ };
299
+
300
+ setNotifications(prevNotifications =>
301
+ prevNotifications.map(notification =>
302
+ notification.id === existingNotification.id ? updatedNotification : notification
303
+ )
304
+ );
305
+
306
+ if (onNotification) {
307
+ onNotification(updatedNotification);
308
+ }
309
+
310
+ return updatedNotification;
311
+ } else {
312
+ // Create a new notification
313
+ const newNotification: RegulatoryNotification = {
314
+ id: generateId('notification'),
315
+ breachId,
316
+ sentAt: Date.now(),
317
+ ...notificationData
318
+ };
319
+
320
+ setNotifications(prevNotifications => [...prevNotifications, newNotification]);
321
+
322
+ if (onNotification) {
323
+ onNotification(newNotification);
324
+ }
325
+
326
+ return newNotification;
327
+ }
328
+ };
329
+
330
+ // Get a regulatory notification for a breach
331
+ const getNotification = (breachId: string): RegulatoryNotification | null => {
332
+ return notifications.find(notification => notification.breachId === breachId) || null;
333
+ };
334
+
335
+ // Get breaches that require notification within the next X hours
336
+ const getBreachesRequiringNotification = (hoursThreshold = 24): Array<{
337
+ report: BreachReport;
338
+ assessment: RiskAssessment;
339
+ requirements: NotificationRequirement;
340
+ hoursRemaining: number;
341
+ }> => {
342
+ const now = Date.now();
343
+ const result: Array<{
344
+ report: BreachReport;
345
+ assessment: RiskAssessment;
346
+ requirements: NotificationRequirement;
347
+ hoursRemaining: number;
348
+ }> = [];
349
+
350
+ reports.forEach(report => {
351
+ // Skip if a notification has already been sent
352
+ if (notifications.some(notification => notification.breachId === report.id)) {
353
+ return;
354
+ }
355
+
356
+ const assessment = getAssessment(report.id);
357
+ if (!assessment) {
358
+ return;
359
+ }
360
+
361
+ const requirements = calculateNotificationRequirements(report.id);
362
+ if (!requirements || !requirements.nitdaNotificationRequired) {
363
+ return;
364
+ }
365
+
366
+ const timeRemaining = requirements.nitdaNotificationDeadline - now;
367
+ const hoursRemaining = timeRemaining / (60 * 60 * 1000);
368
+
369
+ if (hoursRemaining <= hoursThreshold) {
370
+ result.push({
371
+ report,
372
+ assessment,
373
+ requirements,
374
+ hoursRemaining
375
+ });
376
+ }
377
+ });
378
+
379
+ // Sort by hours remaining (ascending)
380
+ return result.sort((a, b) => a.hoursRemaining - b.hoursRemaining);
381
+ };
382
+
383
+ // Clear all breach data
384
+ const clearBreachData = () => {
385
+ setReports([]);
386
+ setAssessments([]);
387
+ setNotifications([]);
388
+
389
+ if (useLocalStorage && typeof window !== 'undefined') {
390
+ localStorage.removeItem(storageKey);
391
+ }
392
+ };
393
+
394
+ return {
395
+ reports,
396
+ assessments,
397
+ notifications,
398
+ reportBreach,
399
+ updateReport,
400
+ getReport,
401
+ assessRisk,
402
+ getAssessment,
403
+ calculateNotificationRequirements,
404
+ sendNotification,
405
+ getNotification,
406
+ getBreachesRequiringNotification,
407
+ clearBreachData
408
+ };
409
+ }
@@ -0,0 +1,263 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { ConsentOption, ConsentSettings, ConsentStorageOptions } from '../types/consent';
3
+ import { validateConsent } from '../utils/consent';
4
+
5
+ interface UseConsentOptions {
6
+ /**
7
+ * Consent options to present to the user
8
+ */
9
+ options: ConsentOption[];
10
+
11
+ /**
12
+ * Storage options for consent settings
13
+ */
14
+ storageOptions?: ConsentStorageOptions;
15
+
16
+ /**
17
+ * Version of the consent form
18
+ * @default "1.0"
19
+ */
20
+ version?: string;
21
+
22
+ /**
23
+ * Callback function called when consent settings change
24
+ */
25
+ onChange?: (settings: ConsentSettings) => void;
26
+ }
27
+
28
+ interface UseConsentReturn {
29
+ /**
30
+ * Current consent settings
31
+ */
32
+ settings: ConsentSettings | null;
33
+
34
+ /**
35
+ * Whether consent has been given for a specific option
36
+ */
37
+ hasConsent: (optionId: string) => boolean;
38
+
39
+ /**
40
+ * Update consent settings
41
+ */
42
+ updateConsent: (consents: Record<string, boolean>) => void;
43
+
44
+ /**
45
+ * Accept all consent options
46
+ */
47
+ acceptAll: () => void;
48
+
49
+ /**
50
+ * Reject all non-required consent options
51
+ */
52
+ rejectAll: () => void;
53
+
54
+ /**
55
+ * Whether the consent banner should be shown
56
+ */
57
+ shouldShowBanner: boolean;
58
+
59
+ /**
60
+ * Whether consent settings are valid
61
+ */
62
+ isValid: boolean;
63
+
64
+ /**
65
+ * Validation errors (if any)
66
+ */
67
+ validationErrors: string[];
68
+
69
+ /**
70
+ * Reset consent settings (clear from storage)
71
+ */
72
+ resetConsent: () => void;
73
+ }
74
+
75
+ /**
76
+ * Hook for managing user consent in compliance with NDPR
77
+ */
78
+ export function useConsent({
79
+ options,
80
+ storageOptions = {},
81
+ version = "1.0",
82
+ onChange
83
+ }: UseConsentOptions): UseConsentReturn {
84
+ const {
85
+ storageKey = "ndpr_consent",
86
+ storageType = "localStorage"
87
+ } = storageOptions;
88
+
89
+ const [settings, setSettings] = useState<ConsentSettings | null>(null);
90
+ const [shouldShowBanner, setShouldShowBanner] = useState<boolean>(true);
91
+ const [isValid, setIsValid] = useState<boolean>(false);
92
+ const [validationErrors, setValidationErrors] = useState<string[]>([]);
93
+
94
+ // Load consent settings from storage on mount
95
+ useEffect(() => {
96
+ let savedSettings: ConsentSettings | null = null;
97
+
98
+ try {
99
+ if (storageType === 'localStorage' && typeof window !== 'undefined') {
100
+ const savedData = localStorage.getItem(storageKey);
101
+ if (savedData) {
102
+ savedSettings = JSON.parse(savedData);
103
+ }
104
+ } else if (storageType === 'sessionStorage' && typeof window !== 'undefined') {
105
+ const savedData = sessionStorage.getItem(storageKey);
106
+ if (savedData) {
107
+ savedSettings = JSON.parse(savedData);
108
+ }
109
+ } else if (storageType === 'cookie' && typeof document !== 'undefined') {
110
+ const cookies = document.cookie.split(';');
111
+ const consentCookie = cookies.find(cookie => cookie.trim().startsWith(`${storageKey}=`));
112
+ if (consentCookie) {
113
+ const cookieValue = consentCookie.split('=')[1];
114
+ savedSettings = JSON.parse(decodeURIComponent(cookieValue));
115
+ }
116
+ }
117
+ } catch (error) {
118
+ console.error('Error loading consent settings:', error);
119
+ }
120
+
121
+ if (savedSettings) {
122
+ setSettings(savedSettings);
123
+
124
+ // Validate the saved settings
125
+ const { valid, errors } = validateConsent(savedSettings);
126
+ setIsValid(valid);
127
+ setValidationErrors(errors);
128
+
129
+ // Only hide banner if settings are valid and for the current version
130
+ setShouldShowBanner(!(valid && savedSettings.version === version));
131
+ } else {
132
+ setShouldShowBanner(true);
133
+ }
134
+ }, [storageKey, storageType, version]);
135
+
136
+ // Save settings to storage
137
+ const saveSettings = (newSettings: ConsentSettings) => {
138
+ try {
139
+ const settingsString = JSON.stringify(newSettings);
140
+
141
+ if (storageType === 'localStorage' && typeof window !== 'undefined') {
142
+ localStorage.setItem(storageKey, settingsString);
143
+ } else if (storageType === 'sessionStorage' && typeof window !== 'undefined') {
144
+ sessionStorage.setItem(storageKey, settingsString);
145
+ } else if (storageType === 'cookie' && typeof document !== 'undefined') {
146
+ const { cookieOptions = {} } = storageOptions;
147
+ const {
148
+ domain,
149
+ path = '/',
150
+ expires = 365,
151
+ secure = true,
152
+ sameSite = 'Lax'
153
+ } = cookieOptions;
154
+
155
+ const expiryDate = new Date();
156
+ expiryDate.setDate(expiryDate.getDate() + expires);
157
+
158
+ let cookieString = `${storageKey}=${encodeURIComponent(settingsString)}; path=${path}; expires=${expiryDate.toUTCString()}`;
159
+
160
+ if (domain) {
161
+ cookieString += `; domain=${domain}`;
162
+ }
163
+
164
+ if (secure) {
165
+ cookieString += '; secure';
166
+ }
167
+
168
+ cookieString += `; samesite=${sameSite}`;
169
+
170
+ document.cookie = cookieString;
171
+ }
172
+
173
+ // Validate the new settings
174
+ const { valid, errors } = validateConsent(newSettings);
175
+ setIsValid(valid);
176
+ setValidationErrors(errors);
177
+
178
+ // Call onChange callback if provided
179
+ if (onChange) {
180
+ onChange(newSettings);
181
+ }
182
+ } catch (error) {
183
+ console.error('Error saving consent settings:', error);
184
+ }
185
+ };
186
+
187
+ // Update consent settings
188
+ const updateConsent = (consents: Record<string, boolean>) => {
189
+ const newSettings: ConsentSettings = {
190
+ consents,
191
+ timestamp: Date.now(),
192
+ version,
193
+ method: 'explicit',
194
+ hasInteracted: true
195
+ };
196
+
197
+ setSettings(newSettings);
198
+ saveSettings(newSettings);
199
+ setShouldShowBanner(false);
200
+ };
201
+
202
+ // Accept all consent options
203
+ const acceptAll = () => {
204
+ const allConsents: Record<string, boolean> = {};
205
+ options.forEach(option => {
206
+ allConsents[option.id] = true;
207
+ });
208
+
209
+ updateConsent(allConsents);
210
+ };
211
+
212
+ // Reject all non-required consent options
213
+ const rejectAll = () => {
214
+ const rejectedConsents: Record<string, boolean> = {};
215
+ options.forEach(option => {
216
+ rejectedConsents[option.id] = option.required || false;
217
+ });
218
+
219
+ updateConsent(rejectedConsents);
220
+ };
221
+
222
+ // Check if consent has been given for a specific option
223
+ const hasConsent = (optionId: string): boolean => {
224
+ return !!settings?.consents[optionId];
225
+ };
226
+
227
+ // Reset consent settings
228
+ const resetConsent = () => {
229
+ if (storageType === 'localStorage' && typeof window !== 'undefined') {
230
+ localStorage.removeItem(storageKey);
231
+ } else if (storageType === 'sessionStorage' && typeof window !== 'undefined') {
232
+ sessionStorage.removeItem(storageKey);
233
+ } else if (storageType === 'cookie' && typeof document !== 'undefined') {
234
+ const { cookieOptions = {} } = storageOptions;
235
+ const { domain, path = '/' } = cookieOptions;
236
+
237
+ let cookieString = `${storageKey}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
238
+
239
+ if (domain) {
240
+ cookieString += `; domain=${domain}`;
241
+ }
242
+
243
+ document.cookie = cookieString;
244
+ }
245
+
246
+ setSettings(null);
247
+ setShouldShowBanner(true);
248
+ setIsValid(false);
249
+ setValidationErrors([]);
250
+ };
251
+
252
+ return {
253
+ settings,
254
+ hasConsent,
255
+ updateConsent,
256
+ acceptAll,
257
+ rejectAll,
258
+ shouldShowBanner,
259
+ isValid,
260
+ validationErrors,
261
+ resetConsent
262
+ };
263
+ }