@terreno/ui 0.7.2 → 0.8.0

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 (82) hide show
  1. package/dist/BooleanField.js +23 -23
  2. package/dist/BooleanField.js.map +1 -1
  3. package/dist/ConsentFormScreen.d.ts +14 -0
  4. package/dist/ConsentFormScreen.js +93 -0
  5. package/dist/ConsentFormScreen.js.map +1 -0
  6. package/dist/ConsentHistory.d.ts +8 -0
  7. package/dist/ConsentHistory.js +70 -0
  8. package/dist/ConsentHistory.js.map +1 -0
  9. package/dist/ConsentNavigator.d.ts +9 -0
  10. package/dist/ConsentNavigator.js +72 -0
  11. package/dist/ConsentNavigator.js.map +1 -0
  12. package/dist/DataTable.js +1 -1
  13. package/dist/DataTable.js.map +1 -1
  14. package/dist/DateTimeActionSheet.js +22 -6
  15. package/dist/DateTimeActionSheet.js.map +1 -1
  16. package/dist/DateTimeField.d.ts +22 -0
  17. package/dist/DateTimeField.js +187 -67
  18. package/dist/DateTimeField.js.map +1 -1
  19. package/dist/DraggableList.d.ts +66 -0
  20. package/dist/DraggableList.js +241 -0
  21. package/dist/DraggableList.js.map +1 -0
  22. package/dist/Link.js +1 -1
  23. package/dist/Link.js.map +1 -1
  24. package/dist/MarkdownEditor.d.ts +12 -0
  25. package/dist/MarkdownEditor.js +12 -0
  26. package/dist/MarkdownEditor.js.map +1 -0
  27. package/dist/MarkdownEditorField.d.ts +1 -0
  28. package/dist/MarkdownEditorField.js +16 -16
  29. package/dist/MarkdownEditorField.js.map +1 -1
  30. package/dist/Modal.js +11 -1
  31. package/dist/Modal.js.map +1 -1
  32. package/dist/PickerSelect.js +10 -0
  33. package/dist/PickerSelect.js.map +1 -1
  34. package/dist/TerrenoProvider.js +10 -1
  35. package/dist/TerrenoProvider.js.map +1 -1
  36. package/dist/UpgradeRequiredScreen.d.ts +8 -0
  37. package/dist/UpgradeRequiredScreen.js +10 -0
  38. package/dist/UpgradeRequiredScreen.js.map +1 -0
  39. package/dist/generateConsentHistoryPdf.d.ts +2 -0
  40. package/dist/generateConsentHistoryPdf.js +185 -0
  41. package/dist/generateConsentHistoryPdf.js.map +1 -0
  42. package/dist/index.d.ts +9 -0
  43. package/dist/index.js +9 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/useConsentForms.d.ts +29 -0
  46. package/dist/useConsentForms.js +50 -0
  47. package/dist/useConsentForms.js.map +1 -0
  48. package/dist/useConsentHistory.d.ts +31 -0
  49. package/dist/useConsentHistory.js +17 -0
  50. package/dist/useConsentHistory.js.map +1 -0
  51. package/dist/useSubmitConsent.d.ts +12 -0
  52. package/dist/useSubmitConsent.js +23 -0
  53. package/dist/useSubmitConsent.js.map +1 -0
  54. package/package.json +4 -2
  55. package/src/BooleanField.test.tsx +3 -5
  56. package/src/BooleanField.tsx +33 -31
  57. package/src/ConsentFormScreen.tsx +216 -0
  58. package/src/ConsentHistory.tsx +249 -0
  59. package/src/ConsentNavigator.test.tsx +111 -0
  60. package/src/ConsentNavigator.tsx +128 -0
  61. package/src/DataTable.tsx +1 -1
  62. package/src/DateTimeActionSheet.tsx +19 -6
  63. package/src/DateTimeField.tsx +416 -133
  64. package/src/DraggableList.tsx +424 -0
  65. package/src/Link.tsx +1 -1
  66. package/src/MarkdownEditor.tsx +66 -0
  67. package/src/MarkdownEditorField.tsx +32 -28
  68. package/src/Modal.tsx +19 -1
  69. package/src/PickerSelect.tsx +11 -0
  70. package/src/TerrenoProvider.tsx +10 -1
  71. package/src/TimezonePicker.test.tsx +9 -1
  72. package/src/UpgradeRequiredScreen.tsx +52 -0
  73. package/src/__snapshots__/BooleanField.test.tsx.snap +167 -203
  74. package/src/__snapshots__/DataTable.test.tsx.snap +0 -114
  75. package/src/__snapshots__/Field.test.tsx.snap +53 -69
  76. package/src/__snapshots__/Link.test.tsx.snap +14 -21
  77. package/src/__snapshots__/TimezonePicker.test.tsx.snap +0 -4710
  78. package/src/generateConsentHistoryPdf.ts +211 -0
  79. package/src/index.tsx +9 -1
  80. package/src/useConsentForms.ts +70 -0
  81. package/src/useConsentHistory.ts +40 -0
  82. package/src/useSubmitConsent.ts +35 -0
