From eba5fb3c3dfedae70b80e72e4981b05a4327be50 Mon Sep 17 00:00:00 2001
From: BrtqKr <bartlomiej.krason@swmansion.com>
Date: Tue, 23 Jul 2024 15:51:32 +0200
Subject: [PATCH 1/7] fix conversions wip

---
 package-lock.json                             |  7 ++++
 package.json                                  |  1 +
 src/CONST.ts                                  |  1 +
 .../AttachmentPicker/index.native.tsx         | 33 ++++++++++++++++++-
 4 files changed, 41 insertions(+), 1 deletion(-)

diff --git a/package-lock.json b/package-lock.json
index df337fdd663d..2c6ed800e5a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -173,6 +173,7 @@
         "@testing-library/jest-native": "5.4.1",
         "@testing-library/react-native": "11.5.1",
         "@trivago/prettier-plugin-sort-imports": "^4.2.0",
+        "@types/base-64": "^1.0.2",
         "@types/canvas-size": "^1.2.2",
         "@types/concurrently": "^7.0.0",
         "@types/jest": "^29.5.2",
@@ -17238,6 +17239,12 @@
         "@babel/types": "^7.3.0"
       }
     },
+    "node_modules/@types/base-64": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.2.tgz",
+      "integrity": "sha512-uPgKMmM9fmn7I+Zi6YBqctOye4SlJsHKcisjHIMWpb2YKZRc36GpKyNuQ03JcT+oNXg1m7Uv4wU94EVltn8/cw==",
+      "dev": true
+    },
     "node_modules/@types/body-parser": {
       "version": "1.19.2",
       "dev": true,
diff --git a/package.json b/package.json
index 29030d148853..8439481adb8c 100644
--- a/package.json
+++ b/package.json
@@ -228,6 +228,7 @@
     "@testing-library/jest-native": "5.4.1",
     "@testing-library/react-native": "11.5.1",
     "@trivago/prettier-plugin-sort-imports": "^4.2.0",
+    "@types/base-64": "^1.0.2",
     "@types/canvas-size": "^1.2.2",
     "@types/concurrently": "^7.0.0",
     "@types/jest": "^29.5.2",
diff --git a/src/CONST.ts b/src/CONST.ts
index a1193fd8bf32..b9869f641e21 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -74,6 +74,7 @@ const onboardingChoices = {
 type OnboardingPurposeType = ValueOf<typeof onboardingChoices>;
 
 const CONST = {
+    HEIC_SIGNATURE: '000000186674797068656963',
     RECENT_WAYPOINTS_NUMBER: 20,
     DEFAULT_DB_NAME: 'OnyxDB',
     DEFAULT_TABLE_NAME: 'keyvaluepairs',
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index 7d4fbd97f4f7..c56f0c9949c1 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -1,9 +1,13 @@
+/* eslint-disable @lwc/lwc/no-async-await */
+import {decode} from 'base-64';
 import {Str} from 'expensify-common';
+import {manipulateAsync, SaveFormat} from 'expo-image-manipulator';
 import React, {useCallback, useMemo, useRef, useState} from 'react';
 import {Alert, View} from 'react-native';
 import RNFetchBlob from 'react-native-blob-util';
 import RNDocumentPicker from 'react-native-document-picker';
 import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker';
+import RNFS from 'react-native-fs';
 import {launchImageLibrary} from 'react-native-image-picker';
 import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker';
 import ImageSize from 'react-native-image-size';
@@ -46,6 +50,7 @@ const imagePickerOptions = {
     saveToPhotos: false,
     selectionLimit: 1,
     includeExtra: false,
+    assetRepresentationMode: 'current',
 };
 
 /**
@@ -140,7 +145,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s
     const showImagePicker = useCallback(
         (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise<ImagePickerResponse>): Promise<Asset[] | void> =>
             new Promise((resolve, reject) => {
-                imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => {
+                imagePickerFunc(getImagePickerOptions(type), async (response: ImagePickerResponse) => {
                     if (response.didCancel) {
                         // When the user cancelled resolve with no attachment
                         return resolve();
@@ -158,6 +163,32 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s
                         return reject(new Error(`Error during attachment selection: ${response.errorMessage}`));
                     }
 
+                    const targetAsset = response.assets?.[0];
+
+                    console.log('TARGET ASSET ', targetAsset);
+                    const fileContent = await RNFS.read(targetAsset?.uri, 12, 0, 'base64');
+                    const hexSignature = Array.from(decode(fileContent))
+                        .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0'))
+                        .slice(0, 32)
+                        .join('')
+                        .toUpperCase();
+
+                    const isHEIC = hexSignature.startsWith(CONST.HEIC_SIGNATURE);
+
+                    // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature
+                    if (isHEIC) {
+                        const manipResult = await manipulateAsync(targetAsset?.uri, [], {format: SaveFormat.JPEG});
+
+                        const convertedAsset = {
+                            uri: manipResult.uri,
+                            type: 'image/jpeg',
+                            width: manipResult.width,
+                            height: manipResult.height,
+                        };
+
+                        return resolve([convertedAsset]);
+                    }
+
                     return resolve(response.assets);
                 });
             }),

From 7bf0fe98952c9ee74c417b54bffbed61bee0624c Mon Sep 17 00:00:00 2001
From: BrtqKr <bartlomiej.krason@swmansion.com>
Date: Tue, 23 Jul 2024 16:59:06 +0200
Subject: [PATCH 2/7] cleanups, fix for video, type changes

---
 .../AttachmentPicker/index.native.tsx         | 45 ++++++++++---------
 1 file changed, 25 insertions(+), 20 deletions(-)

diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index c56f0c9949c1..2314abc87ab5 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -9,7 +9,7 @@ import RNDocumentPicker from 'react-native-document-picker';
 import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker';
 import RNFS from 'react-native-fs';
 import {launchImageLibrary} from 'react-native-image-picker';
-import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker';
+import type {Asset, Callback, CameraOptions, ImageLibraryOptions, ImagePickerResponse} from 'react-native-image-picker';
 import ImageSize from 'react-native-image-size';
 import type {FileObject, ImagePickerResponse as FileResponse} from '@components/AttachmentModal';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -45,7 +45,7 @@ type Item = {
  * See https://github.com/react-native-image-picker/react-native-image-picker/#options
  * for ImagePicker configuration options
  */
-const imagePickerOptions = {
+const imagePickerOptions: Partial<CameraOptions | ImageLibraryOptions> = {
     includeBase64: false,
     saveToPhotos: false,
     selectionLimit: 1,
@@ -165,28 +165,33 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s
 
                     const targetAsset = response.assets?.[0];
 
-                    console.log('TARGET ASSET ', targetAsset);
-                    const fileContent = await RNFS.read(targetAsset?.uri, 12, 0, 'base64');
-                    const hexSignature = Array.from(decode(fileContent))
-                        .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0'))
-                        .slice(0, 32)
-                        .join('')
-                        .toUpperCase();
+                    if (!targetAsset?.uri) {
+                        return resolve();
+                    }
 
-                    const isHEIC = hexSignature.startsWith(CONST.HEIC_SIGNATURE);
+                    if (targetAsset?.type?.startsWith('image')) {
+                        const fileContent = await RNFS.read(targetAsset.uri, 12, 0, 'base64');
+                        const hexSignature = Array.from(decode(fileContent))
+                            .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0'))
+                            .slice(0, 32)
+                            .join('')
+                            .toUpperCase();
 
-                    // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature
-                    if (isHEIC) {
-                        const manipResult = await manipulateAsync(targetAsset?.uri, [], {format: SaveFormat.JPEG});
+                        const isHEIC = hexSignature.startsWith(CONST.HEIC_SIGNATURE);
 
-                        const convertedAsset = {
-                            uri: manipResult.uri,
-                            type: 'image/jpeg',
-                            width: manipResult.width,
-                            height: manipResult.height,
-                        };
+                        // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature
+                        if (isHEIC) {
+                            const manipResult = await manipulateAsync(targetAsset.uri, [], {format: SaveFormat.JPEG});
 
-                        return resolve([convertedAsset]);
+                            const convertedAsset = {
+                                uri: manipResult.uri,
+                                type: 'image/jpeg',
+                                width: manipResult.width,
+                                height: manipResult.height,
+                            };
+
+                            return resolve([convertedAsset]);
+                        }
                     }
 
                     return resolve(response.assets);

From ccfcbaa6969fb9f4f48ac7149f5d99348dbdf768 Mon Sep 17 00:00:00 2001
From: BrtqKr <bartlomiej.krason@swmansion.com>
Date: Fri, 26 Jul 2024 16:37:55 +0200
Subject: [PATCH 3/7] generalize function, migrate from async await to then

---
 .../AttachmentPicker/index.native.tsx         | 56 +++++++++----------
 src/libs/fileDownload/FileUtils.ts            | 21 +++++++
 2 files changed, 47 insertions(+), 30 deletions(-)

diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index 2314abc87ab5..e60e1b5497bb 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable @lwc/lwc/no-async-await */
-import {decode} from 'base-64';
 import {Str} from 'expensify-common';
 import {manipulateAsync, SaveFormat} from 'expo-image-manipulator';
 import React, {useCallback, useMemo, useRef, useState} from 'react';
@@ -7,7 +5,6 @@ import {Alert, View} from 'react-native';
 import RNFetchBlob from 'react-native-blob-util';
 import RNDocumentPicker from 'react-native-document-picker';
 import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker';
-import RNFS from 'react-native-fs';
 import {launchImageLibrary} from 'react-native-image-picker';
 import type {Asset, Callback, CameraOptions, ImageLibraryOptions, ImagePickerResponse} from 'react-native-image-picker';
 import ImageSize from 'react-native-image-size';
@@ -145,7 +142,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s
     const showImagePicker = useCallback(
         (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise<ImagePickerResponse>): Promise<Asset[] | void> =>
             new Promise((resolve, reject) => {
-                imagePickerFunc(getImagePickerOptions(type), async (response: ImagePickerResponse) => {
+                imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => {
                     if (response.didCancel) {
                         // When the user cancelled resolve with no attachment
                         return resolve();
@@ -164,42 +161,41 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s
                     }
 
                     const targetAsset = response.assets?.[0];
+                    const targetAssetUri = targetAsset?.uri;
 
-                    if (!targetAsset?.uri) {
+                    if (!targetAssetUri) {
                         return resolve();
                     }
 
                     if (targetAsset?.type?.startsWith('image')) {
-                        const fileContent = await RNFS.read(targetAsset.uri, 12, 0, 'base64');
-                        const hexSignature = Array.from(decode(fileContent))
-                            .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0'))
-                            .slice(0, 32)
-                            .join('')
-                            .toUpperCase();
-
-                        const isHEIC = hexSignature.startsWith(CONST.HEIC_SIGNATURE);
-
-                        // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature
-                        if (isHEIC) {
-                            const manipResult = await manipulateAsync(targetAsset.uri, [], {format: SaveFormat.JPEG});
-
-                            const convertedAsset = {
-                                uri: manipResult.uri,
-                                type: 'image/jpeg',
-                                width: manipResult.width,
-                                height: manipResult.height,
-                            };
-
-                            return resolve([convertedAsset]);
-                        }
+                        FileUtils.verifyFileFormat({fileUri: targetAssetUri, formatSignature: CONST.HEIC_SIGNATURE})
+                            .then((isHEIC) => {
+                                // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature
+                                if (isHEIC && targetAssetUri) {
+                                    manipulateAsync(targetAssetUri, [], {format: SaveFormat.JPEG})
+                                        .then((manipResult) => {
+                                            const convertedAsset = {
+                                                uri: manipResult.uri,
+                                                type: 'image/jpeg',
+                                                width: manipResult.width,
+                                                height: manipResult.height,
+                                            };
+
+                                            return resolve([convertedAsset]);
+                                        })
+                                        .catch((err) => reject(err));
+                                } else {
+                                    return resolve(response.assets);
+                                }
+                            })
+                            .catch((err) => reject(err));
+                    } else {
+                        return resolve(response.assets);
                     }
-
-                    return resolve(response.assets);
                 });
             }),
         [showGeneralAlert, type],
     );
-
     /**
      * Launch the DocumentPicker. Results are in the same format as ImagePicker
      *
diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index 7422d8bd8d8b..0e0476519b1a 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -1,5 +1,7 @@
+import {decode} from 'base-64';
 import {Str} from 'expensify-common';
 import {Alert, Linking, Platform} from 'react-native';
+import RNFS from 'react-native-fs';
 import ImageSize from 'react-native-image-size';
 import type {FileObject} from '@components/AttachmentModal';
 import DateUtils from '@libs/DateUtils';
@@ -255,6 +257,24 @@ function validateImageForCorruption(file: FileObject): Promise<void> {
     });
 }
 
+function verifyFileFormat({fileUri, formatSignature}: {fileUri: string; formatSignature: string}) {
+    return RNFS.read(fileUri, 12, 0, 'base64')
+        .then((fileContent) => {
+            const hexSignature = Array.from(decode(fileContent))
+                .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0'))
+                .slice(0, 32)
+                .join('')
+                .toUpperCase();
+
+            const isHEIC = hexSignature.startsWith(formatSignature);
+            return isHEIC;
+        })
+        .catch((error: Error) => {
+            Log.hmmm('Failed to verify the file format: ', error);
+            return null;
+        });
+}
+
 function isLocalFile(receiptUri?: string | number): boolean {
     if (!receiptUri) {
         return false;
@@ -302,4 +322,5 @@ export {
     isImage,
     getFileResolution,
     isHighResolutionImage,
+    verifyFileFormat,
 };

From 88cda03e1ba33b52d45fa7978d9ed196957ac9cf Mon Sep 17 00:00:00 2001
From: BrtqKr <bartlomiej.krason@swmansion.com>
Date: Tue, 30 Jul 2024 10:35:08 +0200
Subject: [PATCH 4/7] rename variable

---
 src/libs/fileDownload/FileUtils.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index 0e0476519b1a..345115b9509f 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -266,8 +266,8 @@ function verifyFileFormat({fileUri, formatSignature}: {fileUri: string; formatSi
                 .join('')
                 .toUpperCase();
 
-            const isHEIC = hexSignature.startsWith(formatSignature);
-            return isHEIC;
+            const isOfProvidedFormat = hexSignature.startsWith(formatSignature);
+            return isOfProvidedFormat;
         })
         .catch((error: Error) => {
             Log.hmmm('Failed to verify the file format: ', error);

From ab63c3e430820240cdbd67f5f8689ca1a7f022b0 Mon Sep 17 00:00:00 2001
From: BrtqKr <bartlomiej.krason@swmansion.com>
Date: Fri, 2 Aug 2024 16:47:35 +0200
Subject: [PATCH 5/7] add multi image format, move implementation to fetch to
 make it compatible with other platforms

---
 src/CONST.ts                                  |  8 ++++-
 .../AttachmentPicker/index.native.tsx         |  2 +-
 src/libs/fileDownload/FileUtils.ts            | 29 +++++++++----------
 3 files changed, 21 insertions(+), 18 deletions(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index b9869f641e21..0ae637c2052a 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -73,8 +73,14 @@ const onboardingChoices = {
 
 type OnboardingPurposeType = ValueOf<typeof onboardingChoices>;
 
+// 667479706D69663100000000
 const CONST = {
-    HEIC_SIGNATURE: '000000186674797068656963',
+    HEIC_SIGNATURES: [
+        '6674797068656963', // 'ftypheic' - Indicates standard HEIC file
+        '6674797068656978', // 'ftypheix' - Indicates a variation of HEIC
+        '6674797068657631', // 'ftyphevc' - Typically for HEVC encoded media (common in HEIF)
+        '667479706d696631', // 'ftypmif1' - Multi-Image Format part of HEIF, broader usage
+    ],
     RECENT_WAYPOINTS_NUMBER: 20,
     DEFAULT_DB_NAME: 'OnyxDB',
     DEFAULT_TABLE_NAME: 'keyvaluepairs',
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index e60e1b5497bb..0a14d18a2324 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -168,7 +168,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s
                     }
 
                     if (targetAsset?.type?.startsWith('image')) {
-                        FileUtils.verifyFileFormat({fileUri: targetAssetUri, formatSignature: CONST.HEIC_SIGNATURE})
+                        FileUtils.verifyFileFormat({fileUri: targetAssetUri, formatSignatures: CONST.HEIC_SIGNATURES})
                             .then((isHEIC) => {
                                 // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature
                                 if (isHEIC && targetAssetUri) {
diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index 345115b9509f..d3ed4a0a3b96 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -1,7 +1,5 @@
-import {decode} from 'base-64';
 import {Str} from 'expensify-common';
 import {Alert, Linking, Platform} from 'react-native';
-import RNFS from 'react-native-fs';
 import ImageSize from 'react-native-image-size';
 import type {FileObject} from '@components/AttachmentModal';
 import DateUtils from '@libs/DateUtils';
@@ -257,21 +255,20 @@ function validateImageForCorruption(file: FileObject): Promise<void> {
     });
 }
 
-function verifyFileFormat({fileUri, formatSignature}: {fileUri: string; formatSignature: string}) {
-    return RNFS.read(fileUri, 12, 0, 'base64')
-        .then((fileContent) => {
-            const hexSignature = Array.from(decode(fileContent))
-                .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0'))
-                .slice(0, 32)
-                .join('')
-                .toUpperCase();
-
-            const isOfProvidedFormat = hexSignature.startsWith(formatSignature);
-            return isOfProvidedFormat;
+function verifyFileFormat({fileUri, formatSignatures}: {fileUri: string; formatSignatures: readonly string[]}) {
+    return fetch(fileUri)
+        .then((file) => file.arrayBuffer())
+        .then((arrayBuffer) => {
+            const uintArray = new Uint8Array(arrayBuffer, 4, 12);
+
+            const hexString = Array.from(uintArray)
+                .map((b) => b.toString(16).padStart(2, '0'))
+                .join('');
+
+            return hexString;
         })
-        .catch((error: Error) => {
-            Log.hmmm('Failed to verify the file format: ', error);
-            return null;
+        .then((hexSignature) => {
+            return formatSignatures.some((signature) => hexSignature.startsWith(signature));
         });
 }
 

From 591320e67c69249db998a095d4f343f15a55a58b Mon Sep 17 00:00:00 2001
From: BrtqKr <bartlomiej.krason@swmansion.com>
Date: Fri, 2 Aug 2024 16:57:19 +0200
Subject: [PATCH 6/7] cleanup

---
 src/CONST.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index 0ae637c2052a..8c7f9c2e4faf 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -73,7 +73,6 @@ const onboardingChoices = {
 
 type OnboardingPurposeType = ValueOf<typeof onboardingChoices>;
 
-// 667479706D69663100000000
 const CONST = {
     HEIC_SIGNATURES: [
         '6674797068656963', // 'ftypheic' - Indicates standard HEIC file

From b8cef07402d3a40c63f36815e0a8416347c1af3b Mon Sep 17 00:00:00 2001
From: BrtqKr <bartlomiej.krason@swmansion.com>
Date: Tue, 6 Aug 2024 10:20:46 +0200
Subject: [PATCH 7/7] add description for verifyFileFormat

---
 src/libs/fileDownload/FileUtils.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index 9fe1a32c2e61..c8520f3c66cd 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -256,6 +256,7 @@ function validateImageForCorruption(file: FileObject): Promise<{width: number; h
     });
 }
 
+/** Verify file format based on the magic bytes of the file - some formats might be identified by multiple signatures */
 function verifyFileFormat({fileUri, formatSignatures}: {fileUri: string; formatSignatures: readonly string[]}) {
     return fetch(fileUri)
         .then((file) => file.arrayBuffer())
