Skip to content
This repository is currently being migrated. It's locked while the migration is in progress.
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6466949
Add new file upload component
AdryienH Apr 1, 2026
3185823
Add image preview
AdryienH Apr 1, 2026
5555f2c
Create separate FilePreview component
AdryienH Apr 1, 2026
f146f15
FIle validation
AdryienH Apr 7, 2026
7f02322
Update submit evidence flow
AdryienH Apr 9, 2026
e08c2a5
Layout fix
AdryienH Apr 10, 2026
1928c3c
Cleanup
AdryienH Apr 10, 2026
2a1781c
Add feature toggle
AdryienH Apr 10, 2026
dc6e941
Fix file spec blurb
AdryienH Apr 10, 2026
394ea0a
Start new message flow
AdryienH Apr 10, 2026
f932c14
Reply message flow
AdryienH Apr 10, 2026
89d0261
Edit draft flow
AdryienH Apr 10, 2026
34e7eb8
Fix theme color
AdryienH Apr 10, 2026
b17514c
Cleanup comments
AdryienH Apr 10, 2026
a458550
Add waygate for new file upload screen
AdryienH Apr 10, 2026
710db37
Merge branch 'develop' into feature/12088-new-file-upload
AdryienH Apr 11, 2026
b3fff72
Revert formatting changes
AdryienH Apr 11, 2026
52c9918
Lint fixes
AdryienH Apr 13, 2026
d88580a
Add delete modal
AdryienH Apr 13, 2026
004c1c9
Hide file upload when reply disabled
AdryienH Apr 13, 2026
0617527
Fix translation
AdryienH Apr 13, 2026
ec54034
Update a11y label
AdryienH Apr 13, 2026
32ba221
Merge branch 'develop' into feature/12088-new-file-upload
AdryienH Apr 14, 2026
eadd5fe
Include file name in a11y label
AdryienH Apr 14, 2026
2a8a0f0
Merge branch 'develop' into feature/12088-new-file-upload
AdryienH Apr 15, 2026
0517200
Move file upload util fns
AdryienH Apr 20, 2026
3cae0e0
Multiple file error display
AdryienH Apr 21, 2026
773063f
Fix cancel text
AdryienH Apr 21, 2026
ee1388f
Add file upload on file request
AdryienH Apr 21, 2026
e94b3cb
Make file list optional
AdryienH Apr 22, 2026
88ab690
Merge remote branch 'develop' into feature/12088-new-file-upload
AdryienH Apr 24, 2026
688822a
Fix message attachment submission
AdryienH Apr 26, 2026
aa6577b
Cleanup old analytics
AdryienH Apr 26, 2026
93f17f0
Revert formatting changes
AdryienH Apr 28, 2026
3d5cb87
Fix asset import
AdryienH Apr 28, 2026
7226177
Add key to fileList
AdryienH Apr 28, 2026
cc6ff8a
Merge branch 'develop' into feature/12088-new-file-upload
AdryienH Apr 29, 2026
99d338f
Merge branch 'develop' into feature/12088-new-file-upload
AdryienH Apr 29, 2026
8dd937d
Add config to change validation settings
AdryienH May 6, 2026
6e890de
Merge branch 'develop' into feature/12088-new-file-upload
AdryienH May 6, 2026
4795f7c
Fix a11y text read out
AdryienH May 6, 2026
822949f
Merge branch 'develop' into feature/12088-new-file-upload
AdryienH May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions VAMobile/src/api/claimsAndAppeals/uploadFileToClaim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const uploadFileToClaim = ({ claimID, documentType, request, files }: UploadFile
export const useUploadFileToClaim = (
claimID: string,
request: ClaimEventData | undefined,
files: Array<Asset> | Array<DocumentPickerResponse> | undefined,
files: Array<Asset | DocumentPickerResponse> | undefined,
) => {
const queryClient = useQueryClient()
return useMutation({
Expand Down Expand Up @@ -136,7 +136,7 @@ export const useUploadFileToClaim = (
}

const createFileRequestDocumentsArray = (
files: Array<Asset> | Array<DocumentPickerResponse>,
files: Array<Asset | DocumentPickerResponse>,
trackedItemId: number | undefined,
documentType: string,
uploadDate: string,
Expand Down
2 changes: 1 addition & 1 deletion VAMobile/src/api/types/ClaimsAndAppealsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,5 +731,5 @@ export type UploadFileToClaimParamaters = {
claimID: string
documentType: string
request: ClaimEventData | undefined
files: Array<Asset> | Array<DocumentPickerResponse>
files: Array<Asset | DocumentPickerResponse>
}
111 changes: 111 additions & 0 deletions VAMobile/src/components/FileUpload/FilePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Image, Pressable } from 'react-native'
import { Asset } from 'react-native-image-picker'

import { Icon } from '@department-of-veterans-affairs/mobile-component-library'
import styled from 'styled-components'

import { Box, TextView } from 'components/index'
import { NAMESPACE } from 'constants/namespaces'
import { DocumentPickerResponse } from 'screens/BenefitsScreen/BenefitsStackScreens'
import { bytesToFinalSizeDisplay, bytesToFinalSizeDisplayA11y } from 'utils/common'
import { useShowActionSheet, useTheme } from 'utils/hooks'
import { themeFn } from 'utils/theme'

export type FilePreviewProps = {
/** Response object from file selection with file name and size information */
file: Asset | DocumentPickerResponse
/** Callback when the delete button is pressed */
onDelete: () => void
/** Accessibility label for the preview element */
a11yLabel: string
}

type StyledImageProps = {
/** prop to set image width */
width: number
/** prop to set image height */
height: number
/** Hardcoded radius of 5 due to design plan */
borderRadius: number
}

const StyledImage = styled(Image)<StyledImageProps>`
width: ${themeFn<StyledImageProps>((_theme, props) => props.width)}px;
height: ${themeFn<StyledImageProps>((_theme, props) => props.height)}px;
border-radius: ${themeFn<StyledImageProps>((_theme, props) => props.borderRadius)}px;
`

const FilePreview: FC<FilePreviewProps> = ({ file, onDelete, a11yLabel }) => {
const { t } = useTranslation(NAMESPACE.COMMON)
const theme = useTheme()
const confirmAlert = useShowActionSheet()

const isImage = !('fileCopyUri' in file)
const fileName = isImage ? file.fileName : (file as DocumentPickerResponse).name
const fileSize = isImage ? file.fileSize : (file as DocumentPickerResponse).size
const imageUri = isImage ? file.uri : undefined
const fileSizeDisplayText = fileSize ? bytesToFinalSizeDisplay(fileSize, t, false) : ''
const fileSizeA11yText = fileSize ? bytesToFinalSizeDisplayA11y(fileSize, t, false) : ''
const accessibilityLabel = a11yLabel?.concat(fileName || '', fileSizeA11yText)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessibilityLabel is built via concat(fileName, fileSizeA11yText) without any spacing/punctuation, which will be read as a single run-on string by screen readers.

Suggested fix: build the label with explicit separators (e.g., commas/spaces) and consider including the file type as well for clarity.

Suggested change
const accessibilityLabel = a11yLabel?.concat(fileName || '', fileSizeA11yText)
const accessibilityLabel = [a11yLabel, fileName, fileSizeA11yText].filter(Boolean).join(', ')

Copilot uses AI. Check for mistakes.

const onPress = (): void => {
const options = [t('remove'), t('keep')]

confirmAlert(
{
options,
title: t('file.removeFile'),
destructiveButtonIndex: 0,
},
(buttonIndex) => {
switch (buttonIndex) {
case 0:
onDelete()
break
case 1:
break
}
},
)
}

return (
<Pressable
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
accessibilityHint={t('fileUpload.delete.a11yHint')}>
<Box flexDirection="row" gap={10}>
{imageUri ? (
<StyledImage
source={{ uri: imageUri }}
width={theme.dimensions.imagePreviewWidth}
height={theme.dimensions.imagePreviewWidth}
borderRadius={5}
/>
) : (
<Box width={theme.dimensions.imagePreviewWidth}>
<Icon
name={'DescriptionOutlined'}
fill={theme.colors.text.primary}
width={theme.dimensions.filePreviewWidth}
height={theme.dimensions.filePreviewWidth}
preventScaling={true}
/>
</Box>
)}
<Box flex={1}>
<TextView variant="HelperTextBold">{fileName}</TextView>
<TextView variant="HelperText">{fileSizeDisplayText}</TextView>
</Box>
<Box>
<Icon name={'Delete'} fill={theme.colors.icon.error} width={24} height={25} preventScaling={true} />
</Box>
</Box>
</Pressable>
)
}

export default FilePreview
146 changes: 146 additions & 0 deletions VAMobile/src/components/FileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Asset } from 'react-native-image-picker'

import { Button, ButtonVariants, Icon } from '@department-of-veterans-affairs/mobile-component-library'

import FilePreview from 'components/FileUpload/FilePreview'
import { Box, BoxProps, TextView } from 'components/index'
import { NAMESPACE } from 'constants/namespaces'
import { DocumentPickerResponse } from 'screens/BenefitsScreen/BenefitsStackScreens'
import theme from 'styles/themes/standardTheme'
import { bytesToFinalSizeDisplay, bytesToFinalSizeDisplayA11y } from 'utils/common'
import { getFileUris, getImageBase64s, getTotalBytesUsedByFiles } from 'utils/fileUpload'
import { useShowActionSheet } from 'utils/hooks'
import { onAddMultipleFileAttachments } from 'utils/secureMessaging'

export type FileUploadProps = {
/** i18n key for the text label next to the file upload field */
labelKey?: string
/** List of Assets or DocumentPickerResponse objects to display */
fileList: Array<Asset | DocumentPickerResponse>
/** Callback after a successful file selection is made */
onFileSelection: (response: Array<Asset | DocumentPickerResponse>) => void
/** Optional callback when a file's delete button is pressed */
onDeleteFile?: (idx: number) => void
/** Callback to return an error message after an invalid file selection */
setError: (value: string) => void
/** Callback to return an a11y-friendly error message after an invalid file selection */
setErrorA11y: (value: string) => void
}

const FileUpload: FC<FileUploadProps> = ({
labelKey,
fileList,
onFileSelection,
onDeleteFile,
setError,
setErrorA11y,
}) => {
const showActionSheetWithOptions = useShowActionSheet()
const { t } = useTranslation(NAMESPACE.COMMON)

const totalBytesUsed = useMemo(() => getTotalBytesUsedByFiles(fileList), [fileList])

const containerProps: BoxProps = {
borderColor: 'photoAdd',
borderWidth: 2,
borderStyle: 'dashed',
borderRadius: 5,
gap: 16,
px: 20,
py: 16,
}

const onChooseAFile = (): void => {
// logAnalyticsEvent(Events.vama_sm_attach('Select a file'))
onAddMultipleFileAttachments(
t,
showActionSheetWithOptions,
setError,
setErrorA11y,
onFileSelection,
getTotalBytesUsedByFiles(fileList),
getFileUris(fileList),
getImageBase64s(fileList),
)
Comment thread
AdryienH marked this conversation as resolved.
}

const getFileList = () => {
if (fileList.length < 1) {
return (
<Box alignItems="center">
<Icon name={'UploadFile'} width={56} height={56} preventScaling={true} />
</Box>
)
}

return (
<>
{fileList.map((file, idx) => {
return (
<FilePreview
file={file}
onDelete={() => onDeleteFile?.(idx)}
a11yLabel={t('fileUpload.ofTotalFiles', {
Comment on lines +79 to +85
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rendered list of FilePreview components is missing a stable key prop. This will trigger React warnings and can cause inefficient re-renders or incorrect UI updates when deleting/reordering.

Suggested fix: add a key using a stable identifier (preferably URI/name + size, or fall back to index only if there’s no stable value).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

fileNum: idx + 1,
count: fileList.length,
})}
/>
)
})}
<Box
flexDirection="row"
borderTopColor="divider"
justifyContent="space-between"
borderTopWidth={2}
pt={8}
px={20}>
<TextView variant="HelperText" color={'fileInfo'}>
{t('fileUpload.ofTenFiles', { numOfFiles: fileList?.length })}
</TextView>
{/*eslint-disable-next-line react-native-a11y/has-accessibility-hint*/}
<TextView
variant="HelperText"
color={'fileInfo'}
accessibilityLabel={t('fileUpload.ofFiftyMB.a11y', {
sizeOfPhotos: bytesToFinalSizeDisplayA11y(totalBytesUsed ? totalBytesUsed : 0, t, false),
})}>
{t('fileUpload.ofFiftyMB', {
sizeOfPhotos: bytesToFinalSizeDisplay(totalBytesUsed ? totalBytesUsed : 0, t, false),
})}
</TextView>
Comment on lines +99 to +112
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI text implies limits of “10 files” and “50MB”, but Secure Messaging enforces 4 attachments (theme.dimensions.maxNumMessageAttachments) and 10MB total (MAX_TOTAL_MESSAGE_ATTACHMENTS_SIZE_IN_BYTES). Using hard-coded limits here will mislead users in Secure Messaging.

Suggested fix: make max-count and max-total-size display values configurable via props (and ensure they match the validation logic used for the current flow).

Copilot uses AI. Check for mistakes.
</Box>
</>
)
}

return (
<>
{labelKey && (
<Box display="flex" flexDirection="row" flexWrap="wrap" mb={theme.dimensions.smallMarginBetween}>
<TextView>{t(labelKey)}</TextView>
</Box>
)}
<Box {...containerProps}>
<Box>
{/*eslint-disable-next-line react-native-a11y/has-accessibility-hint*/}
<TextView
variant="HelperText"
color={'placeholder'}
accessibilityLabel={t('fileUpload.acceptedFileSpecs.a11y')}>
{t('fileUpload.acceptedFileSpecs')}
</TextView>
</Box>
{getFileList()}
<Button
label={fileList.length < 1 ? t('fileUpload.chooseAFile') : t('fileUpload.attachFiles')}
onPress={onChooseAFile}
buttonType={ButtonVariants.Secondary}
/>
</Box>
</>
)
}

export default FileUpload
Comment thread
AdryienH marked this conversation as resolved.
5 changes: 5 additions & 0 deletions VAMobile/src/components/FormWrapper/FormWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import { RadioButton, RadioButtonProps } from '@department-of-veterans-affairs/mobile-component-library'
import _ from 'underscore'

import FileUpload, { FileUploadProps } from 'components/FileUpload/FileUpload'
import {
Box,
ComboBoxInput,
Expand All @@ -26,6 +27,7 @@ export enum FieldType {
TextInput = 'TextInput',
Radios = 'Radios',
FormAttachmentsList = 'FormAttachmentsList',
FileUploadList = 'FileUploadList',
ComboBox = 'Combobox',
}

Expand All @@ -52,6 +54,7 @@ export type FormFieldType<T> = {
| VAModalPickerProps
| RadioGroupProps<T>
| FormAttachmentsProps
| FileUploadProps
| ComboBoxInputProps
/** optional error message to display if the field is required and it hasn't been filled */
fieldErrorMessage?: string
Expand Down Expand Up @@ -272,6 +275,8 @@ const FormWrapper = <T,>({
return <RadioButton {...(fieldProps as RadioButtonProps)} error={errors[index]} />
case FieldType.FormAttachmentsList:
return <FormAttachments {...(fieldProps as FormAttachmentsProps)} />
case FieldType.FileUploadList:
return <FileUpload {...(fieldProps as FileUploadProps)} />
case FieldType.ComboBox:
return (
<ComboBoxInput
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import { Asset } from 'react-native-image-picker'
import { ImagePickerResponse } from 'react-native-image-picker/src/types'

import { StackScreenProps, createStackNavigator } from '@react-navigation/stack'
Expand All @@ -18,6 +19,7 @@ import SelectFile from 'screens/BenefitsScreen/ClaimsScreen/ClaimDetailsScreen/C
import UploadFile from 'screens/BenefitsScreen/ClaimsScreen/ClaimDetailsScreen/ClaimStatus/ClaimFileUpload/SelectFile/UploadFile/UploadFile'
import TakePhotos from 'screens/BenefitsScreen/ClaimsScreen/ClaimDetailsScreen/ClaimStatus/ClaimFileUpload/TakePhotos/TakePhotos'
import UploadOrAddPhotos from 'screens/BenefitsScreen/ClaimsScreen/ClaimDetailsScreen/ClaimStatus/ClaimFileUpload/TakePhotos/UploadOrAddPhotos/UploadOrAddPhotos'
import UploadFiles from 'screens/BenefitsScreen/ClaimsScreen/ClaimDetailsScreen/ClaimStatus/ClaimFileUpload/UploadFiles/UploadFiles'

export type FileRequestStackParams = {
AskForClaimDecision: {
Expand Down Expand Up @@ -72,6 +74,13 @@ export type FileRequestStackParams = {
request?: ClaimEventData
provider?: string
}
UploadFiles: {
claimID: string
firstFileResponse: Array<Asset | DocumentPickerResponse>
firstFileError: { error: string; errorA11y: string }
request?: ClaimEventData
provider?: string
}
}
const FileRequestStack = createStackNavigator<FileRequestStackParams>()

Expand All @@ -93,6 +102,7 @@ export const fileRequestSharedScreens = [
<FileRequestStack.Screen name="TakePhotos" component={TakePhotos} key="TakePhotos" />,
<FileRequestStack.Screen name="UploadFile" component={UploadFile} key="UploadFile" />,
<FileRequestStack.Screen name="UploadOrAddPhotos" component={UploadOrAddPhotos} key="UploadOrAddPhotos" />,
<FileRequestStack.Screen name="UploadFiles" component={UploadFiles} key="UploadFiles" />,
]

type FileRequestSubtaskProps = StackScreenProps<RootNavStackParamList, 'FileRequestSubtask'>
Expand All @@ -102,7 +112,11 @@ function FileRequestSubtask({ route }: FileRequestSubtaskProps) {

return (
<MultiStepSubtask<FileRequestStackParams> stackNavigator={FileRequestStack}>
<FileRequestStack.Screen name="FileRequest" component={FileRequest} initialParams={{ claimID, claim, provider }} />
<FileRequestStack.Screen
name="FileRequest"
component={FileRequest}
initialParams={{ claimID, claim, provider }}
/>
{fileRequestSharedScreens}
</MultiStepSubtask>
)
Expand Down
Loading
Loading