@@ -0,0 +1,211 @@
1
+ import {jsPDF} from "jspdf";
2
+ import {DateTime} from "luxon";
3
+
4
+ import type {ConsentHistoryEntry} from "./useConsentHistory";
5
+
6
+ const PAGE_WIDTH = 210;
7
+ const MARGIN_LEFT = 20;
8
+ const MARGIN_RIGHT = 20;
9
+ const CONTENT_WIDTH = PAGE_WIDTH - MARGIN_LEFT - MARGIN_RIGHT;
10
+ const PAGE_HEIGHT = 297;
11
+ const MARGIN_BOTTOM = 20;
12
+
13
+ const formatDate = (value: unknown): string => {
14
+ if (!value) {
15
+ return "";
16
+ }
17
+ const dt = DateTime.fromISO(String(value));
18
+ if (!dt.isValid) {
19
+ return String(value);
20
+ }
21
+ return dt.toLocaleString(DateTime.DATETIME_FULL);
22
+ };
23
+
24
+ const ensureSpace = (doc: jsPDF, y: number, needed: number): number => {
25
+ if (y + needed > PAGE_HEIGHT - MARGIN_BOTTOM) {
26
+ doc.addPage();
27
+ return 20;
28
+ }
29
+ return y;
30
+ };
31
+
32
+ export const generateConsentHistoryPdf = async (entry: ConsentHistoryEntry): Promise<void> => {
33
+ const doc = new jsPDF({format: "a4", orientation: "portrait", unit: "mm"});
34
+
35
+ const formTitle = entry.form?.title ?? "Unknown Form";
36
+ const formSlug = entry.form?.slug ?? "";
37
+ const formType = entry.form?.type ?? "";
38
+ const formVersion = entry.form?.version ?? entry.formVersionSnapshot;
39
+
40
+ let y = 20;
41
+
42
+ // Title
43
+ doc.setFontSize(18);
44
+ doc.setFont("helvetica", "bold");
45
+ doc.text("Consent Record", MARGIN_LEFT, y);
46
+ y += 10;
47
+
48
+ // Form title
49
+ doc.setFontSize(14);
50
+ doc.setFont("helvetica", "normal");
51
+ doc.text(formTitle, MARGIN_LEFT, y);
52
+ y += 10;
53
+
54
+ // Separator
55
+ doc.setDrawColor(200, 200, 200);
56
+ doc.line(MARGIN_LEFT, y, PAGE_WIDTH - MARGIN_RIGHT, y);
57
+ y += 8;
58
+
59
+ // Helper to add a labeled field
60
+ const addField = (label: string, value: string) => {
61
+ y = ensureSpace(doc, y, 8);
62
+ doc.setFontSize(9);
63
+ doc.setFont("helvetica", "bold");
64
+ doc.setTextColor(100, 100, 100);
65
+ doc.text(label, MARGIN_LEFT, y);
66
+ doc.setFont("helvetica", "normal");
67
+ doc.setTextColor(0, 0, 0);
68
+ doc.setFontSize(10);
69
+ doc.text(value, MARGIN_LEFT + 45, y);
70
+ y += 6;
71
+ };
72
+
73
+ // Response Details
74
+ doc.setFontSize(12);
75
+ doc.setFont("helvetica", "bold");
76
+ doc.setTextColor(0, 0, 0);
77
+ doc.text("Response Details", MARGIN_LEFT, y);
78
+ y += 8;
79
+
80
+ if (formSlug) {
81
+ addField("Form Slug:", formSlug);
82
+ }
83
+ if (formType) {
84
+ addField("Form Type:", formType);
85
+ }
86
+ if (formVersion !== undefined) {
87
+ addField("Form Version:", String(formVersion));
88
+ }
89
+ addField("Decision:", entry.agreed ? "Agreed" : "Declined");
90
+ if (entry.agreedAt) {
91
+ addField("Agreed At:", formatDate(entry.agreedAt));
92
+ }
93
+ if (entry.locale) {
94
+ addField("Locale:", entry.locale);
95
+ }
96
+
97
+ y += 4;
98
+
99
+ // Checkbox Values
100
+ const checkboxEntries =
101
+ entry.checkboxValues && typeof entry.checkboxValues === "object"
102
+ ? Object.entries(entry.checkboxValues)
103
+ : [];
104
+
105
+ if (checkboxEntries.length > 0) {
106
+ y = ensureSpace(doc, y, 12 + checkboxEntries.length * 6);
107
+ doc.setFontSize(12);
108
+ doc.setFont("helvetica", "bold");
109
+ doc.setTextColor(0, 0, 0);
110
+ doc.text("Checkbox Responses", MARGIN_LEFT, y);
111
+ y += 8;
112
+
113
+ for (const [index, checked] of checkboxEntries) {
114
+ y = ensureSpace(doc, y, 6);
115
+ doc.setFontSize(10);
116
+ doc.setFont("helvetica", "normal");
117
+ const label = entry.form?.checkboxes?.[Number(index)]?.label;
118
+ const checkmark = checked ? "[x]" : "[ ]";
119
+ doc.text(`${checkmark} ${label ?? `Checkbox ${index}`}`, MARGIN_LEFT + 4, y);
120
+ y += 6;
121
+ }
122
+ y += 4;
123
+ }
124
+
125
+ // Audit Trail
126
+ const hasAuditTrail = entry.ipAddress || entry.userAgent || entry.formVersionSnapshot;
127
+
128
+ if (hasAuditTrail) {
129
+ y = ensureSpace(doc, y, 20);
130
+ doc.setFontSize(12);
131
+ doc.setFont("helvetica", "bold");
132
+ doc.setTextColor(0, 0, 0);
133
+ doc.text("Audit Trail", MARGIN_LEFT, y);
134
+ y += 8;
135
+
136
+ if (entry.ipAddress) {
137
+ addField("IP Address:", entry.ipAddress);
138
+ }
139
+ if (entry.userAgent) {
140
+ addField("User Agent:", entry.userAgent);
141
+ }
142
+ if (entry.formVersionSnapshot !== undefined) {
143
+ addField("Form Version:", String(entry.formVersionSnapshot));
144
+ }
145
+ if (entry.signedAt) {
146
+ addField("Signed At:", formatDate(entry.signedAt));
147
+ }
148
+ y += 4;
149
+ }
150
+
151
+ // Signature
152
+ if (entry.signature) {
153
+ y = ensureSpace(doc, y, 50);
154
+ doc.setFontSize(12);
155
+ doc.setFont("helvetica", "bold");
156
+ doc.setTextColor(0, 0, 0);
157
+ doc.text("Signature", MARGIN_LEFT, y);
158
+ y += 6;
159
+
160
+ try {
161
+ const format = entry.signature.includes("image/png") ? "PNG" : "JPEG";
162
+ doc.addImage(entry.signature, format, MARGIN_LEFT, y, 80, 30);
163
+ y += 34;
164
+ } catch {
165
+ doc.setFontSize(10);
166
+ doc.setFont("helvetica", "italic");
167
+ doc.text("(Signature image could not be embedded)", MARGIN_LEFT, y);
168
+ y += 6;
169
+ }
170
+ y += 4;
171
+ }
172
+
173
+ // Content Snapshot
174
+ if (entry.contentSnapshot) {
175
+ y = ensureSpace(doc, y, 20);
176
+ doc.setFontSize(12);
177
+ doc.setFont("helvetica", "bold");
178
+ doc.setTextColor(0, 0, 0);
179
+ doc.text("Content Snapshot", MARGIN_LEFT, y);
180
+ y += 8;
181
+
182
+ doc.setFontSize(9);
183
+ doc.setFont("helvetica", "normal");
184
+ doc.setTextColor(50, 50, 50);
185
+
186
+ const lines = doc.splitTextToSize(entry.contentSnapshot, CONTENT_WIDTH);
187
+ for (const line of lines) {
188
+ y = ensureSpace(doc, y, 5);
189
+ doc.text(line, MARGIN_LEFT, y);
190
+ y += 4;
191
+ }
192
+ }
193
+
194
+ // Footer
195
+ y = ensureSpace(doc, y, 20);
196
+ y += 8;
197
+ doc.setDrawColor(200, 200, 200);
198
+ doc.line(MARGIN_LEFT, y, PAGE_WIDTH - MARGIN_RIGHT, y);
199
+ y += 6;
200
+ doc.setFontSize(8);
201
+ doc.setFont("helvetica", "italic");
202
+ doc.setTextColor(150, 150, 150);
203
+ doc.text(`Generated ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`, MARGIN_LEFT, y);
204
+ if (entry._id) {
205
+ doc.text(`Response ID: ${entry._id}`, PAGE_WIDTH - MARGIN_RIGHT, y, {align: "right"});
206
+ }
207
+
208
+ // Download
209
+ const filename = `consent-${formSlug || "response"}-${DateTime.now().toFormat("yyyy-MM-dd")}.pdf`;
210
+ doc.save(filename);
211
+ };
package/src/index.tsx CHANGED
@@ -16,6 +16,9 @@ export * from "./Button";
16
16
  export * from "./Card";
