@terreno/ui 0.13.3 → 0.14.1

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 (179) hide show
  1. package/dist/ActionSheet.d.ts +5 -5
  2. package/dist/ActionSheet.js +2 -2
  3. package/dist/ActionSheet.js.map +1 -1
  4. package/dist/Avatar.js +1 -1
  5. package/dist/Avatar.js.map +1 -1
  6. package/dist/Banner.js.map +1 -1
  7. package/dist/Box.js +2 -0
  8. package/dist/Box.js.map +1 -1
  9. package/dist/Button.d.ts +2 -2
  10. package/dist/Button.js +35 -23
  11. package/dist/Button.js.map +1 -1
  12. package/dist/Common.d.ts +16 -4
  13. package/dist/Common.js +4 -4
  14. package/dist/Common.js.map +1 -1
  15. package/dist/ConsentFormScreen.js +3 -3
  16. package/dist/ConsentFormScreen.js.map +1 -1
  17. package/dist/ConsentNavigator.d.ts +1 -1
  18. package/dist/ConsentNavigator.js +2 -1
  19. package/dist/ConsentNavigator.js.map +1 -1
  20. package/dist/CustomSelectField.js +3 -1
  21. package/dist/CustomSelectField.js.map +1 -1
  22. package/dist/DataTable.js +1 -1
  23. package/dist/DataTable.js.map +1 -1
  24. package/dist/DateTimeActionSheet.js +2 -1
  25. package/dist/DateTimeActionSheet.js.map +1 -1
  26. package/dist/DateTimeField.js +3 -2
  27. package/dist/DateTimeField.js.map +1 -1
  28. package/dist/DateUtilities.d.ts +25 -25
  29. package/dist/DateUtilities.js +31 -32
  30. package/dist/DateUtilities.js.map +1 -1
  31. package/dist/HeightField.js.map +1 -1
  32. package/dist/Hyperlink.js +19 -9
  33. package/dist/Hyperlink.js.map +1 -1
  34. package/dist/IconButton.js.map +1 -1
  35. package/dist/ImageBackground.d.ts +2 -5
  36. package/dist/ImageBackground.js +1 -1
  37. package/dist/ImageBackground.js.map +1 -1
  38. package/dist/MediaQuery.d.ts +4 -4
  39. package/dist/MediaQuery.js +8 -8
  40. package/dist/MediaQuery.js.map +1 -1
  41. package/dist/ModalSheet.d.ts +3 -2
  42. package/dist/ModalSheet.js +1 -1
  43. package/dist/ModalSheet.js.map +1 -1
  44. package/dist/OfflineBanner.d.ts +21 -0
  45. package/dist/OfflineBanner.js +25 -0
  46. package/dist/OfflineBanner.js.map +1 -0
  47. package/dist/OpenAPIContext.js +1 -1
  48. package/dist/OpenAPIContext.js.map +1 -1
  49. package/dist/Page.d.ts +1 -0
  50. package/dist/Page.js +7 -2
  51. package/dist/Page.js.map +1 -1
  52. package/dist/Pagination.js.map +1 -1
  53. package/dist/Permissions.js +3 -0
  54. package/dist/Permissions.js.map +1 -1
  55. package/dist/PickerSelect.d.ts +1 -1
  56. package/dist/PickerSelect.js +9 -6
  57. package/dist/PickerSelect.js.map +1 -1
  58. package/dist/SelectField.js +1 -1
  59. package/dist/SelectField.js.map +1 -1
  60. package/dist/SplitPage.js +7 -2
  61. package/dist/SplitPage.js.map +1 -1
  62. package/dist/SplitPage.native.js +4 -1
  63. package/dist/SplitPage.native.js.map +1 -1
  64. package/dist/TapToEdit.d.ts +1 -1
  65. package/dist/TapToEdit.js +12 -14
  66. package/dist/TapToEdit.js.map +1 -1
  67. package/dist/Toast.js.map +1 -1
  68. package/dist/ToastNotifications.js +2 -2
  69. package/dist/ToastNotifications.js.map +1 -1
  70. package/dist/Tooltip.d.ts +24 -1
  71. package/dist/Tooltip.js +2 -2
  72. package/dist/Tooltip.js.map +1 -1
  73. package/dist/Unifier.d.ts +3 -3
  74. package/dist/Unifier.js +15 -12
  75. package/dist/Unifier.js.map +1 -1
  76. package/dist/Utilities.d.ts +12 -8
  77. package/dist/Utilities.js +13 -15
  78. package/dist/Utilities.js.map +1 -1
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.js +2 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/signUp/PasswordRequirements.js +3 -3
  83. package/dist/signUp/PasswordRequirements.js.map +1 -1
  84. package/dist/table/TableHeaderCell.js +1 -9
  85. package/dist/table/TableHeaderCell.js.map +1 -1
  86. package/dist/table/tableContext.d.ts +1 -1
  87. package/dist/table/tableContext.js +2 -2
  88. package/dist/table/tableContext.js.map +1 -1
  89. package/package.json +2 -1
  90. package/src/ActionSheet.test.tsx +1 -0
  91. package/src/ActionSheet.tsx +8 -6
  92. package/src/Avatar.tsx +9 -2
  93. package/src/Badge.test.tsx +1 -0
  94. package/src/Banner.test.tsx +71 -0
  95. package/src/Banner.tsx +1 -1
  96. package/src/Box.test.tsx +1 -0
  97. package/src/Box.tsx +10 -6
  98. package/src/Button.test.tsx +35 -0
  99. package/src/Button.tsx +65 -34
  100. package/src/Common.ts +42 -19
  101. package/src/ConsentFormScreen.test.tsx +124 -0
  102. package/src/ConsentFormScreen.tsx +18 -6
  103. package/src/ConsentNavigator.test.tsx +1 -0
  104. package/src/ConsentNavigator.tsx +5 -3
  105. package/src/CustomSelectField.tsx +3 -1
  106. package/src/DataTable.test.tsx +218 -0
  107. package/src/DataTable.tsx +1 -1
  108. package/src/DateTimeActionSheet.tsx +7 -3
  109. package/src/DateTimeField.test.tsx +1 -0
  110. package/src/DateTimeField.tsx +3 -2
  111. package/src/DateUtilities.test.ts +111 -0
  112. package/src/DateUtilities.tsx +43 -44
  113. package/src/DecimalRangeActionSheet.test.tsx +28 -0
  114. package/src/ErrorBoundary.test.tsx +1 -0
  115. package/src/HeightActionSheet.test.tsx +16 -0
  116. package/src/HeightField.test.tsx +106 -1
  117. package/src/HeightField.tsx +2 -1
  118. package/src/Hyperlink.tsx +83 -52
  119. package/src/IconButton.tsx +1 -1
  120. package/src/ImageBackground.tsx +5 -6
  121. package/src/MediaQuery.ts +8 -8
  122. package/src/MobileAddressAutoComplete.test.tsx +20 -1
  123. package/src/ModalSheet.test.tsx +1 -5
  124. package/src/ModalSheet.tsx +15 -6
  125. package/src/NumberField.test.tsx +14 -0
  126. package/src/OfflineBanner.test.tsx +70 -0
  127. package/src/OfflineBanner.tsx +54 -0
  128. package/src/OpenAPIContext.tsx +3 -2
  129. package/src/Page.test.tsx +28 -0
  130. package/src/Page.tsx +18 -2
  131. package/src/Pagination.tsx +1 -1
  132. package/src/Permissions.ts +3 -0
  133. package/src/PickerSelect.tsx +20 -17
  134. package/src/SelectBadge.test.tsx +1 -0
  135. package/src/SelectField.tsx +1 -1
  136. package/src/Signature.test.tsx +1 -0
  137. package/src/SplitPage.native.tsx +2 -0
  138. package/src/SplitPage.tsx +6 -1
  139. package/src/TapToEdit.test.tsx +48 -0
  140. package/src/TapToEdit.tsx +13 -14
  141. package/src/Toast.tsx +1 -1
  142. package/src/ToastNotifications.test.tsx +738 -0
  143. package/src/ToastNotifications.tsx +3 -6
  144. package/src/Tooltip.test.tsx +586 -8
  145. package/src/Tooltip.tsx +2 -2
  146. package/src/Unifier.ts +20 -16
  147. package/src/Utilities.tsx +20 -19
  148. package/src/WebAddressAutocomplete.test.tsx +138 -0
  149. package/src/WebDropdownMenu.test.tsx +23 -0
  150. package/src/__snapshots__/AddressField.test.tsx.snap +3 -1
  151. package/src/__snapshots__/Button.test.tsx.snap +92 -50
  152. package/src/__snapshots__/CustomSelectField.test.tsx.snap +21 -7
  153. package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +14 -8
  154. package/src/__snapshots__/ErrorPage.test.tsx.snap +7 -4
  155. package/src/__snapshots__/Field.test.tsx.snap +18 -6
  156. package/src/__snapshots__/HeightActionSheet.test.tsx.snap +14 -8
  157. package/src/__snapshots__/HeightField.test.tsx.snap +35 -20
  158. package/src/__snapshots__/InfoModalIcon.test.tsx.snap +28 -16
  159. package/src/__snapshots__/Modal.test.tsx.snap +19 -10
  160. package/src/__snapshots__/ModalSheet.test.tsx.snap +0 -1
  161. package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +14 -8
  162. package/src/__snapshots__/Page.test.tsx.snap +7 -4
  163. package/src/__snapshots__/SelectField.test.tsx.snap +18 -6
  164. package/src/__snapshots__/TerrenoProvider.test.tsx.snap +0 -2
  165. package/src/__snapshots__/TimezonePicker.test.tsx.snap +18 -6
  166. package/src/bunSetup.ts +25 -2
  167. package/src/index.tsx +2 -1
  168. package/src/login/LoginScreen.test.tsx +23 -1
  169. package/src/login/__snapshots__/LoginScreen.test.tsx.snap +15 -6
  170. package/src/signUp/PasswordRequirements.tsx +9 -6
  171. package/src/signUp/__snapshots__/PasswordRequirements.test.tsx.snap +50 -2
  172. package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +35 -5
  173. package/src/table/TableHeaderCell.tsx +8 -11
  174. package/src/table/TableRow.test.tsx +31 -1
  175. package/src/table/__snapshots__/TableBadge.test.tsx.snap +3 -1
  176. package/src/table/__snapshots__/TableHeaderCell.test.tsx.snap +2 -0
  177. package/src/table/tableContext.tsx +2 -2
  178. package/src/types/react-native-swiper-flatlist.d.ts +1 -0
  179. package/src/useStoredState.test.tsx +47 -0
