@jobber/components-native 0.40.1 → 0.41.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 (93) hide show
  1. package/dist/src/FormatFile/FormatFile.js +114 -0
  2. package/dist/src/FormatFile/FormatFile.style.js +16 -0
  3. package/dist/src/FormatFile/components/ErrorIcon/ErrorIcon.js +8 -0
  4. package/dist/src/FormatFile/components/ErrorIcon/ErrorIcon.style.js +10 -0
  5. package/dist/src/FormatFile/components/ErrorIcon/index.js +1 -0
  6. package/dist/src/FormatFile/components/FileView/FileView.js +67 -0
  7. package/dist/src/FormatFile/components/FileView/FileView.style.js +64 -0
  8. package/dist/src/FormatFile/components/FileView/index.js +1 -0
  9. package/dist/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.js +22 -0
  10. package/dist/src/FormatFile/components/FormatFileBottomSheet/index.js +1 -0
  11. package/dist/src/FormatFile/components/FormatFileBottomSheet/messages.js +13 -0
  12. package/dist/src/FormatFile/components/MediaView/MediaView.js +56 -0
  13. package/dist/src/FormatFile/components/MediaView/MediaView.style.js +27 -0
  14. package/dist/src/FormatFile/components/MediaView/index.js +1 -0
  15. package/dist/src/FormatFile/components/ProgressBar/ProgressBar.js +29 -0
  16. package/dist/src/FormatFile/components/ProgressBar/ProgressBar.style.js +15 -0
  17. package/dist/src/FormatFile/components/ProgressBar/index.js +1 -0
  18. package/dist/src/FormatFile/components/_mocks/mockFiles.js +78 -0
  19. package/dist/src/FormatFile/constants.js +14 -0
  20. package/dist/src/FormatFile/context/FormatFileContext.js +8 -0
  21. package/dist/src/FormatFile/context/types.js +1 -0
  22. package/dist/src/FormatFile/index.js +1 -0
  23. package/dist/src/FormatFile/messages.js +23 -0
  24. package/dist/src/FormatFile/types.js +8 -0
  25. package/dist/src/FormatFile/utils/computeA11yLabel.js +12 -0
  26. package/dist/src/FormatFile/utils/createUseCreateThumbnail.js +22 -0
  27. package/dist/src/FormatFile/utils/index.js +1 -0
  28. package/dist/src/index.js +1 -0
  29. package/dist/src/utils/test/wait.js +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +1 -0
  32. package/dist/types/src/FormatFile/FormatFile.d.ts +47 -0
  33. package/dist/types/src/FormatFile/FormatFile.style.d.ts +14 -0
  34. package/dist/types/src/FormatFile/components/ErrorIcon/ErrorIcon.d.ts +2 -0
  35. package/dist/types/src/FormatFile/components/ErrorIcon/ErrorIcon.style.d.ts +8 -0
  36. package/dist/types/src/FormatFile/components/ErrorIcon/index.d.ts +1 -0
  37. package/dist/types/src/FormatFile/components/FileView/FileView.d.ts +12 -0
  38. package/dist/types/src/FormatFile/components/FileView/FileView.style.d.ts +62 -0
  39. package/dist/types/src/FormatFile/components/FileView/index.d.ts +1 -0
  40. package/dist/types/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.d.ts +11 -0
  41. package/dist/types/src/FormatFile/components/FormatFileBottomSheet/index.d.ts +2 -0
  42. package/dist/types/src/FormatFile/components/FormatFileBottomSheet/messages.d.ts +12 -0
  43. package/dist/types/src/FormatFile/components/MediaView/MediaView.d.ts +12 -0
  44. package/dist/types/src/FormatFile/components/MediaView/MediaView.style.d.ts +25 -0
  45. package/dist/types/src/FormatFile/components/MediaView/index.d.ts +1 -0
  46. package/dist/types/src/FormatFile/components/ProgressBar/ProgressBar.d.ts +19 -0
  47. package/dist/types/src/FormatFile/components/ProgressBar/ProgressBar.style.d.ts +13 -0
  48. package/dist/types/src/FormatFile/components/ProgressBar/index.d.ts +1 -0
  49. package/dist/types/src/FormatFile/components/_mocks/mockFiles.d.ts +18 -0
  50. package/dist/types/src/FormatFile/constants.d.ts +6 -0
  51. package/dist/types/src/FormatFile/context/FormatFileContext.d.ts +10 -0
  52. package/dist/types/src/FormatFile/context/types.d.ts +9 -0
  53. package/dist/types/src/FormatFile/index.d.ts +3 -0
  54. package/dist/types/src/FormatFile/messages.d.ts +22 -0
  55. package/dist/types/src/FormatFile/types.d.ts +105 -0
  56. package/dist/types/src/FormatFile/utils/computeA11yLabel.d.ts +9 -0
  57. package/dist/types/src/FormatFile/utils/createUseCreateThumbnail.d.ts +5 -0
  58. package/dist/types/src/FormatFile/utils/index.d.ts +1 -0
  59. package/dist/types/src/InputCurrency/InputCurrency.d.ts +1 -1
  60. package/dist/types/src/index.d.ts +1 -0
  61. package/package.json +3 -2
  62. package/src/FormatFile/FormatFile.style.ts +17 -0
  63. package/src/FormatFile/FormatFile.test.tsx +333 -0
  64. package/src/FormatFile/FormatFile.tsx +300 -0
  65. package/src/FormatFile/components/ErrorIcon/ErrorIcon.style.ts +11 -0
  66. package/src/FormatFile/components/ErrorIcon/ErrorIcon.tsx +12 -0
  67. package/src/FormatFile/components/ErrorIcon/index.ts +1 -0
  68. package/src/FormatFile/components/FileView/FileView.style.ts +65 -0
  69. package/src/FormatFile/components/FileView/FileView.tsx +134 -0
  70. package/src/FormatFile/components/FileView/index.ts +1 -0
  71. package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.test.tsx +108 -0
  72. package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.tsx +56 -0
  73. package/src/FormatFile/components/FormatFileBottomSheet/index.ts +2 -0
  74. package/src/FormatFile/components/FormatFileBottomSheet/messages.ts +14 -0
  75. package/src/FormatFile/components/MediaView/MediaView.style.ts +28 -0
  76. package/src/FormatFile/components/MediaView/MediaView.tsx +145 -0
  77. package/src/FormatFile/components/MediaView/index.ts +1 -0
  78. package/src/FormatFile/components/ProgressBar/ProgressBar.style.tsx +16 -0
  79. package/src/FormatFile/components/ProgressBar/ProgressBar.tsx +57 -0
  80. package/src/FormatFile/components/ProgressBar/index.ts +1 -0
  81. package/src/FormatFile/components/_mocks/mockFiles.ts +105 -0
  82. package/src/FormatFile/constants.ts +15 -0
  83. package/src/FormatFile/context/FormatFileContext.ts +13 -0
  84. package/src/FormatFile/context/types.ts +12 -0
  85. package/src/FormatFile/index.ts +13 -0
  86. package/src/FormatFile/messages.ts +24 -0
  87. package/src/FormatFile/types.ts +126 -0
  88. package/src/FormatFile/utils/computeA11yLabel.ts +26 -0
  89. package/src/FormatFile/utils/createUseCreateThumbnail.ts +33 -0
  90. package/src/FormatFile/utils/index.ts +1 -0
  91. package/src/InputCurrency/InputCurrency.tsx +1 -1
  92. package/src/index.ts +1 -0
  93. package/src/utils/test/wait.ts +3 -1