17
17
  export * from "./CheckBox";
18
18
  export * from "./Common";
19
+ export * from "./ConsentFormScreen";
20
+ export * from "./ConsentHistory";
21
+ export * from "./ConsentNavigator";
19
22
  export * from "./Constants";
20
23
  export * from "./CustomSelectField";
21
24
  export * from "./DataTable";
@@ -24,6 +27,7 @@ export * from "./DateTimeField";
24
27
  export * from "./DateUtilities";
25
28
  export * from "./DecimalRangeActionSheet";
26
29
  export * from "./DismissButton";
30
+ export {DraggableList} from "./DraggableList";
27
31
  export * from "./EmailField";
28
32
  export {default as EmojiSelector} from "./EmojiSelector";
29
33
  export * from "./ErrorBoundary";
@@ -44,7 +48,7 @@ export * from "./InfoModalIcon";
44
48
  export * from "./InfoTooltipButton";
45
49
  export * from "./Link";
46
50
  export * from "./login";
47
-
51
+ export * from "./MarkdownEditor";
48
52
  export * from "./MarkdownEditorField";
49
53
  export * from "./MarkdownView";
50
54
  export * from "./MediaQuery";
@@ -97,9 +101,13 @@ export * from "./table/TableTitle";
97
101
  export * from "./table/tableContext";