package/src/Common.ts CHANGED
@@ -4,6 +4,7 @@ import type {ReactElement, ReactNode} from "react";
4
4
  import type {
5
5
  ImageStyle,
6
6
  ListRenderItemInfo,
7
+ ScrollView,
7
8
  StyleProp,
8
9
  TextInput,
9
10
  TextStyle,
@@ -394,12 +395,12 @@ export const SPACING_MAP = {
394
395
  12: 80,
395
396
  };
396
397
 
397
- export function getSpacing(spacing: SignedUpTo12) {
398
+ export const getSpacing = (spacing: SignedUpTo12) => {
398
399
  if (spacing < 0) {
399
400
  return SPACING_MAP[Math.abs(spacing) as UnsignedUpTo12] * -1;
400
401
  }
401
402
  return SPACING_MAP[spacing as UnsignedUpTo12];
402
- }
403
+ };
403
404
 
404
405
  export type TextFieldType =
405
406
  | "date"
@@ -463,7 +464,7 @@ export interface BoxPropsBase {
463
464
  lgColumn?: UnsignedUpTo12;
464
465
  dangerouslySetInlineStyle?: {
465
466
  __style: {
466
- // noExplicitAny: escape hatch for arbitrary inline style values
467
+ // biome-ignore lint/suspicious/noExplicitAny: escape hatch for arbitrary inline style values that users may need to set
467
468
  [key: string]: any;
468
469
  };
469
470
  };
@@ -549,7 +550,7 @@ export interface BoxPropsBase {
549
550
 
550
551
  avoidKeyboard?: boolean;
551
552
  keyboardOffset?: number;
552
- scrollRef?: React.RefObject<any>;
553
+ scrollRef?: React.RefObject<ScrollView | null>;
553
554
  onScroll?: (offsetY: number) => void;
554
555
  onLayout?: (event: LayoutChangeEvent) => void;
555
556
  testID?: string;
@@ -752,9 +753,9 @@ const ROUNDING_MAP = {
752
753
  xl: 32,
753
754
  };
754
755
 
755
- export function getRounding(rounding: Rounding) {
756
+ export const getRounding = (rounding: Rounding) => {
756
757
  return ROUNDING_MAP[rounding];
757
- }
758
+ };
758
759
 
759
760
  export interface HeadingProps {
760
761
  align?: "left" | "right" | "center" | "justify"; // default "left"
@@ -858,17 +859,17 @@ export interface SplitPageProps {
858
859
  loading?: boolean;
859
860
  color?: SurfaceColor;
860
861
  keyboardOffset?: number;
861
- // noExplicitAny: ListRenderItemInfo generic type depends on the consumer's data shape
862
+ // biome-ignore lint/suspicious/noExplicitAny: ListRenderItemInfo generic type depends on the consumer's data shape
862
863
  renderListViewItem: (itemInfo: ListRenderItemInfo<any>) => ReactElement | null;
863
864
  renderListViewHeader?: () => ReactElement | null;
864
865
  renderContent?: (index?: number) => ReactElement | ReactElement[] | null;
865
- // noExplicitAny: list data type varies by consumer's data model
866
+ // biome-ignore lint/suspicious/noExplicitAny: list data type varies by consumer's data model
866
867
  listViewData: any[];
867
868
  listViewExtraData?: unknown;
868
869
  listViewWidth?: number;
869
870
  listViewMaxWidth?: number;
870
871
  renderChild?: () => ReactChild;
871
- // noExplicitAny: callback value type varies by consumer's data model
872
+ // biome-ignore lint/suspicious/noExplicitAny: callback value type varies by consumer's data model
872
873
  onSelectionChange?: (value?: any) => void | Promise<void>;
873
874
  }
874
875
 
@@ -1500,6 +1501,8 @@ export interface BodyProps {
1500
1501
  children?: ReactNode;
1501
1502
  }
1502
1503
 
1504
+ export type ButtonPressAnimation = "scale" | "opacity" | "none";
1505
+
1503
1506
  export interface ButtonProps {
1504
1507
  /**
1505
1508
  * The text content of the confirmation modal.
@@ -1538,6 +1541,11 @@ export interface ButtonProps {
1538
1541
  * The subtitle of the confirmation modal.
1539
1542
  */
1540
1543
  modalSubTitle?: string;
1544
+ /**
1545
+ * The press animation to use when the button is touched.
1546
+ * @default "scale"
1547
+ */
1548
+ pressAnimation?: ButtonPressAnimation;
1541
1549
  /**
1542
1550
  * The test ID for the button, used for testing purposes.
1543
1551
  */
@@ -1675,6 +1683,7 @@ export interface DateTimeActionSheetProps {
1675
1683
  type?: "date" | "time" | "datetime";
1676
1684
  // Returns an ISO 8601 string. If mode is "time", the date portion is today.
1677
1685
  onChange: OnChangeCallback;
1686
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
1678
1687
  actionSheetRef: React.RefObject<any>;
1679
1688
  visible: boolean;
1680
1689
  onDismiss: () => void;
@@ -1686,6 +1695,7 @@ export interface DecimalRangeActionSheetProps {
1686
1695
  min: number;
1687
1696
  max: number;
1688
1697
  onChange: OnChangeCallback;
1698
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
1689
1699
  actionSheetRef: React.RefObject<any>;
1690
1700
  }
1691
1701
 
@@ -1745,6 +1755,7 @@ export type FieldProps =
1745
1755
  export interface HeightActionSheetProps {
1746
1756
  value?: string;
1747
1757
  onChange: OnChangeCallback;
1758
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
1748
1759
  actionSheetRef: React.RefObject<any>;
1749
1760
  /** Minimum height in total inches */
1750
1761
  min?: number;
@@ -1756,14 +1767,17 @@ export interface HeightActionSheetProps {
1756
1767
 
1757
1768
  export interface HyperlinkProps {
1758
1769
  linkDefault?: boolean;
1759
- // noExplicitAny: type comes from external linkify-it library which lacks an exported type
1770
+ // biome-ignore lint/suspicious/noExplicitAny: linkify-it library's main export lacks a TypeScript type definition
1760
1771
  linkify?: any;
1772
+ // biome-ignore lint/suspicious/noExplicitAny: StyleProp's generic is heterogeneous (TextStyle | ViewStyle) for link contexts
1761
1773
  linkStyle?: StyleProp<any>;
1762
1774
  linkText?: string | ((url: string) => string);
1763
1775
  onPress?: (url: string) => void;
1764
1776
  onLongPress?: (url: string, text: string) => void;
1777
+ // biome-ignore lint/suspicious/noExplicitAny: returned view props are spread onto a heterogeneous View; consumers pass arbitrary props
1765
1778
  injectViewProps?: (url: string) => any;
1766
1779
  children?: React.ReactNode;
1780
+ // biome-ignore lint/suspicious/noExplicitAny: StyleProp's generic is heterogeneous for the container which holds mixed Text/View children
1767
1781
  style?: StyleProp<any>;
1768
1782
  }
1769
1783
 
@@ -1913,12 +1927,12 @@ export interface ModalProps {
1913
1927
  /**
1914
1928
  * The function to call when the primary button is clicked.
1915
1929
  */
1916
- // noExplicitAny: callback value type varies by consumer context
1930
+ // biome-ignore lint/suspicious/noExplicitAny: callback value type varies by consumer context
1917
1931
  primaryButtonOnClick?: (value?: any) => void | Promise<void>;
1918
1932
  /**
1919
1933
  * The function to call when the secondary button is clicked.
1920
1934
  */
1921
- // noExplicitAny: callback value type varies by consumer context
1935
+ // biome-ignore lint/suspicious/noExplicitAny: callback value type varies by consumer context
1922
1936
  secondaryButtonOnClick?: (value?: any) => void | Promise<void>;
1923
1937
  }
1924
1938
 
@@ -1927,11 +1941,12 @@ export interface NumberPickerActionSheetProps {
1927
1941
  min: number;
1928
1942
  max: number;
1929
1943
  onChange: OnChangeCallback;
1944
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
1930
1945
  actionSheetRef: React.RefObject<any>;
1931
1946
  }
1932
1947
 
1933
1948
  export interface PageProps {
1934
- // noExplicitAny: React Navigation type varies by navigation stack configuration
1949
+ // biome-ignore lint/suspicious/noExplicitAny: React Navigation type varies by navigation stack configuration
1935
1950
  navigation?: any;
1936
1951
  scroll?: boolean;
1937
1952
  loading?: boolean;
@@ -1950,6 +1965,12 @@ export interface PageProps {
1950
1965
  rightButtonOnClick?: () => void;
1951
1966
  children?: ReactChildren;
1952
1967
  onError?: (error: Error, stack: string) => void;
1968
+ /**
1969
+ * When true, wraps content in SafeAreaView so it respects top/bottom device
1970
+ * insets (camera notches, home indicator, status bar). Opt-in to avoid
1971
+ * regressing existing screens that handle insets at the navigation level.
1972
+ */
1973
+ safeArea?: boolean;
1953
1974
  }
1954
1975
 
1955
1976
  export interface ProgressBarProps {
@@ -2252,6 +2273,7 @@ export interface TextFieldPickerActionSheetProps {
2252
2273
  value?: string;
2253
2274
  mode?: "date" | "time";
2254
2275
  onChange: OnChangeCallback;
2276
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
2255
2277
  actionSheetRef: React.RefObject<any>;
2256
2278
  }
2257
2279
 
@@ -2371,19 +2393,19 @@ export type TapToEditProps =
2371
2393
 
2372
2394
  export interface BaseTapToEditProps extends Omit<FieldProps, "onChange" | "value"> {
2373
2395
  title: string;
2374
- // noExplicitAny: value type varies across TapToEdit field types (text, number, date, etc.)
2396
+ // biome-ignore lint/suspicious/noExplicitAny: value type varies across TapToEdit field types (text, number, date, etc.)
2375
2397
  value: any;
2376
2398
 
2377
2399
  /**
2378
2400
  * Not required if not editable.
2379
2401
  */
2380
- // noExplicitAny: value type varies across TapToEdit field types
2402
+ // biome-ignore lint/suspicious/noExplicitAny: value type varies across TapToEdit field types
2381
2403
  setValue?: (value: any) => void;
2382
2404
 
2383
2405
  /**
2384
2406
  * Not required if not editable.
2385
2407
  */
2386
- // noExplicitAny: value type varies across TapToEdit field types
2408
+ // biome-ignore lint/suspicious/noExplicitAny: value type varies across TapToEdit field types
2387
2409
  onSave?: (value: any) => void | Promise<void>;
2388
2410
 
2389
2411
  /**
@@ -2396,7 +2418,7 @@ export interface BaseTapToEditProps extends Omit<FieldProps, "onChange" | "value
2396
2418
  * Enable edit mode from outside the component.
2397
2419
  */
2398
2420
  isEditing?: boolean;
2399
- // noExplicitAny: input value type varies across TapToEdit field types
2421
+ // biome-ignore lint/suspicious/noExplicitAny: input value type varies across TapToEdit field types
2400
2422
  transform?: (value: any) => string;
2401
2423
  /**
2402
2424
  * Show a confirmation modal before saving the value.
@@ -2485,11 +2507,12 @@ export interface ModelFields {
2485
2507
 
2486
2508
  export interface OpenAPISpec {
2487
2509
  paths: {
2488
- // noExplicitAny: OpenAPI path items are deeply accessed with chained property lookups
2510
+ // biome-ignore lint/suspicious/noExplicitAny: OpenAPI path items are deeply accessed with chained property lookups
2489
2511
  [key: string]: any;
2490
2512
  };
2491
2513
  }
2492
2514
 
2515
+ // biome-ignore lint/suspicious/noExplicitAny: ModelFieldConfig is a passthrough for arbitrary field configuration objects from various model contexts
2493
2516
  export type ModelFieldConfig = any;
2494
2517
 
2495
2518
  export interface OpenAPIProviderProps {
@@ -2518,7 +2541,7 @@ export interface ModelAdminFieldConfig {
2518
2541
 
2519
2542
  // The props for a custom column component for ModelAdmin.
2520
2543
  export interface ModelAdminCustomComponentProps extends Omit<FieldProps, "name"> {
2521
- // noExplicitAny: document shape varies by model used with ModelAdmin
2544
+ // biome-ignore lint/suspicious/noExplicitAny: document shape varies by model used with ModelAdmin
2522
2545
  doc: any;
2523
2546
  fieldKey: string; // Dot notation representation of the field.
2524
2547
  // user: User;
@@ -2,6 +2,7 @@ import {describe, expect, it, mock} from "bun:test";
2
2
  import {act, fireEvent} from "@testing-library/react-native";
3
3
 
4
4
  import {ConsentFormScreen} from "./ConsentFormScreen";
5
+ import {SignatureField} from "./SignatureField";
5
6
  import {renderWithTheme} from "./test-utils";
6
7
  import type {ConsentFormPublic} from "./useConsentForms";
7
8
 
@@ -208,6 +209,108 @@ describe("ConsentFormScreen", () => {
208
209
  expect(queryByTestId("consent-form-required-legend")).toBeNull();
209
210
  });
210
211
 
212
+ it("confirms the modal and toggles the checkbox on", () => {
213
+ const form: ConsentFormPublic = {
214
+ ...baseForm,
215
+ checkboxes: [{confirmationPrompt: "Are you sure?", label: "Tricky", required: true}],
216
+ };
217
+ const {getByTestId, getByText, queryByTestId} = renderWithTheme(
218
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
219
+ );
220
+ act(() => {
221
+ fireEvent.press(getByTestId("consent-form-checkbox-0"));
222
+ });
223
+ expect(getByText("Are you sure?")).toBeTruthy();
224
+ // Press the "Confirm" button inside the modal
225
+ act(() => {
226
+ fireEvent.press(getByText("Confirm"));
227
+ });
228
+ return new Promise<void>((resolve) => {
229
+ setTimeout(() => {
230
+ // Checkbox hint should be gone because the required checkbox was toggled on
231
+ expect(queryByTestId("consent-footer-checkboxes-hint")).toBeNull();
232
+ resolve();
233
+ }, 600);
234
+ });
235
+ });
236
+
237
+ it("dismisses the modal without toggling the checkbox", () => {
238
+ const form: ConsentFormPublic = {
239
+ ...baseForm,
240
+ checkboxes: [{confirmationPrompt: "Are you sure?", label: "Tricky", required: true}],
241
+ };
242
+ const {getByTestId, getByText} = renderWithTheme(
243
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
244
+ );
245
+ act(() => {
246
+ fireEvent.press(getByTestId("consent-form-checkbox-0"));
247
+ });
248
+ expect(getByText("Are you sure?")).toBeTruthy();
249
+ // Press the "Cancel" button inside the modal
250
+ act(() => {
251
+ fireEvent.press(getByText("Cancel"));
252
+ });
253
+ return new Promise<void>((resolve) => {
254
+ setTimeout(() => {
255
+ // The checkbox hint should still show because the checkbox was not toggled
256
+ expect(getByTestId("consent-footer-checkboxes-hint")).toBeTruthy();
257
+ resolve();
258
+ }, 600);
259
+ });
260
+ });
261
+
262
+ it("auto-satisfies scroll requirement when content fits the viewport via contentSizeChange", () => {
263
+ const form = {...baseForm, requireScrollToBottom: true};
264
+ const {getByTestId, queryByTestId} = renderWithTheme(
265
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
266
+ );
267
+ const scroll = getByTestId("consent-form-scroll-view");
268
+ // First set the layout height, then content size smaller than layout
269
+ act(() => {
270
+ fireEvent(scroll, "layout", {nativeEvent: {layout: {height: 500}}});
271
+ });
272
+ act(() => {
273
+ fireEvent(scroll, "contentSizeChange", 0, 400);
274
+ });
275
+ expect(queryByTestId("consent-form-scroll-hint")).toBeNull();
276
+ });
277
+
278
+ it("auto-satisfies scroll requirement when content fits the viewport via layout", () => {
279
+ const form = {...baseForm, requireScrollToBottom: true};
280
+ const {getByTestId, queryByTestId} = renderWithTheme(
281
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
282
+ );
283
+ const scroll = getByTestId("consent-form-scroll-view");
284
+ // First set the content size, then layout height larger than content
285
+ act(() => {
286
+ fireEvent(scroll, "contentSizeChange", 0, 300);
287
+ });
288
+ act(() => {
289
+ fireEvent(scroll, "layout", {nativeEvent: {layout: {height: 500}}});
290
+ });
291
+ expect(queryByTestId("consent-form-scroll-hint")).toBeNull();
292
+ });
293
+
294
+ it("handleScroll returns early when already scrolled to bottom", () => {
295
+ const form = {...baseForm, requireScrollToBottom: false};
296
+ const {getByTestId} = renderWithTheme(
297
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
298
+ );
299
+ const scroll = getByTestId("consent-form-scroll-view");
300
+ // requireScrollToBottom is false so hasScrolledToBottom starts as true.
301
+ // Firing scroll should hit the early return at line 81.
302
+ act(() => {
303
+ fireEvent(scroll, "scroll", {
304
+ nativeEvent: {
305
+ contentOffset: {y: 0},
306
+ contentSize: {height: 1000},
307
+ layoutMeasurement: {height: 500},
308
+ },
309
+ });
310
+ });
311
+ expect(scroll).toBeTruthy();
312
+ });
313
+
211
314
  it("shows the checkbox footer hint when a required checkbox is unchecked", () => {
212
315
  const form: ConsentFormPublic = {
213
316
  ...baseForm,
@@ -232,4 +335,25 @@ describe("ConsentFormScreen", () => {
232
335
  });
233
336
  expect(queryByTestId("consent-footer-checkboxes-hint")).toBeNull();
234
337
  });
338
+
339
+ it("exercises SignatureField onChange, onStart, and onEnd callbacks", () => {
340
+ const form: ConsentFormPublic = {
341
+ ...baseForm,
342
+ captureSignature: true,
343
+ };
344
+ const {UNSAFE_getByType} = renderWithTheme(
345
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
346
+ );
347
+ const sig = UNSAFE_getByType(SignatureField);
348
+ act(() => {
349
+ sig.props.onChange("data:image/png;base64,abc");
350
+ });
351
+ act(() => {
352
+ sig.props.onStart();
353
+ });
354
+ act(() => {
355
+ sig.props.onEnd();
356
+ });
357
+ expect(sig).toBeTruthy();
358
+ });
235
359
  });
@@ -1,5 +1,13 @@
1
1
  import React, {useState} from "react";
2
- import {Pressable, ScrollView} from "react-native";
2
+ import {
3
+ type LayoutChangeEvent,
4
+ type NativeScrollEvent,
5
+ type NativeSyntheticEvent,
6
+ Platform,
7
+ Pressable,
8
+ ScrollView,
9
+ View,
10
+ } from "react-native";
3
11
 
4
12
  import {Box} from "./Box";
5
13
  import {Button} from "./Button";
@@ -62,7 +70,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
62
70
  }
63
71
  };
64
72
 
65
- const handleLayout = (event: any) => {
73
+ const handleLayout = (event: LayoutChangeEvent) => {
66
74
  const h = event.nativeEvent.layout.height;
67
75
  setLayoutHeight(h);
68
76
  if (!hasScrolledToBottom && contentHeight > 0 && h > 0 && contentHeight <= h) {
@@ -70,7 +78,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
70
78
  }
71
79
  };
72
80
 
73
- const handleScroll = (event: any) => {
81
+ const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
74
82
  if (hasScrolledToBottom) {
75
83
  return;
76
84
  }
@@ -160,7 +168,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
160
168
  );
161
169
 
162
170
  return (
163
- <Page color="base" footer={footer} maxWidth="100%" scroll={false} title={form.title}>
171
+ <Page color="base" footer={footer} maxWidth="100%" safeArea scroll={false} title={form.title}>
164
172
  <ScrollView
165
173
  onContentSizeChange={handleContentSizeChange}
166
174
  onLayout={handleLayout}
@@ -206,7 +214,11 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
206
214
  )}
207
215
 
208
216
  {Boolean(form.captureSignature) && (
209
- <Box direction="column" gap={2} testID="consent-form-signature">
217
+ <View
218
+ onTouchEnd={Platform.OS === "ios" ? () => setScrollEnabled(true) : undefined}
219
+ onTouchStart={Platform.OS === "ios" ? () => setScrollEnabled(false) : undefined}
220
+ testID="consent-form-signature"
221
+ >
210
222
  <SignatureField
211
223
  onChange={(value) => setSignatureValue(value)}
212
224
  onEnd={() => setScrollEnabled(true)}
@@ -214,7 +226,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
214
226
  title="Signature"
215
227
  value={signatureValue}
216
228
  />
217
- </Box>
229
+ </View>
218
230
  )}
219
231
 
220
232
  {Boolean(form.requireScrollToBottom && !hasScrolledToBottom) && (
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {describe, expect, it, mock} from "bun:test";
2
3
  import React from "react";
3
4
  import {Pressable} from "react-native";
@@ -10,11 +10,12 @@ import type {SubmitConsentBody} from "./useSubmitConsent";
10
10
  import {useSubmitConsent} from "./useSubmitConsent";
11
11
 
12
12
  interface ConsentNavigatorProps {
13
+ // biome-ignore lint/suspicious/noExplicitAny: RTK Query api instance is a complex generic type that varies per consumer
13
14
  api: any;
14
15
  baseUrl?: string;
15
16
  children: React.ReactNode;
16
17
  extraScreens?: React.ReactNode[];
17
- onError?: (error: any) => void;
18
+ onError?: (error: unknown) => void;
18
19
  variables?: Record<string, string>;
19
20
  }
20
21
 
@@ -48,7 +49,8 @@ export const ConsentNavigator: React.FC<ConsentNavigatorProps> = ({
48
49
  }
49
50
 
50
51
  if (error) {
51
- const status = (error as any)?.status ?? (error as any)?.originalStatus;
52
+ const errorObj = error as {status?: number; originalStatus?: number};
53
+ const status = errorObj?.status ?? errorObj?.originalStatus;
52
54
  console.warn("[ConsentNavigator] Error fetching pending consents:", {error, status});
53
55
  // On auth errors, pass through to let the app handle re-authentication
54
56
  if (status === 401 || status === 403) {
@@ -80,7 +82,7 @@ export const ConsentNavigator: React.FC<ConsentNavigatorProps> = ({
80
82
  `[ConsentNavigator] Showing extra screen ${extraScreenIndex + 1}/${validExtraScreens.length}`
81
83
  );
82
84
  if (React.isValidElement(currentScreen)) {
83
- return React.cloneElement(currentScreen as React.ReactElement<any>, {
85
+ return React.cloneElement(currentScreen as React.ReactElement<{onNext?: () => void}>, {
84
86
  onNext: () => setExtraScreenIndex((i) => i + 1),
85
87
  });
86
88
  }
@@ -101,7 +101,9 @@ export const CustomSelectField: FC<CustomSelectFieldProps> = ({
101
101
  <TextField
102
102
  disabled={disabled}
103
103
  id="customOptions"
104
- inputRef={(ref: any) => (textInputRef.current = ref)}
104
+ inputRef={(ref: TextInput | null) => {
105
+ textInputRef.current = ref;
106
+ }}
105
107
  onChange={onChange}
106
108
  placeholder="None selected"
107
109
  type="text"