@jobber/components-native 0.89.4 → 0.90.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.
@@ -2,7 +2,7 @@ import type { DeepPartial, FieldValues, UseFormHandleSubmit, UseFormReturn } fro
2
2
  import type { MutableRefObject, RefObject } from "react";
3
3
  import type { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
4
4
  import type { InternalFormProps } from "../types";
5
- type UseInternalFormProps<T extends FieldValues, SubmitResponseType> = Pick<InternalFormProps<T, SubmitResponseType>, "mode" | "reValidateMode" | "initialValues" | "formRef" | "localCacheKey" | "localCacheExclude" | "localCacheId"> & {
5
+ type UseInternalFormProps<T extends FieldValues, SubmitResponseType> = Pick<InternalFormProps<T, SubmitResponseType>, "mode" | "reValidateMode" | "initialValues" | "formRef" | "localCacheKey" | "localCacheExclude" | "localCacheId" | "UNSAFE_allowDiscardLocalCacheWhenOffline"> & {
6
6
  scrollViewRef?: RefObject<KeyboardAwareScrollView>;
7
7
  readonly saveButtonHeight: number;
8
8
  readonly messageBannerHeight: number;
@@ -15,5 +15,5 @@ interface UseInternalForm<T extends FieldValues> {
15
15
  readonly removeListenerRef: MutableRefObject<() => void>;
16
16
  readonly setLocalCache: (data: DeepPartial<T>) => void;
17
17
  }
18
- export declare function useInternalForm<T extends FieldValues, SubmitResponseType>({ mode, reValidateMode, initialValues, formRef, localCacheKey, localCacheId, scrollViewRef, saveButtonHeight, messageBannerHeight, }: UseInternalFormProps<T, SubmitResponseType>): UseInternalForm<T>;
18
+ export declare function useInternalForm<T extends FieldValues, SubmitResponseType>({ mode, reValidateMode, initialValues, formRef, localCacheKey, localCacheId, scrollViewRef, saveButtonHeight, messageBannerHeight, UNSAFE_allowDiscardLocalCacheWhenOffline, }: UseInternalFormProps<T, SubmitResponseType>): UseInternalForm<T>;
19
19
  export {};
@@ -131,6 +131,12 @@ export interface FormProps<T extends FieldValues, SubmitResponseType> {
131
131
  * If a user opens the same form the data will only be loaded if the `localCacheId` matches
132
132
  */
133
133
  localCacheId?: string | string[];
134
+ /**
135
+ * If true, the local cache will be removed when the user navigates away from
136
+ * the dirty form even when offline. By default, cache is only removed on back when online.
137
+ * Defaults to false.
138
+ */
139
+ UNSAFE_allowDiscardLocalCacheWhenOffline?: boolean;
134
140
  /**
135
141
  * Secondary Action for ButtonGroup
136
142
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.89.4",
3
+ "version": "0.90.0",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -94,5 +94,5 @@
94
94
  "react-native-safe-area-context": "^5.4.0",
95
95
  "react-native-svg": ">=12.0.0"
96
96
  },
97
- "gitHead": "adc76ab79cbad1b62fa1d3ff35020f061b390292"
97
+ "gitHead": "39c1c0fc8175f82c32c7636b972712a710fbf527"
98
98
  }
@@ -104,6 +104,7 @@ interface FormTestProps {
104
104
  readonly onBeforeSubmit?: jest.Mock;
105
105
  readonly renderFooter?: React.ReactNode;
106
106
  readonly saveButtonOffset?: number;
107
+ readonly UNSAFE_allowDiscardLocalCacheWhenOffline?: boolean;
107
108
  }
108
109
 
109
110
  function FormTest(props: FormTestProps) {
@@ -125,6 +126,7 @@ function MockForm({
125
126
  localCacheId,
126
127
  renderFooter,
127
128
  saveButtonOffset,
129
+ UNSAFE_allowDiscardLocalCacheWhenOffline = false,
128
130
  }: FormTestProps) {
129
131
  const formErrors: FormBannerErrors = {};
130
132
 
@@ -154,6 +156,9 @@ function MockForm({
154
156
  onBeforeSubmit={onBeforeSubmit}
155
157
  renderFooter={renderFooter}
156
158
  saveButtonOffset={saveButtonOffset}
159
+ UNSAFE_allowDiscardLocalCacheWhenOffline={
160
+ UNSAFE_allowDiscardLocalCacheWhenOffline
161
+ }
157
162
  >
158
163
  <InputText
159
164
  name={testInputTextName}
@@ -561,4 +566,142 @@ describe("Form", () => {
561
566
  expect(queryByTestId("ATL-FormSafeArea")).toBeNull();
562
567
  });
563
568
  });
569
+
570
+ describe("Leaving the form", () => {
571
+ let mockUseConfirmBeforeBack: jest.Mock;
572
+ let mockRemoveLocalCache: jest.Mock;
573
+ const atlantisContextSpy = jest.spyOn(
574
+ atlantisContext,
575
+ "useAtlantisContext",
576
+ );
577
+
578
+ beforeEach(() => {
579
+ mockUseConfirmBeforeBack = jest
580
+ .fn()
581
+ .mockReturnValue({ current: jest.fn() });
582
+ mockRemoveLocalCache = jest.fn();
583
+
584
+ jest
585
+ .spyOn(
586
+ require("../Form/context/AtlantisFormContext"),
587
+ "useAtlantisFormContext",
588
+ )
589
+ .mockReturnValue({
590
+ useConfirmBeforeBack: mockUseConfirmBeforeBack,
591
+ useInternalFormLocalCache: () => ({
592
+ setLocalCache: jest.fn(),
593
+ removeLocalCache: mockRemoveLocalCache,
594
+ }),
595
+ edgeToEdgeEnabled: false,
596
+ });
597
+ });
598
+
599
+ afterEach(() => {
600
+ jest.restoreAllMocks();
601
+ });
602
+
603
+ describe("when UNSAFE_allowDiscardLocalCacheWhenOffline is false", () => {
604
+ it("should NOT pass onAcceptEvent when offline", () => {
605
+ atlantisContextSpy.mockReturnValue({
606
+ ...atlantisContextDefaultValues,
607
+ isOnline: false,
608
+ });
609
+
610
+ render(
611
+ <FormTest
612
+ onSubmit={onSubmitMock}
613
+ UNSAFE_allowDiscardLocalCacheWhenOffline={false}
614
+ localCacheKey="testCacheKey"
615
+ />,
616
+ );
617
+
618
+ expect(mockUseConfirmBeforeBack).toHaveBeenCalled();
619
+ const callArgs = mockUseConfirmBeforeBack.mock.calls[0][0];
620
+ expect(callArgs.onAcceptEvent).toBeUndefined();
621
+ expect(callArgs.showLostProgressMessage).toBe(false);
622
+ });
623
+
624
+ it("should pass onAcceptEvent when online", () => {
625
+ atlantisContextSpy.mockReturnValue({
626
+ ...atlantisContextDefaultValues,
627
+ isOnline: true,
628
+ });
629
+
630
+ render(
631
+ <FormTest
632
+ onSubmit={onSubmitMock}
633
+ UNSAFE_allowDiscardLocalCacheWhenOffline={false}
634
+ localCacheKey="testCacheKey"
635
+ />,
636
+ );
637
+
638
+ expect(mockUseConfirmBeforeBack).toHaveBeenCalled();
639
+ const callArgs = mockUseConfirmBeforeBack.mock.calls[0][0];
640
+ expect(callArgs.onAcceptEvent).toBe(mockRemoveLocalCache);
641
+ expect(callArgs.showLostProgressMessage).toBe(true);
642
+ });
643
+ });
644
+
645
+ describe("when UNSAFE_allowDiscardLocalCacheWhenOffline is true", () => {
646
+ it("should pass onAcceptEvent when offline", () => {
647
+ atlantisContextSpy.mockReturnValue({
648
+ ...atlantisContextDefaultValues,
649
+ isOnline: false,
650
+ });
651
+
652
+ render(
653
+ <FormTest
654
+ onSubmit={onSubmitMock}
655
+ UNSAFE_allowDiscardLocalCacheWhenOffline={true}
656
+ localCacheKey="testCacheKey"
657
+ />,
658
+ );
659
+
660
+ expect(mockUseConfirmBeforeBack).toHaveBeenCalled();
661
+ const callArgs = mockUseConfirmBeforeBack.mock.calls[0][0];
662
+ expect(callArgs.onAcceptEvent).toBe(mockRemoveLocalCache);
663
+ expect(callArgs.showLostProgressMessage).toBe(true);
664
+ });
665
+
666
+ it("should pass onAcceptEvent when online", () => {
667
+ atlantisContextSpy.mockReturnValue({
668
+ ...atlantisContextDefaultValues,
669
+ isOnline: true,
670
+ });
671
+
672
+ render(
673
+ <FormTest
674
+ onSubmit={onSubmitMock}
675
+ UNSAFE_allowDiscardLocalCacheWhenOffline={true}
676
+ localCacheKey="testCacheKey"
677
+ />,
678
+ );
679
+
680
+ expect(mockUseConfirmBeforeBack).toHaveBeenCalled();
681
+ const callArgs = mockUseConfirmBeforeBack.mock.calls[0][0];
682
+ expect(callArgs.onAcceptEvent).toBe(mockRemoveLocalCache);
683
+ expect(callArgs.showLostProgressMessage).toBe(true);
684
+ });
685
+ });
686
+
687
+ describe("without localCacheKey", () => {
688
+ it("should always show lost progress message when no cache key is provided", () => {
689
+ atlantisContextSpy.mockReturnValue({
690
+ ...atlantisContextDefaultValues,
691
+ isOnline: false,
692
+ });
693
+
694
+ render(
695
+ <FormTest
696
+ onSubmit={onSubmitMock}
697
+ UNSAFE_allowDiscardLocalCacheWhenOffline={false}
698
+ />,
699
+ );
700
+
701
+ expect(mockUseConfirmBeforeBack).toHaveBeenCalled();
702
+ const callArgs = mockUseConfirmBeforeBack.mock.calls[0][0];
703
+ expect(callArgs.showLostProgressMessage).toBe(true);
704
+ });
705
+ });
706
+ });
564
707
  });
package/src/Form/Form.tsx CHANGED
@@ -66,6 +66,7 @@ function InternalForm<T extends FieldValues, S>({
66
66
  saveButtonOffset,
67
67
  showStickySaveButton = false,
68
68
  renderFooter,
69
+ UNSAFE_allowDiscardLocalCacheWhenOffline,
69
70
  }: InternalFormProps<T, S>) {
70
71
  const { scrollViewRef, bottomViewRef, scrollToTop } = useFormViewRefs();
71
72
  const [saveButtonHeight, setSaveButtonHeight] = useState(0);
@@ -87,6 +88,7 @@ function InternalForm<T extends FieldValues, S>({
87
88
  scrollViewRef,
88
89
  saveButtonHeight,
89
90
  messageBannerHeight,
91
+ UNSAFE_allowDiscardLocalCacheWhenOffline,
90
92
  });
91
93
  const { windowHeight, headerHeight } = useScreenInformation();
92
94
  const [keyboardHeight, setKeyboardHeight] = useState(0);
@@ -20,6 +20,7 @@ type UseInternalFormProps<T extends FieldValues, SubmitResponseType> = Pick<
20
20
  | "localCacheKey"
21
21
  | "localCacheExclude"
22
22
  | "localCacheId"
23
+ | "UNSAFE_allowDiscardLocalCacheWhenOffline"
23
24
  > & {
24
25
  scrollViewRef?: RefObject<KeyboardAwareScrollView>;
25
26
  readonly saveButtonHeight: number;
@@ -45,6 +46,7 @@ export function useInternalForm<T extends FieldValues, SubmitResponseType>({
45
46
  scrollViewRef,
46
47
  saveButtonHeight,
47
48
  messageBannerHeight,
49
+ UNSAFE_allowDiscardLocalCacheWhenOffline = false,
48
50
  }: UseInternalFormProps<T, SubmitResponseType>): UseInternalForm<T> {
49
51
  const { useConfirmBeforeBack, useInternalFormLocalCache } =
50
52
  useAtlantisFormContext();
@@ -82,11 +84,16 @@ export function useInternalForm<T extends FieldValues, SubmitResponseType>({
82
84
  };
83
85
  }
84
86
 
87
+ const shouldRemoveCacheOnBack = UNSAFE_allowDiscardLocalCacheWhenOffline
88
+ ? true
89
+ : isOnline;
90
+
85
91
  const removeListenerRef = useConfirmBeforeBack({
86
92
  alwaysPreventBack: isSubmitting,
87
93
  shouldShowAlert: isDirty,
88
- onAcceptEvent: isOnline ? removeLocalCache : undefined,
89
- showLostProgressMessage: isOnline || !clientSideSaveOn ? true : false,
94
+ onAcceptEvent: shouldRemoveCacheOnBack ? removeLocalCache : undefined,
95
+ showLostProgressMessage:
96
+ shouldRemoveCacheOnBack || !clientSideSaveOn ? true : false,
90
97
  });
91
98
 
92
99
  return {
package/src/Form/types.ts CHANGED
@@ -171,6 +171,13 @@ export interface FormProps<T extends FieldValues, SubmitResponseType> {
171
171
  */
172
172
  localCacheId?: string | string[];
173
173
 
174
+ /**
175
+ * If true, the local cache will be removed when the user navigates away from
176
+ * the dirty form even when offline. By default, cache is only removed on back when online.
177
+ * Defaults to false.
178
+ */
179
+ UNSAFE_allowDiscardLocalCacheWhenOffline?: boolean;
180
+
174
181
  /**
175
182
  * Secondary Action for ButtonGroup
176
183
  */