@@ -0,0 +1,65 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../../../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ overlay: {
6
+ backgroundColor: tokens["color-overlay--dimmed"],
7
+ },
8
+ overlayError: {
9
+ backgroundColor: tokens["color-overlay--dimmed"],
10
+ borderColor: tokens["color-critical"],
11
+ borderWidth: tokens["border-base"],
12
+ },
13
+ iconCenter: {
14
+ marginBottom: tokens["space-large"],
15
+ },
16
+ fileBackground: {
17
+ width: "100%",
18
+ display: "flex",
19
+ alignItems: "center",
20
+ },
21
+ fileBackgroundGrid: {
22
+ height: "100%",
23
+ },
24
+ fileBackgroundFlat: {
25
+ justifyContent: "center",
26
+ height: tokens["space-extravagant"],
27
+ },
28
+ fileIconGrid: {
29
+ top: tokens["space-small"],
30
+ },
31
+ fileIconFlat: {
32
+ top: tokens["space-smaller"],
33
+ },
34
+ fileNameError: {
35
+ alignItems: "center",
36
+ borderTopColor: tokens["color-border"],
37
+ borderTopWidth: tokens["space-minuscule"],
38
+ width: "100%",
39
+ marginTop: tokens["space-base"],
40
+ },
41
+ fileName: {
42
+ alignItems: "center",
43
+ borderTopColor: tokens["color-border"],
44
+ borderTopWidth: tokens["space-minuscule"],
45
+ width: "100%",
46
+ },
47
+ fileNameGrid: {
48
+ position: "absolute",
49
+ right: 0,
50
+ bottom: 0,
51
+ left: 0,
52
+ },
53
+ fileNameFlat: {
54
+ top: tokens["space-small"],
55
+ },
56
+ fileOverlay: {
57
+ position: "absolute",
58
+ justifyContent: "center",
59
+ right: 0,
60
+ bottom: 0,
61
+ left: 0,
62
+ height: "100%",
63
+ paddingHorizontal: tokens["space-small"],
64
+ },
65
+ });
@@ -0,0 +1,134 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { useIntl } from "react-intl";
4
+ import { IconNames } from "@jobber/design";
5
+ import { styles } from "./FileView.style";
6
+ import { Icon } from "../../../Icon";
7
+ import { Text } from "../../../Text";
8
+ import { FormattedFile, StatusCode } from "../../types";
9
+ import { computeA11yLabel } from "../../utils";
10
+ import { ProgressBar } from "../ProgressBar";
11
+ import { ErrorIcon } from "../ErrorIcon";
12
+
13
+ interface FileViewProps {
14
+ accessibilityLabel?: string;
15
+ showOverlay: boolean;
16
+ showError: boolean;
17
+ file: FormattedFile;
18
+ styleInGrid: boolean;
19
+ onUploadComplete: () => void;
20
+ }
21
+
22
+ export function FileView({
23
+ accessibilityLabel,
24
+ styleInGrid,
25
+ file,
26
+ showOverlay,
27
+ showError,
28
+ onUploadComplete,
29
+ }: FileViewProps): JSX.Element {
30
+ const { formatMessage } = useIntl();
31
+ const a11yLabel = computeA11yLabel({
32
+ accessibilityLabel,
33
+ showOverlay,
34
+ showError,
35
+ formatMessage,
36
+ });
37
+
38
+ const freezeProgressBar =
39
+ file.status !== StatusCode.Completed && file.progress >= 0.9;
40
+ return (
41
+ <View
42
+ style={[
43
+ styles.fileBackground,
44
+ styleInGrid ? styles.fileBackgroundGrid : styles.fileBackgroundFlat,
45
+ ]}
46
+ accessible={true}
47
+ accessibilityLabel={a11yLabel}
48
+ >
49
+ <View
50
+ style={[
51
+ styles.fileBackground,
52
+ styleInGrid ? styles.fileIconGrid : styles.fileIconFlat,
53
+ ]}
54
+ testID={"test-file"}
55
+ >
56
+ {(styleInGrid || !showError) && file.showFileTypeIndicator && (
57
+ <Icon
58
+ size={"large"}
59
+ name={mapFileTypeToIconName({
60
+ fileName: file.name,
61
+ fileType: file.type,
62
+ })}
63
+ />
64
+ )}
65
+ </View>
66
+ <View
67
+ style={[
68
+ showError && styleInGrid ? styles.fileNameError : styles.fileName,
69
+ styleInGrid ? styles.fileNameGrid : styles.fileNameFlat,
70
+ ]}
71
+ >
72
+ <Text level="textSupporting" variation="subdued" maxLines="single">
73
+ {file.name}
74
+ </Text>
75
+ </View>
76
+ {!showError && showOverlay && (
77
+ <View
78
+ style={[styles.fileBackground, styles.overlay, styles.fileOverlay]}
79
+ testID="format-file-progress-bar-container"
80
+ >
81
+ <ProgressBar
82
+ status={file.status}
83
+ progress={freezeProgressBar ? 0.9 : file.progress}
84
+ onComplete={onUploadComplete}
85
+ />
86
+ </View>
87
+ )}
88
+ {showError && (
89
+ <View
90
+ style={[
91
+ styles.fileBackground,
92
+ styles.overlayError,
93
+ styles.fileOverlay,
94
+ ]}
95
+ testID="format-file-error-container"
96
+ >
97
+ <View style={!styleInGrid ? styles.iconCenter : undefined}>
98
+ <ErrorIcon />
99
+ </View>
100
+ </View>
101
+ )}
102
+ </View>
103
+ );
104
+ }
105
+
106
+ interface FileTypeToIconNameParams {
107
+ fileName?: string;
108
+ fileType?: string;
109
+ }
110
+ function mapFileTypeToIconName({
111
+ fileName,
112
+ fileType,
113
+ }: FileTypeToIconNameParams): IconNames {
114
+ if (!fileName && !fileType) {
115
+ return "alert";
116
+ }
117
+ if (fileType?.includes("pdf") || fileName?.match(/~*.pdf$/)) {
118
+ return "pdf";
119
+ } else if (
120
+ fileType?.includes("ms-word") ||
121
+ fileName?.match(/~*.doc$|docx$/)
122
+ ) {
123
+ return "word";
124
+ } else if (
125
+ fileType?.includes("ms-excel") ||
126
+ fileName?.match(/~*.xls$|xlsx$/)
127
+ ) {
128
+ return "excel";
129
+ } else if (fileType?.includes("video") || fileName?.match(/~*.mp4$|mp4$/)) {
130
+ return "video";
131
+ } else {
132
+ return "file";
133
+ }
134
+ }
@@ -0,0 +1 @@
1
+ export { FileView } from "./FileView";
@@ -0,0 +1,108 @@
1
+ import React, { createRef } from "react";
2
+ import { RenderAPI, fireEvent, render } from "@testing-library/react-native";
3
+ import { Host } from "react-native-portalize";
4
+ import { useIntl } from "react-intl";
5
+ import { act } from "react-test-renderer";
6
+ import {
7
+ BottomSheetOptionsSuffix,
8
+ FormatFileBottomSheet,
9
+ } from "./FormatFileBottomSheet";
10
+ import { messages } from "./messages";
11
+ import { BottomSheetRef } from "../../../BottomSheet/BottomSheet";
12
+
13
+ let Platform: { OS: "ios" | "android" };
14
+ const onRemove = jest.fn();
15
+ const onPreview = jest.fn();
16
+ const bottomSheetRef = createRef<BottomSheetRef>();
17
+
18
+ beforeEach(() => {
19
+ Platform = require("react-native").Platform;
20
+ });
21
+
22
+ const renderBottomSheet = (
23
+ bottomSheetOptionsSuffix: BottomSheetOptionsSuffix,
24
+ ) => {
25
+ return render(
26
+ <Host>
27
+ <FormatFileBottomSheet
28
+ onPreviewPress={onPreview}
29
+ onRemovePress={onRemove}
30
+ bottomSheetRef={bottomSheetRef}
31
+ bottomSheetOptionsSuffix={bottomSheetOptionsSuffix}
32
+ />
33
+ </Host>,
34
+ );
35
+ };
36
+
37
+ const basicRenderTestWithValue = () => {
38
+ describe.each([["image"], ["receipt"], ["file"], ["video"]])(
39
+ "when FormatFileBottomSheet for %s is opened",
40
+ bottomSheetOptionsSuffix => {
41
+ const { formatMessage } = useIntl();
42
+ const previewLabel = formatMessage(messages.lightBoxPreviewButton, {
43
+ bottomSheetOptionsSuffix,
44
+ });
45
+ const removeLabel = formatMessage(messages.removeButton, {
46
+ bottomSheetOptionsSuffix,
47
+ });
48
+ let tree: RenderAPI;
49
+
50
+ beforeEach(() => {
51
+ tree = renderBottomSheet(
52
+ bottomSheetOptionsSuffix as BottomSheetOptionsSuffix,
53
+ );
54
+ act(() => {
55
+ bottomSheetRef.current?.open();
56
+ });
57
+ });
58
+ afterEach(() => {
59
+ jest.clearAllMocks();
60
+ });
61
+
62
+ describe("onPreviewPress", () => {
63
+ it("renders the preview option", () => {
64
+ const { getByLabelText } = tree;
65
+
66
+ expect(getByLabelText(previewLabel)).toBeDefined();
67
+ });
68
+
69
+ it("is called when pressed", () => {
70
+ const { getByLabelText } = tree;
71
+ fireEvent.press(getByLabelText(previewLabel));
72
+ expect(onPreview).toHaveBeenCalledTimes(1);
73
+ });
74
+ });
75
+
76
+ describe("onRemovePress", () => {
77
+ it("renders the remove option", () => {
78
+ const { getByLabelText } = tree;
79
+
80
+ expect(getByLabelText(removeLabel)).toBeDefined();
81
+ });
82
+
83
+ it("is called when pressed", () => {
84
+ const { getByLabelText } = tree;
85
+ fireEvent.press(getByLabelText(removeLabel));
86
+
87
+ expect(onRemove).toHaveBeenCalledTimes(1);
88
+ });
89
+ });
90
+ },
91
+ );
92
+ };
93
+
94
+ describe("ios", () => {
95
+ beforeEach(() => {
96
+ Platform.OS = "ios";
97
+ });
98
+
99
+ basicRenderTestWithValue();
100
+ });
101
+
102
+ describe("android", () => {
103
+ beforeEach(() => {
104
+ Platform.OS = "android";
105
+ });
106
+
107
+ basicRenderTestWithValue();
108
+ });
@@ -0,0 +1,56 @@
1
+ import React, { RefObject } from "react";
2
+ import { Portal } from "react-native-portalize";
3
+ import { useIntl } from "react-intl";
4
+ import { messages } from "./messages";
5
+ import { BottomSheet, BottomSheetRef } from "../../../BottomSheet/BottomSheet";
6
+ import { BottomSheetOption } from "../../../BottomSheet/components/BottomSheetOption";
7
+
8
+ export type BottomSheetOptionsSuffix = "receipt" | "image" | "file" | "video";
9
+
10
+ interface FormatFileBottomSheetProps {
11
+ bottomSheetRef: RefObject<BottomSheetRef>;
12
+ onPreviewPress?: () => void;
13
+ onRemovePress?: () => void;
14
+ bottomSheetOptionsSuffix?: BottomSheetOptionsSuffix;
15
+ }
16
+
17
+ export const FormatFileBottomSheet = ({
18
+ bottomSheetRef,
19
+ onPreviewPress,
20
+ onRemovePress,
21
+ bottomSheetOptionsSuffix,
22
+ }: FormatFileBottomSheetProps): JSX.Element => {
23
+ const { formatMessage } = useIntl();
24
+
25
+ const handlePress = (onPressAction: () => void) => {
26
+ onPressAction();
27
+
28
+ bottomSheetRef.current?.close();
29
+ };
30
+
31
+ return (
32
+ <Portal>
33
+ <BottomSheet ref={bottomSheetRef}>
34
+ {onPreviewPress ? (
35
+ <BottomSheetOption
36
+ icon={"eye"}
37
+ text={formatMessage(messages.lightBoxPreviewButton, {
38
+ bottomSheetOptionsSuffix,
39
+ })}
40
+ onPress={() => handlePress(onPreviewPress)}
41
+ />
42
+ ) : undefined}
43
+ {onRemovePress ? (
44
+ <BottomSheetOption
45
+ icon={"trash"}
46
+ destructive={true}
47
+ text={formatMessage(messages.removeButton, {
48
+ bottomSheetOptionsSuffix,
49
+ })}
50
+ onPress={() => handlePress(onRemovePress)}
51
+ />
52
+ ) : undefined}
53
+ </BottomSheet>
54
+ </Portal>
55
+ );
56
+ };
@@ -0,0 +1,2 @@
1
+ export { FormatFileBottomSheet } from "./FormatFileBottomSheet";
2
+ export type { BottomSheetOptionsSuffix } from "./FormatFileBottomSheet";
@@ -0,0 +1,14 @@
1
+ import { defineMessages } from "react-intl";
2
+
3
+ export const messages = defineMessages({
4
+ lightBoxPreviewButton: {
5
+ id: "lightBoxPreviewButton",
6
+ defaultMessage: "Preview {bottomSheetOptionsSuffix}",
7
+ description: "Label for button when preview the file",
8
+ },
9
+ removeButton: {
10
+ id: "removeButton",
11
+ defaultMessage: "Remove {bottomSheetOptionsSuffix}",
12
+ description: "Label for button when remove the file",
13
+ },
14
+ });
@@ -0,0 +1,28 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../../../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ imageBackground: {
6
+ width: "100%",
7
+ justifyContent: "center",
8
+ alignItems: "center",
9
+ },
10
+ imageBackgroundFlat: {
11
+ aspectRatio: 375 / 150,
12
+ },
13
+ imageBackgroundGrid: {
14
+ height: "100%",
15
+ },
16
+ overlay: {
17
+ height: "100%",
18
+ backgroundColor: tokens["color-overlay--dimmed"],
19
+ paddingHorizontal: tokens["space-small"],
20
+ },
21
+ overlayError: {
22
+ height: "100%",
23
+ backgroundColor: tokens["color-overlay--dimmed"],
24
+ paddingHorizontal: tokens["space-small"],
25
+ borderColor: tokens["color-critical"],
26
+ borderWidth: tokens["border-base"],
27
+ },
28
+ });
@@ -0,0 +1,145 @@
1
+ import React, { useState } from "react";
2
+ import { ImageBackground, View } from "react-native";
3
+ import { useIntl } from "react-intl";
4
+ import { styles } from "./MediaView.style";
5
+ import { FormattedFile, StatusCode } from "../../types";
6
+ import { computeA11yLabel } from "../../utils";
7
+ import { ActivityIndicator } from "../../../ActivityIndicator";
8
+ import { Icon } from "../../../Icon";
9
+ import { ProgressBar } from "../ProgressBar";
10
+ import { ErrorIcon } from "../ErrorIcon";
11
+ import { useAtlantisFormatFileContext } from "../../context/FormatFileContext";
12
+
13
+ interface MediaViewProps {
14
+ accessibilityLabel?: string;
15
+ showOverlay: boolean;
16
+ showError: boolean;
17
+ file: FormattedFile;
18
+ styleInGrid: boolean;
19
+ onUploadComplete: () => void;
20
+ }
21
+
22
+ export function MediaView({
23
+ accessibilityLabel,
24
+ showOverlay,
25
+ showError,
26
+ file,
27
+ styleInGrid,
28
+ onUploadComplete,
29
+ }: MediaViewProps): JSX.Element {
30
+ const { formatMessage } = useIntl();
31
+ const { useCreateThumbnail } = useAtlantisFormatFileContext();
32
+ const { thumbnail, error } = useCreateThumbnail(file);
33
+ const [isLoading, setIsLoading] = useState(false);
34
+
35
+ const a11yLabel = computeA11yLabel({
36
+ accessibilityLabel,
37
+ showOverlay,
38
+ showError,
39
+ formatMessage,
40
+ });
41
+
42
+ const hasError = showError || error;
43
+ const uri = thumbnail || file.thumbnailUrl || file.source;
44
+
45
+ return (
46
+ <View accessible={true} accessibilityLabel={a11yLabel}>
47
+ <ImageBackground
48
+ style={[
49
+ styles.imageBackground,
50
+ styleInGrid ? styles.imageBackgroundGrid : styles.imageBackgroundFlat,
51
+ ]}
52
+ resizeMode={styleInGrid ? "cover" : "contain"}
53
+ source={{ uri }}
54
+ testID={"test-image"}
55
+ onLoadStart={() => setIsLoading(true)}
56
+ onLoadEnd={() => setIsLoading(false)}
57
+ >
58
+ <Overlay
59
+ isLoading={isLoading}
60
+ showOverlay={showOverlay}
61
+ hasError={hasError}
62
+ file={file}
63
+ onUploadComplete={onUploadComplete}
64
+ />
65
+ </ImageBackground>
66
+ </View>
67
+ );
68
+ }
69
+
70
+ interface OverlayProps {
71
+ isLoading: boolean;
72
+ showOverlay: boolean;
73
+ hasError: boolean;
74
+ file: FormattedFile;
75
+ onUploadComplete: () => void;
76
+ }
77
+
78
+ function Overlay({
79
+ isLoading,
80
+ showOverlay,
81
+ hasError,
82
+ file,
83
+ onUploadComplete,
84
+ }: OverlayProps) {
85
+ if (isLoading) return <ActivityIndicator />;
86
+
87
+ if (hasError) return <ErrorOverlay />;
88
+
89
+ if (showOverlay) {
90
+ return (
91
+ <ProgressOverlay
92
+ status={file.status}
93
+ progress={file.progress}
94
+ onUploadComplete={onUploadComplete}
95
+ />
96
+ );
97
+ }
98
+
99
+ if (isVideo(file.type) && file.showFileTypeIndicator) {
100
+ return <Icon name="video" color="white" />;
101
+ }
102
+
103
+ return <></>;
104
+ }
105
+
106
+ interface ProgressOverlayProps {
107
+ status: StatusCode;
108
+ progress: number;
109
+ onUploadComplete: () => void;
110
+ }
111
+
112
+ function ProgressOverlay({
113
+ status,
114
+ progress,
115
+ onUploadComplete,
116
+ }: ProgressOverlayProps) {
117
+ const freezeProgressBar = status !== StatusCode.Completed && progress >= 0.9;
118
+ return (
119
+ <View
120
+ style={[styles.imageBackground, styles.overlay]}
121
+ testID="format-file-progress-bar-container"
122
+ >
123
+ <ProgressBar
124
+ status={status}
125
+ progress={freezeProgressBar ? 0.9 : progress}
126
+ onComplete={onUploadComplete}
127
+ />
128
+ </View>
129
+ );
130
+ }
131
+
132
+ function ErrorOverlay() {
133
+ return (
134
+ <View
135
+ style={[styles.imageBackground, styles.overlayError]}
136
+ testID="format-file-error-container"
137
+ >
138
+ <ErrorIcon />
139
+ </View>
140
+ );
141
+ }
142
+
143
+ function isVideo(fileType = ""): boolean {
144
+ return fileType.includes("video");
145
+ }
@@ -0,0 +1 @@
1
+ export { MediaView } from "./MediaView";
@@ -0,0 +1,16 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../../../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ container: {
6
+ width: "100%",
7
+ height: 8,
8
+ borderRadius: tokens["radius-circle"],
9
+ overflow: "hidden",
10
+ backgroundColor: tokens["color-surface--background"],
11
+ },
12
+ progress: {
13
+ height: "100%",
14
+ backgroundColor: tokens["color-green"],
15
+ },
16
+ });
@@ -0,0 +1,57 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Animated, View } from "react-native";
3
+ import { styles } from "./ProgressBar.style";
4
+ import { StatusCode } from "../../types";
5
+
6
+ interface ProgressBarProps {
7
+ /**
8
+ * Upload progress value from 0 to 1
9
+ */
10
+ progress: number;
11
+ /**
12
+ * Upload status
13
+ */
14
+ status: StatusCode;
15
+ /**
16
+ * Function to be called when the progress is finished
17
+ */
18
+ onComplete?: () => void;
19
+ }
20
+
21
+ export const ProgressBar = ({
22
+ progress,
23
+ status,
24
+ onComplete,
25
+ }: ProgressBarProps): JSX.Element => {
26
+ const barWidth = useRef(new Animated.Value(0)).current;
27
+ const progressPercentage = barWidth.interpolate({
28
+ inputRange: [0, 1],
29
+ outputRange: ["0%", `100%`],
30
+ });
31
+
32
+ useEffect(() => {
33
+ Animated.timing(barWidth, {
34
+ toValue: progress,
35
+ duration: 500,
36
+ useNativeDriver: false,
37
+ }).start(({ finished }) => {
38
+ if (status === StatusCode.Completed && finished && onComplete) {
39
+ onComplete();
40
+ }
41
+ });
42
+ }, [progress, barWidth, onComplete, status]);
43
+
44
+ return (
45
+ <View testID="format-file-progress-bar" style={styles.container}>
46
+ <Animated.View
47
+ testID={"format-file-inner-progress-bar"}
48
+ style={[
49
+ styles.progress,
50
+ {
51
+ width: progressPercentage,
52
+ },
53
+ ]}
54
+ />
55
+ </View>
56
+ );
57
+ };
@@ -0,0 +1 @@
1
+ export { ProgressBar } from "./ProgressBar";