98
102
  export * from "./UnifiedAddressAutoComplete";
99
103
  export * from "./Unifier";
104
+ export * from "./UpgradeRequiredScreen";
100
105
  export * from "./UserInactivity";
101
106
  export * from "./Utilities";
107
+ export * from "./useConsentForms";
108
+ export * from "./useConsentHistory";
102
109
  export * from "./useStoredState";
110
+ export * from "./useSubmitConsent";
103
111
  export * from "./WebAddressAutocomplete";
104
112
 
105
113
  // export * from "./Layout";
@@ -0,0 +1,70 @@
1
+ export interface ConsentFormPublic {
2
+ id: string;
3
+ title: string;
4
+ slug: string;
5
+ version: number;
6
+ order: number;
7
+ type: string;
8
+ content: Record<string, string>;
9
+ defaultLocale: string;
10
+ active: boolean;
11
+ captureSignature: boolean;
12
+ requireScrollToBottom: boolean;
13
+ checkboxes: Array<{label: string; required: boolean; confirmationPrompt?: string}>;
14
+ agreeButtonText: string;
15
+ allowDecline: boolean;
16
+ declineButtonText: string;
17
+ required: boolean;
18
+ }
19
+
20
+ import {getLocales} from "expo-localization";
21
+
22
+ export const detectLocale = (): string => {
23
+ // Web
24
+ if (typeof navigator !== "undefined" && navigator.language) {
25
+ return navigator.language;
26
+ }
27
+
28
+ // Native — expo-localization
29
+ try {
30
+ const locale = getLocales()[0]?.languageTag;
31
+ if (locale) {
32
+ return locale;
33
+ }
34
+ } catch {
35
+ // expo-localization not available in this environment
36
+ }
37
+
38
+ return "en";
39
+ };
40
+
41
+ export const useConsentForms = (api: any, baseUrl?: string) => {
42
+ const base = baseUrl || "";
43
+ const apiWithConsentTags = api.enhanceEndpoints({addTagTypes: ["PendingConsents"]});
44
+
45
+ const enhancedApi = apiWithConsentTags.injectEndpoints({
46
+ endpoints: (build: any) => ({
47
+ getPendingConsents: build.query({
48
+ async onQueryStarted(_arg: unknown, {queryFulfilled}: {queryFulfilled: Promise<unknown>}) {
49
+ console.info("[useConsentForms] Fetching pending consent forms");
50
+ try {
51
+ const result = (await queryFulfilled) as {data?: ConsentFormPublic[]};
52
+ console.info("[useConsentForms] Pending consent forms fetched", {
53
+ count: result?.data?.length ?? 0,
54
+ });
55
+ } catch (error) {
56
+ console.warn("[useConsentForms] Failed to fetch pending consent forms", {error});
57
+ }
58
+ },
59
+ providesTags: ["PendingConsents"],
60
+ query: () => `${base}/consents/pending`,
61
+ }),
62
+ }),
63
+ overrideExisting: false,
64
+ });
65
+
66
+ const {data, isLoading, error, refetch} = enhancedApi.useGetPendingConsentsQuery();
67
+ const forms: ConsentFormPublic[] = Array.isArray(data) ? data : (data?.data ?? []);
68
+
69
+ return {error, forms, isLoading, refetch};
70
+ };
@@ -0,0 +1,40 @@
1
+ export interface ConsentHistoryEntry {
2
+ _id: string;
3
+ agreed: boolean;
4
+ agreedAt: string;
5
+ checkboxValues?: Record<string, boolean>;
6
+ contentSnapshot?: string;
7
+ form: {
8
+ captureSignature: boolean;
9
+ checkboxes: Array<{label: string; required: boolean; confirmationPrompt?: string}>;
10
+ slug: string;
11
+ title: string;
12
+ type: string;
13
+ version: number;
14
+ } | null;
15
+ formVersionSnapshot?: number;
16
+ ipAddress?: string;
17
+ locale?: string;
18
+ signature?: string;
19
+ signedAt?: string;
20
+ userAgent?: string;
21
+ }
22
+
23
+ export const useConsentHistory = (api: any, baseUrl?: string) => {
24
+ const base = baseUrl || "";
25
+
26
+ const enhancedApi = api.injectEndpoints({
27
+ endpoints: (build: any) => ({
28
+ getMyConsents: build.query({
29
+ providesTags: ["MyConsents"],
30
+ query: () => `${base}/consents/my`,
31
+ }),
32
+ }),
33
+ overrideExisting: false,
34
+ });
35
+
36
+ const {data, isLoading, error, refetch} = enhancedApi.useGetMyConsentsQuery();
37
+ const entries: ConsentHistoryEntry[] = Array.isArray(data) ? data : (data?.data ?? []);
38
+
39
+ return {entries, error, isLoading, refetch};
40
+ };
@@ -0,0 +1,35 @@
1
+ export interface SubmitConsentBody {
2
+ agreed: boolean;
3
+ checkboxValues?: Record<string, boolean>;
4
+ consentFormId: string;
5
+ locale: string;
6
+ signature?: string;
7
+ }
8
+
9
+ export const useSubmitConsent = (api: any, baseUrl?: string) => {
10
+ const base = baseUrl || "";
11
+ const apiWithConsentTags = api.enhanceEndpoints({addTagTypes: ["PendingConsents"]});
12
+
13
+ const enhancedApi = apiWithConsentTags.injectEndpoints({
14
+ endpoints: (build: any) => ({
15
+ submitConsentResponse: build.mutation({
16
+ invalidatesTags: ["PendingConsents"],
17
+ query: (body: SubmitConsentBody) => ({
18
+ body,
19
+ method: "POST",
20
+ url: `${base}/consents/respond`,
21
+ }),
22
+ }),
23
+ }),
24
+ overrideExisting: false,
25
+ });
26
+
27
+ const [submitMutation, {isLoading: isSubmitting, error}] =
28
+ enhancedApi.useSubmitConsentResponseMutation();
29
+
30
+ const submit = async (body: SubmitConsentBody) => {
31
+ return submitMutation(body).unwrap();
32
+ };
33
+
34
+ return {error, isSubmitting, submit};
35
+ };