diff --git a/android/app/build.gradle b/android/app/build.gradle index 955b3b010..08e41cb74 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -88,8 +88,8 @@ android { applicationId 'com.internxt.cloud' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 112 - versionName "1.8.3" + versionCode 115 + versionName "1.8.4" buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString()) missingDimensionStrategy "react-native-capture-protection", "fullMediaCapture" diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 75bc0256a..653b696b1 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2,6 +2,6 @@ Internxt cover false - 1.8.3 + 1.8.4 automatic \ No newline at end of file diff --git a/app.config.ts b/app.config.ts index 395babdaf..77523f329 100644 --- a/app.config.ts +++ b/app.config.ts @@ -44,6 +44,7 @@ const appConfig: ExpoConfig & { extra: AppEnv & { NODE_ENV: AppStage; RELEASE_ID associatedDomains: ['webcredentials:www.internxt.com'], buildNumber: env[stage].IOS_BUILD_NUMBER.toString(), infoPlist: { + UIDesignRequiresCompatibility: true, NSFaceIDUsageDescription: 'Protect the app access to secure the available files', NSCameraUsageDescription: 'Allow $(PRODUCT_NAME) to access your camera to upload a newly captured photo to the storage service', diff --git a/ios/Internxt.xcodeproj/project.pbxproj b/ios/Internxt.xcodeproj/project.pbxproj index b3a347cd3..8f48d9133 100644 --- a/ios/Internxt.xcodeproj/project.pbxproj +++ b/ios/Internxt.xcodeproj/project.pbxproj @@ -7,28 +7,28 @@ objects = { /* Begin PBXBuildFile section */ + 0A3640DCB02F44AABAD84072 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5CA2D57E4C403BBC990224 /* noop-file.swift */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; 96905EF65AED1B983A6B3ABC /* libPods-Internxt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Internxt.a */; }; - A2F1FB5E224D420F8533FE07 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F9C1695EAA4E749324272B /* noop-file.swift */; }; B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 0722A93BC221445392598F00 /* Internxt-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Internxt-Bridging-Header.h"; path = "Internxt/Internxt-Bridging-Header.h"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* Internxt.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Internxt.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Internxt/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Internxt/AppDelegate.mm; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Internxt/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Internxt/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Internxt/main.m; sourceTree = ""; }; - 334F4DDFA16147FD84D2D5F4 /* Internxt-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Internxt-Bridging-Header.h"; path = "Internxt/Internxt-Bridging-Header.h"; sourceTree = ""; }; - 36F9C1695EAA4E749324272B /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Internxt/noop-file.swift"; sourceTree = ""; }; 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Internxt.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Internxt.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 6C2E3173556A471DD304B334 /* Pods-Internxt.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Internxt.debug.xcconfig"; path = "Target Support Files/Pods-Internxt/Pods-Internxt.debug.xcconfig"; sourceTree = ""; }; 7A4D352CD337FB3A3BF06240 /* Pods-Internxt.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Internxt.release.xcconfig"; path = "Target Support Files/Pods-Internxt/Pods-Internxt.release.xcconfig"; sourceTree = ""; }; + 8B5CA2D57E4C403BBC990224 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Internxt/noop-file.swift"; sourceTree = ""; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Internxt/SplashScreen.storyboard; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -57,8 +57,8 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB71A68108700A75B9A /* main.m */, AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, - 36F9C1695EAA4E749324272B /* noop-file.swift */, - 334F4DDFA16147FD84D2D5F4 /* Internxt-Bridging-Header.h */, + 8B5CA2D57E4C403BBC990224 /* noop-file.swift */, + 0722A93BC221445392598F00 /* Internxt-Bridging-Header.h */, ); name = Internxt; sourceTree = ""; @@ -144,7 +144,7 @@ buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Internxt" */; buildPhases = ( 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, - 3251DD91593037F32E56E32E /* [Expo] Configure project */, + 4C0344FC9074B35620BFC8BA /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, @@ -169,8 +169,8 @@ LastUpgradeCheck = 1130; TargetAttributes = { 13B07F861A680F5B00A75B9A = { - DevelopmentTeam = JR4S3SY396; LastSwiftMigration = 1250; + DevelopmentTeam = "JR4S3SY396"; ProvisioningStyle = Automatic; }; }; @@ -244,7 +244,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 3251DD91593037F32E56E32E /* [Expo] Configure project */ = { + 4C0344FC9074B35620BFC8BA /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -309,7 +309,7 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */, - A2F1FB5E224D420F8533FE07 /* noop-file.swift in Sources */, + 0A3640DCB02F44AABAD84072 /* noop-file.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -323,10 +323,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Internxt/Internxt.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JR4S3SY396; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", @@ -349,6 +346,9 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; + DEVELOPMENT_TEAM = "JR4S3SY396"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; }; name = Debug; }; @@ -359,10 +359,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Internxt/Internxt.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JR4S3SY396; INFOPLIST_FILE = Internxt/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -379,6 +376,9 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; + DEVELOPMENT_TEAM = "JR4S3SY396"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; }; name = Release; }; diff --git a/ios/Internxt/AppDelegate.mm b/ios/Internxt/AppDelegate.mm index d4eb869c3..37d415b57 100644 --- a/ios/Internxt/AppDelegate.mm +++ b/ios/Internxt/AppDelegate.mm @@ -24,7 +24,7 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge - (NSURL *)getBundleURL { #if DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif diff --git a/ios/Internxt/Info.plist b/ios/Internxt/Info.plist index 5df62bdf3..f839b372e 100644 --- a/ios/Internxt/Info.plist +++ b/ios/Internxt/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.8.3 + 1.8.4 CFBundleSignature ???? CFBundleURLTypes @@ -56,6 +56,8 @@ Allow $(PRODUCT_NAME) to access your photos to sync your device camera roll with our Photos cloud service RCTRootViewBackgroundColor 4294967295 + UIDesignRequiresCompatibility + UILaunchStoryboardName SplashScreen UIRequiredDeviceCapabilities diff --git a/ios/Internxt/Supporting/Expo.plist b/ios/Internxt/Supporting/Expo.plist index 84df42871..52b61e8d0 100644 --- a/ios/Internxt/Supporting/Expo.plist +++ b/ios/Internxt/Supporting/Expo.plist @@ -9,7 +9,7 @@ EXUpdatesLaunchWaitMs 0 EXUpdatesRuntimeVersion - 1.8.3 + 1.8.4 EXUpdatesURL https://u.expo.dev/680f4feb-6315-4a50-93ec-36dcd0b831d2 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4a0005f26..e03948518 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -89,17 +89,17 @@ PODS: - React-Core - jail-monkey (2.8.3): - React-Core - - libwebp (1.3.2): - - libwebp/demux (= 1.3.2) - - libwebp/mux (= 1.3.2) - - libwebp/sharpyuv (= 1.3.2) - - libwebp/webp (= 1.3.2) - - libwebp/demux (1.3.2): + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): - libwebp/webp - - libwebp/mux (1.3.2): + - libwebp/mux (1.5.0): - libwebp/demux - - libwebp/sharpyuv (1.3.2) - - libwebp/webp (1.3.2): + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): - libwebp/sharpyuv - RCT-Folly (2022.05.16.00): - boost @@ -1543,7 +1543,7 @@ SPEC CHECKSUMS: IDZSwiftCommonCrypto: aefd3487b88dc3d7a1de2553188c720ac3194940 internxt-mobile-sdk: 821a26ae1521019b968b5c2bc716ca5498e8a7d1 jail-monkey: 066e0af74e67cbf432fbb4d214b046ef6dccf910 - libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCTRequired: ca1d7414aba0b27efcfa2ccd37637edb1ab77d96 RCTTypeSafety: 678e344fb976ff98343ca61dc62e151f3a042292 diff --git a/package.json b/package.json index 3a1d162be..226b36604 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "drive-mobile", - "version": "v1.8.3", + "version": "v1.8.4", "private": true, "license": "GNU", "scripts": { "prepare": "husky install", + "postinstall": "patch-package", "start": "expo start --dev-client", "android": "expo run:android", "ios": "expo run:ios", @@ -166,8 +167,10 @@ "jest": "^29.3.1", "jest-expo": "~50.0.4", "metro-react-native-babel-transformer": "^0.77.0", + "patch-package": "^8.0.1", "path": "^0.12.7", "postcss": "^8.2.15", + "postinstall-postinstall": "^2.1.0", "prettier": "^2.3.2", "react-native-svg-transformer": "^0.14.3", "react-test-renderer": "^18.2.0", @@ -179,5 +182,8 @@ }, "engines": { "node": ">=16.16.0" + }, + "resolutions": { + "pbkdf2": "^3.1.3" } } diff --git a/patches/expo-device+5.9.4.patch b/patches/expo-device+5.9.4.patch new file mode 100644 index 000000000..c6485a6ce --- /dev/null +++ b/patches/expo-device+5.9.4.patch @@ -0,0 +1,21 @@ +diff --git a/node_modules/expo-device/ios/UIDevice.swift b/node_modules/expo-device/ios/UIDevice.swift +index 7d58ee8..9c2969e 100644 +--- a/node_modules/expo-device/ios/UIDevice.swift ++++ b/node_modules/expo-device/ios/UIDevice.swift +@@ -184,9 +184,13 @@ public extension UIDevice { + // swiftlint:enable closure_body_length + + // Credit: https://github.com/developerinsider/isJailBroken/blob/master/IsJailBroken/Extension/UIDevice%2BJailBroken.swift +- var isSimulator: Bool { +- return TARGET_OS_SIMULATOR != 0 +- } ++ var isSimulator: Bool { ++ #if targetEnvironment(simulator) ++ return true ++ #else ++ return false ++ #endif ++ } + + var isJailbroken: Bool { + if UIDevice.current.isSimulator { diff --git a/src/components/DriveNavigableItem/index.tsx b/src/components/DriveNavigableItem/index.tsx index f5b836bb0..53eebbb73 100644 --- a/src/components/DriveNavigableItem/index.tsx +++ b/src/components/DriveNavigableItem/index.tsx @@ -3,7 +3,7 @@ import prettysize from 'prettysize'; import React from 'react'; import { TouchableHighlight, View } from 'react-native'; import { useTailwind } from 'tailwind-rn'; -import { FolderIcon, getFileTypeIcon } from '../../helpers'; +import { checkIsFolder, FolderIcon, getFileTypeIcon } from '../../helpers'; import { getDisplayName } from '../../helpers/itemNames'; import useGetColor from '../../hooks/useColor'; import globalStyle from '../../styles/global'; @@ -13,7 +13,9 @@ import AppText from '../AppText'; const DriveNavigableItem: React.FC = ({ isLoading, disabled, ...props }) => { const tailwind = useTailwind(); const getColor = useGetColor(); - const isFolder = !props.data.fileId; + + const isFolder = checkIsFolder(props.data); + const iconSize = 40; const IconFile = getFileTypeIcon(props.data.type || ''); diff --git a/src/components/modals/DriveItemInfoModal/index.tsx b/src/components/modals/DriveItemInfoModal/index.tsx index 65ae7744a..04f7ab282 100644 --- a/src/components/modals/DriveItemInfoModal/index.tsx +++ b/src/components/modals/DriveItemInfoModal/index.tsx @@ -28,7 +28,7 @@ import AppText from 'src/components/AppText'; import { SLEEP_BECAUSE_MAYBE_BACKEND_IS_NOT_RETURNING_FRESHLY_MODIFIED_OR_CREATED_ITEMS_YET } from 'src/helpers/services'; import { useTailwind } from 'tailwind-rn'; import strings from '../../../../assets/lang/strings'; -import { FolderIcon, getFileTypeIcon } from '../../../helpers'; +import { checkIsFolder, FolderIcon, getFileSize, getFileTypeIcon, isEmptyFile } from '../../../helpers'; import useGetColor from '../../../hooks/useColor'; import { MAX_SIZE_TO_DOWNLOAD } from '../../../services/drive/constants'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; @@ -56,7 +56,7 @@ function DriveItemInfoModal(): JSX.Element { return <>; } - const isFolder = !item.fileId; + const isFolder = checkIsFolder(item); const handleRenameItem = () => { dispatch(uiActions.setShowItemModal(false)); @@ -165,7 +165,8 @@ function DriveItemInfoModal(): JSX.Element { const handleExportFile = async () => { try { - if (!item.fileId) { + const fileSize = getFileSize(item); + if (!item.fileId && fileSize !== 0) { throw new Error('Item fileID not found'); } const canDownloadFile = isFileDownloadable(); @@ -185,15 +186,20 @@ function DriveItemInfoModal(): JSX.Element { return decryptedFilePath; } - setDownloadProgress({ totalBytes: 0, progress: 0, bytesReceived: 0 }); - setExporting(true); - const downloadPath = await downloadItem( - item.fileId, - item.bucket as string, - decryptedFilePath, - parseInt(item.size?.toString() ?? '0'), - ); - setExporting(false); + let downloadPath: string; + + if (isEmptyFile(item)) { + await drive.file.createEmptyDownloadedFile(decryptedFilePath); + downloadPath = decryptedFilePath; + } else { + if (!item.fileId) { + throw new Error('Item fileID not found for non-empty file'); + } + setDownloadProgress({ totalBytes: 0, progress: 0, bytesReceived: 0 }); + setExporting(true); + downloadPath = await downloadItem(item.fileId, item.bucket as string, decryptedFilePath, fileSize); + setExporting(false); + } await fs.shareFile({ title: item.name, fileUri: downloadPath, @@ -204,18 +210,24 @@ function DriveItemInfoModal(): JSX.Element { errorService.reportError(error); } finally { setExporting(false); + dispatch(uiActions.setShowItemModal(false)); } }; + const handleAbortDownload = () => { setExporting(false); + dispatch(uiActions.setShowItemModal(false)); + if (!downloadAbortableRef.current) return; downloadAbortableRef.current('User requested abort'); }; + const handleAndroidDownloadFile = async () => { try { setDownloadProgress({ totalBytes: 0, progress: 0, bytesReceived: 0 }); - if (!item.fileId) { + const fileSize = getFileSize(item); + if (!item.fileId && fileSize !== 0) { throw new Error('Item fileID not found'); } const canDownloadFile = isFileDownloadable(); @@ -233,14 +245,13 @@ function DriveItemInfoModal(): JSX.Element { // 2. If the file doesn't exists, download it if (!existsDecrypted) { - setExporting(true); - await downloadItem( - item.fileId, - item.bucket as string, - decryptedFilePath, - parseInt(item.size?.toString() ?? '0'), - ); - setExporting(false); + if (isEmptyFile(item)) { + await drive.file.createEmptyDownloadedFile(decryptedFilePath); + } else { + setExporting(true); + await downloadItem(item.fileId as string, item.bucket as string, decryptedFilePath, fileSize); + setExporting(false); + } } // 3. Copy the decrypted file (is a tmp, so this will dissapear, that's why we copy it) @@ -259,7 +270,8 @@ function DriveItemInfoModal(): JSX.Element { const handleiOSSaveToFiles = async () => { try { setDownloadProgress({ totalBytes: 0, progress: 0, bytesReceived: 0 }); - if (!item.fileId) { + const fileSize = getFileSize(item); + if (!item.fileId && fileSize !== 0) { throw new Error('Item fileID not found'); } const canDownloadFile = isFileDownloadable(); @@ -277,14 +289,14 @@ function DriveItemInfoModal(): JSX.Element { // 2. If the file doesn't exists, download it if (!existsDecrypted) { - setExporting(true); - await downloadItem( - item.fileId, - item.bucket as string, - decryptedFilePath, - parseInt(item.size?.toString() ?? '0'), - ); - setExporting(false); + if (isEmptyFile(item)) { + await drive.file.createEmptyDownloadedFile(decryptedFilePath); + } else { + setExporting(true); + + await downloadItem(item.fileId as string, item.bucket as string, decryptedFilePath, fileSize); + setExporting(false); + } } // 3. Share to iOS files app @@ -293,6 +305,8 @@ function DriveItemInfoModal(): JSX.Element { fileUri: decryptedFilePath, saveToiOSFiles: true, }); + + notifications.success(strings.messages.driveDownloadSuccess); } catch (error) { notifications.error(strings.errors.generic.message); logger.error('Error on handleiOSSaveToFiles function:', JSON.stringify(error)); @@ -364,7 +378,7 @@ function DriveItemInfoModal(): JSX.Element { } if ( - item?.size && + (item?.size || item?.size === 0) && downloadProgress?.bytesReceived && downloadProgress?.bytesReceived >= parseInt(item?.size?.toString()) ) { diff --git a/src/components/modals/MoveItemsModal/index.tsx b/src/components/modals/MoveItemsModal/index.tsx index 1dc427059..1d664d33f 100644 --- a/src/components/modals/MoveItemsModal/index.tsx +++ b/src/components/modals/MoveItemsModal/index.tsx @@ -25,6 +25,7 @@ import Portal from '@burstware/react-native-portal'; import { useDrive } from '@internxt-mobile/hooks/drive'; import { useNavigation } from '@react-navigation/native'; import { useTailwind } from 'tailwind-rn'; +import { checkIsFile, checkIsFolder } from '../../../helpers'; import useGetColor from '../../../hooks/useColor'; import { logger } from '../../../services/common'; import notificationsService from '../../../services/NotificationsService'; @@ -82,7 +83,7 @@ function MoveItemsModal(): JSX.Element { status: DriveItemStatus.Idle, data: { bucket: child.bucket, - isFolder: 'fileId' in child ? false : true, + isFolder: checkIsFolder(child as any), thumbnails: (child as DriveFileData).thumbnails, currentThumbnail: null, createdAt: child.createdAt, @@ -106,7 +107,7 @@ function MoveItemsModal(): JSX.Element { [sortMode, destinationFolderContentResponse], ); - const isFolder = !!(itemToMove && !itemToMove.fileId); + const isFolder = checkIsFolder(itemToMove); const canGoBack = currentFolderIsRootFolder ? false : true; const onMoveButtonPressed = () => { setConfirmModalOpen(true); @@ -170,6 +171,7 @@ function MoveItemsModal(): JSX.Element { plainName: itemToMove.name, folderId: destinationFolderContentResponse.id, folderUuid: destinationFolderId, + isFolder, }; // Added any because itemToMove is not typed correctly driveCtx.addItemToTree(destinationFolderId, itemForDestination as any, isFolder); @@ -380,7 +382,7 @@ function MoveItemsModal(): JSX.Element { return ( ; } - const isFolder = !item.fileId; + const isFolder = checkIsFolder(item); const handleCopyLink = async () => { const existingLink = await driveUseCases.generateShareLink({ diff --git a/src/components/modals/SharedLinkSettingsModal/SharedLinkSettingsModal.tsx b/src/components/modals/SharedLinkSettingsModal/SharedLinkSettingsModal.tsx index ee3daa09d..3ffa515c5 100644 --- a/src/components/modals/SharedLinkSettingsModal/SharedLinkSettingsModal.tsx +++ b/src/components/modals/SharedLinkSettingsModal/SharedLinkSettingsModal.tsx @@ -11,6 +11,7 @@ import AppText from 'src/components/AppText'; import { useAppDispatch, useAppSelector } from 'src/store/hooks'; import { driveActions } from 'src/store/slices/drive'; import { useTailwind } from 'tailwind-rn'; +import { checkIsFolder } from '../../../helpers'; import BottomModal from '../BottomModal'; import { GeneratingLinkModal } from '../common/GeneratingLinkModal'; import { animations } from './animations'; @@ -120,7 +121,7 @@ export const SharedLinkSettingsModal: React.FC = ( return; } - const isFolder = item?.fileId ? false : true; + const isFolder = checkIsFolder(item); // A share link already exists, obtain it if (item?.token && item?.code) { diff --git a/src/components/modals/SignOutModal/index.tsx b/src/components/modals/SignOutModal/index.tsx index 180144c32..10574a1e6 100644 --- a/src/components/modals/SignOutModal/index.tsx +++ b/src/components/modals/SignOutModal/index.tsx @@ -1,17 +1,16 @@ -import React from 'react'; import { View } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useTailwind } from 'tailwind-rn'; import strings from '../../../../assets/lang/strings'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { authSelectors, authThunks } from '../../../store/slices/auth'; import { uiActions } from '../../../store/slices/ui'; -import CenterModal from '../CenterModal'; +import { RootScreenNavigationProp } from '../../../types/navigation'; import AppButton from '../../AppButton'; import AppText from '../../AppText'; -import { authSelectors, authThunks } from '../../../store/slices/auth'; -import { useNavigation } from '@react-navigation/native'; -import { RootScreenNavigationProp } from '../../../types/navigation'; -import { useTailwind } from 'tailwind-rn'; import UserProfilePicture from '../../UserProfilePicture'; +import CenterModal from '../CenterModal'; function SignOutModal(): JSX.Element { const tailwind = useTailwind(); @@ -27,7 +26,7 @@ function SignOutModal(): JSX.Element { onClosed(); }; const onSignOutButtonPressed = () => { - dispatch(authThunks.signOutThunk()); + dispatch(authThunks.signOutThunk({ reason: 'manual' })); navigation.replace('SignIn'); onClosed(); }; @@ -35,7 +34,7 @@ function SignOutModal(): JSX.Element { return ( - + {strings.modals.SignOutModal.title} diff --git a/src/contexts/Drive/Drive.context.tsx b/src/contexts/Drive/Drive.context.tsx index ffc030b8f..e8e92a542 100644 --- a/src/contexts/Drive/Drive.context.tsx +++ b/src/contexts/Drive/Drive.context.tsx @@ -9,6 +9,7 @@ import { AppStateStatus, NativeEventSubscription } from 'react-native'; import { driveFolderService } from '@internxt-mobile/services/drive/folder'; import { Thumbnail } from '@internxt/sdk/dist/drive/storage/types'; +import { mapFileWithIsFolder, mapFolderWithIsFolder } from 'src/helpers/driveItemMappers'; export type DriveFoldersTreeNode = { name: string; @@ -139,8 +140,8 @@ export const DriveContextProvider: React.FC = ({ chil return { thereAreMoreFiles, thereAreMoreFolders, - folders: foldersInFolder.folders.map((folder) => { - const driveFolder = { + folders: foldersInFolder.folders.map((folder) => + mapFolderWithIsFolder({ ...folder, updatedAt: folder.updatedAt.toString(), createdAt: folder.createdAt.toString(), @@ -152,13 +153,10 @@ export const DriveContextProvider: React.FC = ({ chil userId: folder.userId, // @ts-expect-error - API is returning status, missing from SDK status: folder.status, - isFolder: true, - }; - - return driveFolder; - }), - files: filesInFolder.files.map((file) => { - const driveFile = { + }), + ), + files: filesInFolder.files.map((file) => + mapFileWithIsFolder({ ...file, uuid: file.uuid, id: file.id, @@ -173,10 +171,8 @@ export const DriveContextProvider: React.FC = ({ chil size: typeof file.size === 'bigint' ? Number(file.size) : file.size, folderId: file.folderId, thumbnails: file.thumbnails ?? [], - }; - - return driveFile; - }), + }), + ), }; }; @@ -189,24 +185,25 @@ export const DriveContextProvider: React.FC = ({ chil const folderContent = await driveFolderService.getFolderContentByUuid(folderId); return { - folders: folderContent.children.map((folder) => ({ - uuid: folder.uuid, - plainName: folder.plainName || folder.plain_name || '', - id: folder.id, - bucket: folder.bucket || null, - createdAt: folder.createdAt, - deleted: false, - name: folder.plainName ?? folder.plain_name ?? (folder.name || ''), - parentId: folder.parentId || folder.parent_id || null, - parentUuid: folderId, - updatedAt: folder.updatedAt, - userId: folder.userId, - // @ts-expect-error - API is returning status, missing from SDK - status: folder.status, - isFolder: true, - })), - files: folderContent.files.map( - (file): DriveFileForTree => ({ + folders: folderContent.children.map((folder) => + mapFolderWithIsFolder({ + uuid: folder.uuid, + plainName: folder.plainName || folder.plain_name || '', + id: folder.id, + bucket: folder.bucket || null, + createdAt: folder.createdAt, + deleted: false, + name: folder.plainName ?? folder.plain_name ?? (folder.name || ''), + parentId: folder.parentId || folder.parent_id || null, + parentUuid: folderId, + updatedAt: folder.updatedAt, + userId: folder.userId, + // @ts-expect-error - API is returning status, missing from SDK + status: folder.status, + }), + ), + files: folderContent.files.map((file) => + mapFileWithIsFolder({ uuid: file.uuid, plainName: file.plainName || file.plain_name || '', bucket: file.bucket, diff --git a/src/helpers/driveItemMappers.spec.ts b/src/helpers/driveItemMappers.spec.ts new file mode 100644 index 000000000..0f79ad9a3 --- /dev/null +++ b/src/helpers/driveItemMappers.spec.ts @@ -0,0 +1,407 @@ +import { SharedFiles, SharedFolders } from '@internxt/sdk/dist/drive/share/types'; +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; +import { TrashItem } from '../services/drive/trash'; +import { + mapFileWithIsFolder, + mapFilesWithIsFolder, + mapFolderWithIsFolder, + mapFoldersWithIsFolder, + mapRecentFile, + mapSharedFile, + mapSharedFolder, + mapTrashFile, + mapTrashFolder, +} from './driveItemMappers'; + +describe('Drive item mappers', () => { + describe('Generic mappers', () => { + describe('mapFileWithIsFolder', () => { + it('when mapping a file object, then it adds isFolder: false', () => { + const file = { + id: 1, + name: 'document.pdf', + type: 'pdf', + size: 1024, + }; + + const result = mapFileWithIsFolder(file); + + expect(result).toEqual({ + id: 1, + name: 'document.pdf', + type: 'pdf', + size: 1024, + isFolder: false, + }); + expect(result.isFolder).toBe(false); + }); + + it('when mapping a file with existing properties, then it preserves all properties', () => { + const file = { + id: 123, + uuid: 'abc-123', + fileId: 'file-456', + bucket: 'bucket-789', + createdAt: '2025-12-18', + }; + + const result = mapFileWithIsFolder(file); + + expect(result).toMatchObject(file); + expect(result.isFolder).toBe(false); + }); + }); + + describe('mapFolderWithIsFolder', () => { + it('when mapping a folder object, then it adds isFolder: true', () => { + const folder = { + id: 1, + name: 'My Folder', + parentId: 2, + }; + + const result = mapFolderWithIsFolder(folder); + + expect(result).toEqual({ + id: 1, + name: 'My Folder', + parentId: 2, + isFolder: true, + }); + expect(result.isFolder).toBe(true); + }); + + it('when mapping a folder with existing properties, then it preserves all properties', () => { + const folder = { + id: 456, + uuid: 'folder-uuid-789', + parentUuid: 'parent-uuid-123', + createdAt: '2025-12-18', + }; + + const result = mapFolderWithIsFolder(folder); + + expect(result).toMatchObject(folder); + expect(result.isFolder).toBe(true); + }); + }); + + describe('mapFilesWithIsFolder', () => { + it('when mapping an array of files, then all have isFolder: false', () => { + const files = [ + { id: 1, name: 'file1.txt' }, + { id: 2, name: 'file2.jpg' }, + { id: 3, name: 'file3.pdf' }, + ]; + + const result = mapFilesWithIsFolder(files); + + expect(result).toHaveLength(3); + result.forEach((file) => { + expect(file.isFolder).toBe(false); + }); + }); + + it('when mapping an empty array, then it returns an empty array', () => { + const result = mapFilesWithIsFolder([]); + + expect(result).toEqual([]); + }); + }); + + describe('mapFoldersWithIsFolder', () => { + it('when mapping an array of folders, then all have isFolder: true', () => { + const folders = [ + { id: 1, name: 'Folder 1' }, + { id: 2, name: 'Folder 2' }, + { id: 3, name: 'Folder 3' }, + ]; + + const result = mapFoldersWithIsFolder(folders); + + expect(result).toHaveLength(3); + result.forEach((folder) => { + expect(folder.isFolder).toBe(true); + }); + }); + + it('when mapping an empty array, then it returns an empty array', () => { + const result = mapFoldersWithIsFolder([]); + + expect(result).toEqual([]); + }); + }); + }); + + describe('Recent file mapper', () => { + describe('mapRecentFile', () => { + it('when mapping a recent file with plainName, then it uses plainName as name', () => { + const recentFile = { + id: 1, + fileId: 'file-123', + name: 'encrypted-name', + plainName: 'My Document.pdf', + type: 'pdf', + size: 2048, + } as unknown as DriveFileData; + + const result = mapRecentFile(recentFile); + + expect(result.name).toBe('My Document.pdf'); + expect(result.isFolder).toBe(false); + }); + + it('when mapping a recent file without plainName, then it uses name', () => { + const recentFile = { + id: 2, + fileId: 'file-456', + name: 'document.txt', + plainName: null, + type: 'txt', + size: 512, + } as unknown as DriveFileData; + + const result = mapRecentFile(recentFile); + + expect(result.name).toBe('document.txt'); + expect(result.isFolder).toBe(false); + }); + + it('when mapping a recent file, then it preserves all original properties', () => { + const recentFile = { + id: 3, + fileId: 'file-789', + name: 'name', + plainName: 'plainName', + type: 'jpg', + size: 1024, + uuid: 'uuid-123', + bucket: 'bucket-456', + createdAt: '2025-12-18', + updatedAt: '2025-12-18', + } as unknown as DriveFileData; + + const result = mapRecentFile(recentFile); + + expect(result).toMatchObject({ + id: 3, + fileId: 'file-789', + type: 'jpg', + size: 1024, + uuid: 'uuid-123', + bucket: 'bucket-456', + }); + expect(result.name).toBe('plainName'); + expect(result.isFolder).toBe(false); + }); + }); + }); + + describe('Trash item mappers', () => { + describe('mapTrashFile', () => { + it('when mapping a trash file, then it uses plainName and sets isFolder: false', () => { + const trashFile = { + id: 1, + name: 'encrypted-name', + plainName: 'Deleted File.pdf', + type: 'pdf', + size: 1024, + } as unknown as TrashItem; + + const result = mapTrashFile(trashFile); + + expect(result.name).toBe('Deleted File.pdf'); + expect(result.isFolder).toBe(false); + }); + + it('when mapping a trash file, then it preserves all properties', () => { + const trashFile = { + id: 2, + uuid: 'trash-file-uuid', + plainName: 'File.txt', + type: 'txt', + size: 512, + deletedAt: '2025-12-18', + } as unknown as TrashItem; + + const result = mapTrashFile(trashFile); + + expect(result).toMatchObject({ + id: 2, + uuid: 'trash-file-uuid', + type: 'txt', + size: 512, + deletedAt: '2025-12-18', + }); + expect(result.name).toBe('File.txt'); + expect(result.isFolder).toBe(false); + }); + }); + + describe('mapTrashFolder', () => { + it('when mapping a trash folder, then it sets isFolder: true', () => { + const trashFolder = { + id: 1, + name: 'Deleted Folder', + type: 'folder', + parentUuid: 'parent-uuid', + } as unknown as TrashItem; + + const result = mapTrashFolder(trashFolder); + + expect(result.isFolder).toBe(true); + }); + + it('when mapping a trash folder, then it preserves all properties', () => { + const trashFolder = { + id: 3, + uuid: 'trash-folder-uuid', + name: 'Old Folder', + parentUuid: 'parent-uuid-123', + deletedAt: '2025-12-18', + } as unknown as TrashItem; + + const result = mapTrashFolder(trashFolder); + + expect(result).toMatchObject({ + id: 3, + uuid: 'trash-folder-uuid', + name: 'Old Folder', + parentUuid: 'parent-uuid-123', + deletedAt: '2025-12-18', + }); + expect(result.isFolder).toBe(true); + }); + }); + }); + + describe('Shared item mappers', () => { + describe('mapSharedFile', () => { + it('when mapping a shared file, then it sets isFolder: false', () => { + const sharedFile = { + id: 1, + name: 'Shared Document.pdf', + type: 'pdf', + size: 2048, + } as unknown as SharedFiles; + + const result = mapSharedFile(sharedFile); + + expect(result.isFolder).toBe(false); + }); + + it('when mapping a shared file, then it preserves all properties', () => { + const sharedFile = { + id: 2, + uuid: 'shared-file-uuid', + name: 'Report.xlsx', + type: 'xlsx', + size: 4096, + views: 10, + } as unknown as SharedFiles; + + const result = mapSharedFile(sharedFile); + + expect(result).toMatchObject({ + id: 2, + uuid: 'shared-file-uuid', + name: 'Report.xlsx', + type: 'xlsx', + size: 4096, + views: 10, + }); + expect(result.isFolder).toBe(false); + }); + }); + + describe('mapSharedFolder', () => { + it('when mapping a shared folder, then it sets isFolder: true', () => { + const sharedFolder = { + id: 1, + name: 'Shared Folder', + } as unknown as SharedFolders; + + const result = mapSharedFolder(sharedFolder); + + expect(result.isFolder).toBe(true); + }); + + it('when mapping a shared folder, then it preserves all properties', () => { + const sharedFolder = { + id: 3, + uuid: 'shared-folder-uuid', + name: 'Team Documents', + views: 25, + } as unknown as SharedFolders; + + const result = mapSharedFolder(sharedFolder); + + expect(result).toMatchObject({ + id: 3, + uuid: 'shared-folder-uuid', + name: 'Team Documents', + views: 25, + }); + expect(result.isFolder).toBe(true); + }); + }); + }); + + describe('Edge cases', () => { + it('when mapping a file with type "folder", then isFolder is still false', () => { + const file = { + id: 1, + name: 'archive.folder', + type: 'folder', + size: 2048, + }; + + const result = mapFileWithIsFolder(file); + + expect(result.isFolder).toBe(false); + expect(result.type).toBe('folder'); + }); + + it('when mapping objects with existing isFolder field, then it gets overwritten', () => { + const fileWithWrongFlag = { + id: 1, + name: 'file.txt', + isFolder: true, + } as any; + + const result = mapFileWithIsFolder(fileWithWrongFlag); + + expect(result.isFolder).toBe(false); + }); + + it('when mapping large arrays, then all items are processed correctly', () => { + const files = Array.from({ length: 100 }, (_, i) => ({ + id: i, + name: `file-${i}.txt`, + })); + + const result = mapFilesWithIsFolder(files); + + expect(result).toHaveLength(100); + expect(result.every((f) => f.isFolder === false)).toBe(true); + }); + }); + + describe('Type consistency', () => { + it('when using mapFileWithIsFolder, then TypeScript infers isFolder as literal false', () => { + const file = { id: 1, name: 'test.txt' }; + const result = mapFileWithIsFolder(file); + + const isFolderType: false = result.isFolder; + expect(isFolderType).toBe(false); + }); + + it('when using mapFolderWithIsFolder, then TypeScript infers isFolder as literal true', () => { + const folder = { id: 1, name: 'test folder' }; + const result = mapFolderWithIsFolder(folder); + + const isFolderType: true = result.isFolder; + expect(isFolderType).toBe(true); + }); + }); +}); diff --git a/src/helpers/driveItemMappers.ts b/src/helpers/driveItemMappers.ts new file mode 100644 index 000000000..3083dd149 --- /dev/null +++ b/src/helpers/driveItemMappers.ts @@ -0,0 +1,73 @@ +import { SharedFiles, SharedFolders } from '@internxt/sdk/dist/drive/share/types'; +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; +import { TrashItem } from '../services/drive/trash'; + +/** + * Adds isFolder field to a file item + */ +export const mapFileWithIsFolder = (file: T): T & { isFolder: false } => ({ + ...file, + isFolder: false, +}); + +/** + * Adds isFolder field to a folder item + */ +export const mapFolderWithIsFolder = (folder: T): T & { isFolder: true } => ({ + ...folder, + isFolder: true, +}); + +/** + * Maps an array of files adding isFolder: false to each + */ +export const mapFilesWithIsFolder = (files: T[]): (T & { isFolder: false })[] => + files.map(mapFileWithIsFolder); + +/** + * Maps an array of folders adding isFolder: true to each + */ +export const mapFoldersWithIsFolder = (folders: T[]): (T & { isFolder: true })[] => + folders.map(mapFolderWithIsFolder); + +/** + * Maps recent files (which are always files) + */ +export const mapRecentFile = (file: DriveFileData) => ({ + ...file, + name: file.plainName ?? file.name, + isFolder: false, +}); + +/** + * Maps trash files + */ +export const mapTrashFile = (file: TrashItem) => ({ + ...file, + name: file.plainName, + isFolder: false, +}); + +/** + * Maps trash folders + */ +export const mapTrashFolder = (folder: TrashItem) => ({ + ...folder, + isFolder: true, +}); + +/** + * Maps shared files + */ +export const mapSharedFile = (file: SharedFiles) => ({ + ...file, + isFolder: false, +}); + +/** + * Maps shared folders + */ +export const mapSharedFolder = (folder: SharedFolders) => ({ + ...folder, + isFolder: true, +}); diff --git a/src/helpers/driveItems.spec.ts b/src/helpers/driveItems.spec.ts new file mode 100644 index 000000000..090042ccd --- /dev/null +++ b/src/helpers/driveItems.spec.ts @@ -0,0 +1,452 @@ +import { TrashItem } from '../services/drive/trash/driveTrash.service'; +import { DriveItemData, DriveItemFocused } from '../types/drive'; +import { checkIsFile, checkIsFolder, getFileSize, isEmptyFile } from './driveItems'; + +describe('Drive item classification', () => { + describe('Identifying folders', () => { + describe('Using isFolder field (preferred)', () => { + it('when an item has isFolder true, then it is recognized as a folder', () => { + const folder: Partial = { + id: 1, + type: 'jpg', + name: 'My Folder', + isFolder: true, + }; + + expect(checkIsFolder(folder as DriveItemData)).toBe(true); + }); + + it('when an item has isFolder false, then it is not recognized as a folder', () => { + const file: Partial = { + id: 1, + type: 'folder', + name: 'My File', + isFolder: false, + }; + + expect(checkIsFolder(file as DriveItemData)).toBe(false); + }); + + it('when a file with folder extension has isFolder false, then it is correctly identified as file', () => { + const file: Partial = { + id: 1, + type: 'folder', + name: 'archive.folder', + isFolder: false, + }; + + expect(checkIsFolder(file as DriveItemData)).toBe(false); + }); + }); + + describe('Fallback to parentUuid detection', () => { + it('when an item has a parent folder reference, then it is recognized as a folder', () => { + const folder: Partial = { + id: 1, + name: 'My Folder', + parentUuid: 'parent-uuid-123', + }; + + expect(checkIsFolder(folder as DriveItemData)).toBe(true); + }); + + it('when an item has parentUuid without isFolder field, then it is recognized as a folder', () => { + const folder: Partial = { + id: 1, + name: 'My Folder', + parentUuid: 'parent-uuid-123', + }; + + expect(checkIsFolder(folder as DriveItemData)).toBe(true); + }); + }); + + it('when an item is a regular file, then it is not recognized as a folder', () => { + const file: Partial = { + id: 1, + type: 'jpg', + name: 'image.jpg', + fileId: 'file-123', + folderUuid: 'folder-uuid-456', + size: 1024, + }; + + expect(checkIsFolder(file as DriveItemData)).toBe(false); + }); + + it('when an item is a file without extension, then it is not recognized as a folder', () => { + const file = { + id: 1, + type: null, + name: 'file-no-extension', + fileId: 'file-123', + folderUuid: 'folder-uuid-456', + size: 512, + } as unknown; + + expect(checkIsFolder(file as DriveItemData)).toBe(false); + }); + + it('when no item is provided, then it is not recognized as a folder', () => { + expect(checkIsFolder(null as any)).toBe(false); + expect(checkIsFolder(undefined as any)).toBe(false); + }); + + describe('Trashed items', () => { + it('when a trashed item is a folder with parentUuid, then it is recognized as a folder', () => { + const trashFolder = { + id: 1, + type: 'folder', + name: 'Deleted Folder', + parentUuid: 'parent-uuid', + isFolder: true, + } as unknown; + + expect(checkIsFolder(trashFolder as TrashItem)).toBe(true); + }); + + it('when a trashed item is a file without parentUuid, then it is not recognized as a folder', () => { + const trashFile = { + id: 1, + type: 'pdf', + name: 'document.pdf', + folderUuid: 'folder-uuid', + isFolder: false, + } as unknown; + + expect(checkIsFolder(trashFile as TrashItem)).toBe(false); + }); + }); + }); + + describe('Detecting empty files', () => { + it('when a file has zero bytes as number, then it is detected as empty', () => { + const emptyFile: Partial = { + id: 1, + name: 'empty.bin', + type: 'bin', + size: 0, + }; + + expect(isEmptyFile(emptyFile as DriveItemData)).toBe(true); + }); + + it('when a file has zero bytes as string, then it is detected as empty', () => { + const emptyFile = { + id: 1, + name: 'empty.bin', + type: 'bin', + size: '0', + } as unknown; + + expect(isEmptyFile(emptyFile as DriveItemData)).toBe(true); + }); + + it('when a file has content, then it is not detected as empty', () => { + const fileWithContent: Partial = { + id: 1, + name: 'document.pdf', + type: 'pdf', + size: 1024, + }; + + expect(isEmptyFile(fileWithContent as DriveItemData)).toBe(false); + }); + + it('when a file has content as string, then it is not detected as empty', () => { + const fileWithContent = { + id: 1, + name: 'image.jpg', + type: 'jpg', + size: '2048', + } as unknown; + + expect(isEmptyFile(fileWithContent as DriveItemData)).toBe(false); + }); + + it('when file size is not available, then it is not detected as empty', () => { + const fileWithoutSize = { + id: 1, + name: 'file.txt', + type: 'txt', + } as DriveItemData; + + const fileWithNullSize = { + id: 1, + name: 'file.txt', + type: 'txt', + size: null, + } as unknown as DriveItemData; + + expect(isEmptyFile(fileWithoutSize)).toBe(false); + expect(isEmptyFile(fileWithNullSize)).toBe(false); + }); + + it('when no item is provided, then it is not detected as empty', () => { + expect(isEmptyFile(null as any)).toBe(false); + }); + + it('when an item has no size information, then it is not detected as empty', () => { + const item: any = { + id: 1, + name: 'item', + }; + + expect(isEmptyFile(item)).toBe(false); + }); + }); + + describe('Getting file size', () => { + it('when file size is stored as number, then the numeric size is returned', () => { + const file: Partial = { + id: 1, + name: 'file.txt', + size: 1024, + }; + + expect(getFileSize(file as DriveItemData)).toBe(1024); + }); + + it('when file size is stored as string, then the numeric size is returned', () => { + const file = { + id: 1, + name: 'file.txt', + size: '2048', + } as unknown; + + expect(getFileSize(file as DriveItemData)).toBe(2048); + }); + + it('when file has zero bytes as number, then zero is returned', () => { + const file: Partial = { + id: 1, + name: 'empty.bin', + size: 0, + }; + + expect(getFileSize(file as DriveItemData)).toBe(0); + }); + + it('when file has zero bytes as string, then zero is returned', () => { + const file = { + id: 1, + name: 'empty.bin', + size: '0', + } as unknown; + + expect(getFileSize(file as DriveItemData)).toBe(0); + }); + + it('when file size is not available, then zero is returned', () => { + const fileWithUndefinedSize = { + id: 1, + name: 'file.txt', + } as DriveItemData; + + const fileWithNullSize = { + id: 1, + name: 'file.txt', + size: null, + } as unknown as DriveItemData; + + expect(getFileSize(fileWithUndefinedSize)).toBe(0); + expect(getFileSize(fileWithNullSize)).toBe(0); + }); + + it('when no item is provided, then zero is returned', () => { + expect(getFileSize(null as any)).toBe(0); + }); + + it('when an item has no size information, then zero is returned', () => { + const item: any = { + id: 1, + name: 'item', + }; + + expect(getFileSize(item)).toBe(0); + }); + + it('when file is very large, then the correct size is returned', () => { + const largeFile = { + id: 1, + name: 'large-file.bin', + size: '2147483648', + } as unknown; + + expect(getFileSize(largeFile as DriveItemData)).toBe(2147483648); + }); + }); + + describe('Identifying files', () => { + it('when an item is a regular file, then it is recognized as a file', () => { + const file: Partial = { + id: 1, + type: 'pdf', + name: 'document.pdf', + fileId: 'file-123', + folderUuid: 'folder-uuid-456', + size: 1024, + }; + + expect(checkIsFile(file as DriveItemData)).toBe(true); + }); + + it('when an item is a folder, then it is not recognized as a file', () => { + const folder: Partial = { + id: 1, + type: 'folder', + name: 'My Folder', + parentUuid: 'parent-uuid-123', + isFolder: true, + }; + + expect(checkIsFile(folder as DriveItemData)).toBe(false); + }); + + it('when an item is an empty file, then it is recognized as a file', () => { + const emptyFile: Partial = { + id: 1, + type: 'bin', + name: 'empty-file.bin', + folderUuid: 'folder-uuid-456', + size: 0, + }; + + expect(checkIsFile(emptyFile as DriveItemData)).toBe(true); + }); + + it('when an item is a file without extension, then it is recognized as a file', () => { + const file = { + id: 1, + type: null, + name: 'file-no-extension', + fileId: 'file-123', + folderUuid: 'folder-uuid-456', + size: 512, + } as unknown; + + expect(checkIsFile(file as DriveItemData)).toBe(true); + }); + + describe('Focused items', () => { + it('when a focused item is a file, then it is recognized as a file', () => { + const focusedFile: DriveItemFocused = { + id: 1, + type: 'jpg', + name: 'photo.jpg', + fileId: 'file-123', + folderUuid: 'folder-uuid', + size: 2048, + updatedAt: '2025-12-18T00:00:00Z', + isFolder: false, + }; + + expect(checkIsFile(focusedFile)).toBe(true); + }); + + it('when a focused item is a folder, then it is not recognized as a file', () => { + const focusedFolder: DriveItemFocused = { + id: 1, + type: 'folder', + name: 'Documents', + parentUuid: 'parent-uuid', + updatedAt: '2025-12-18T00:00:00Z', + isFolder: true, + }; + + expect(checkIsFile(focusedFolder)).toBe(false); + }); + }); + }); + + describe('Real-world scenarios', () => { + it('when distinguishing between empty file and empty folder, then each is correctly identified', () => { + const emptyFile: Partial = { + id: 1, + type: 'bin', + name: 'empty-file.bin', + folderUuid: 'folder-uuid', + size: 0, + isFolder: false, + }; + + const emptyFolder: Partial = { + id: 2, + type: 'folder', + name: 'Empty Folder', + parentUuid: 'parent-uuid', + size: 0, + isFolder: true, + }; + + expect(checkIsFile(emptyFile as DriveItemData)).toBe(true); + expect(checkIsFolder(emptyFile as DriveItemData)).toBe(false); + expect(isEmptyFile(emptyFile as DriveItemData)).toBe(true); + expect(getFileSize(emptyFile as DriveItemData)).toBe(0); + + expect(checkIsFile(emptyFolder as DriveItemData)).toBe(false); + expect(checkIsFolder(emptyFolder as DriveItemData)).toBe(true); + expect(isEmptyFile(emptyFolder as DriveItemData)).toBe(true); + expect(getFileSize(emptyFolder as DriveItemData)).toBe(0); + }); + + it('when processing a large file from production, then it is correctly identified', () => { + const realFile = { + bucket: 'd871da4c5aacc64e106b0afb', + createdAt: '2025-12-11T09:09:01.673Z', + fileId: '693a8a2c7ccfc45e1feb5e30', + folderUuid: '2fdb127e-fdd6-4687-9051-53761920e5d2', + id: 1076106294, + name: '1gb', + size: '1073741824', + type: null, + updatedAt: '2025-12-11T09:09:02.000Z', + uuid: 'd4918a26-8ee8-46fd-9cf2-d49a662d84d7', + isFolder: false, + } as unknown; + + expect(checkIsFile(realFile as DriveItemData)).toBe(true); + expect(checkIsFolder(realFile as DriveItemData)).toBe(false); + expect(isEmptyFile(realFile as DriveItemData)).toBe(false); + expect(getFileSize(realFile as DriveItemData)).toBe(1073741824); + }); + + it('when processing a folder from production, then it is correctly identified', () => { + const realFolder: Partial = { + createdAt: '2025-06-10T06:52:56.000Z', + id: 120640427, + name: 'test folder', + parentUuid: '2fdb127e-fdd6-4687-9051-53761920e5d2', + size: 0, + type: 'folder', + updatedAt: '2025-09-30T08:24:17.000Z', + uuid: 'bc7307f2-7e69-4005-8546-f888b1270a12', + isFolder: true, + }; + + expect(checkIsFile(realFolder as DriveItemData)).toBe(false); + expect(checkIsFolder(realFolder as DriveItemData)).toBe(true); + expect(getFileSize(realFolder as DriveItemData)).toBe(0); + }); + + it('when processing a file with folder extension from backend, then isFolder field takes precedence', () => { + const fileWithFolderType = { + bucket: 'd871da4c5aacc64e106b0afb', + createdAt: '2025-12-11T09:09:01.673Z', + fileId: '693a8a2c7ccfc45e1feb5e30', + folderUuid: '2fdb127e-fdd6-4687-9051-53761920e5d2', + id: 1076106294, + name: 'archive.folder', + size: '2048', + type: 'folder', + updatedAt: '2025-12-11T09:09:02.000Z', + uuid: 'd4918a26-8ee8-46fd-9cf2-d49a662d84d7', + isFolder: false, + } as unknown; + + expect(checkIsFile(fileWithFolderType as DriveItemData)).toBe(true); + expect(checkIsFolder(fileWithFolderType as DriveItemData)).toBe(false); + expect(getFileSize(fileWithFolderType as DriveItemData)).toBe(2048); + }); + }); +}); diff --git a/src/helpers/driveItems.ts b/src/helpers/driveItems.ts new file mode 100644 index 000000000..2bef08fe5 --- /dev/null +++ b/src/helpers/driveItems.ts @@ -0,0 +1,65 @@ +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; +import { TrashItem } from '../services/drive/trash'; +import { DriveItemData, DriveItemDataProps, DriveItemFocused } from '../types/drive'; + +/** + * Checks if a Drive item is a folder + * + * @param item - Drive item to check + * @returns true if the item is a folder, false if it's a file + */ +export const checkIsFolder = ( + item: DriveItemData | DriveItemDataProps | DriveItemFocused | DriveFileData | TrashItem, +): boolean => { + if (!item) return false; + + if ('isFolder' in item) { + return !!item.isFolder; + } + // Only folders have parentUuid + if ('parentUuid' in item && item.parentUuid) return true; + + return false; +}; + +/** + * Checks if a file has zero bytes (empty file) + * + * @param item - Drive item to check + * @returns true if the file size is 0, false otherwise + */ +export const isEmptyFile = (item: DriveItemData | DriveItemFocused): boolean => { + if (!item || !('size' in item)) return false; + + const { size } = item; + if (size === undefined || size === null) return false; + + const sizeNumber = Number(size); + return sizeNumber === 0; +}; + +/** + * Gets the numeric size of a file + * + * @param item - Drive item + * @returns The size as a number, or 0 if size is undefined/null + */ +export const getFileSize = (item: DriveItemData | DriveItemFocused): number => { + if (!item || !('size' in item)) return 0; + + const { size } = item; + + if (size === undefined || size === null) return 0; + + return typeof size === 'number' ? size : Number(size); +}; + +/** + * Checks if a file is a file (not a folder) + * + * @param item - Drive item to check + * @returns true if the item is a file, false if it's a folder + */ +export const checkIsFile = (item: DriveItemData | DriveItemFocused): boolean => { + return !checkIsFolder(item); +}; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index eb4c8d1be..3822879ee 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -3,3 +3,5 @@ export * from './normalize'; export * from './crypt/crypt'; export * from './filetypes'; export * from './update'; +export * from './driveItems'; +export * from './driveItemMappers'; diff --git a/src/navigation/TabExplorerNavigator.tsx b/src/navigation/TabExplorerNavigator.tsx index 294085c77..2c61d4ccb 100644 --- a/src/navigation/TabExplorerNavigator.tsx +++ b/src/navigation/TabExplorerNavigator.tsx @@ -54,7 +54,7 @@ export default function TabExplorerNavigator(props: RootStackScreenProps<'TabExp } catch { const isDeletingAccount = await asyncStorageService.getItem(AsyncStorageKey.IsDeletingAccount); if (isDeletingAccount) { - dispatch(authThunks.signOutThunk()); + dispatch(authThunks.signOutThunk({ reason: 'manual' })); props.navigation.replace('DeactivatedAccount'); } } diff --git a/src/plugins/AxiosPlugin.ts b/src/plugins/AxiosPlugin.ts index b5e747d88..acccadca5 100644 --- a/src/plugins/AxiosPlugin.ts +++ b/src/plugins/AxiosPlugin.ts @@ -8,7 +8,7 @@ const axiosPlugin: AppPlugin = { axios.interceptors.response.use(undefined, (err) => { if (err.response) { if (err.response.status === 401) { - store.dispatch(authThunks.signOutThunk()); + store.dispatch(authThunks.signOutThunk({ reason: 'unauthorized' })); } } diff --git a/src/screens/SettingsScreen/index.tsx b/src/screens/SettingsScreen/index.tsx index 8a57d1e05..765716e6a 100644 --- a/src/screens/SettingsScreen/index.tsx +++ b/src/screens/SettingsScreen/index.tsx @@ -11,7 +11,8 @@ import { Trash, } from 'phosphor-react-native'; import { useEffect, useRef, useState } from 'react'; -import { Linking, Platform, ScrollView, Switch, View } from 'react-native'; +import { Linking, Platform, ScrollView, View } from 'react-native'; +import AppSwitch from '../../components/AppSwitch'; import { storageSelectors } from 'src/store/slices/storage'; import { Language } from 'src/types'; @@ -288,7 +289,7 @@ function SettingsScreen({ navigation }: SettingsScreenProps<'SettingsHome'>): JS - ): JS - { const driveListItem: DriveListItem = { status: DriveItemStatus.Idle, @@ -129,8 +130,16 @@ export function DriveFolderScreen({ navigation }: DriveScreenProps<'DriveFolder' const driveSortedItems = useMemo( () => driveUploadingItems - .concat(folderContent.filter((item) => !item.data.fileId).sort(drive.file.getSortFunction(sortMode))) - .concat(folderContent.filter((item) => item.data.fileId).sort(drive.file.getSortFunction(sortMode))), + .concat( + folderContent + .filter((item) => item.data.isFolder) + .sort(drive.file.getSortFunction(sortMode)), + ) + .concat( + folderContent + .filter((item) => !item.data.isFolder) + .sort(drive.file.getSortFunction(sortMode)), + ), [sortMode, driveUploadingItems, folderContent], ); diff --git a/src/screens/drive/RecentsScreen/RecentsScreen.tsx b/src/screens/drive/RecentsScreen/RecentsScreen.tsx index 24e4db456..8ee988e3d 100644 --- a/src/screens/drive/RecentsScreen/RecentsScreen.tsx +++ b/src/screens/drive/RecentsScreen/RecentsScreen.tsx @@ -14,6 +14,7 @@ import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; import EmptyRecentsImage from 'assets/images/screens/empty-recents.svg'; import NoResultsImage from 'assets/images/screens/no-results.svg'; import { useTailwind } from 'tailwind-rn'; +import { checkIsFolder } from '../../../helpers'; import { DriveItemStatus, DriveListType, DriveListViewMode } from '../../../types/drive'; interface RecentsScreenProps { @@ -77,7 +78,7 @@ export function RecentsScreen({ viewMode={DriveListViewMode.List} data={{ ...item, - isFolder: item.fileId ? false : true, + isFolder: checkIsFolder(item), }} progress={-1} /> diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 62b66dcfd..2fec8c8ea 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -1,3 +1,4 @@ +import { logger } from '@internxt-mobile/services/common'; import { internxtMobileSDKConfig } from '@internxt/mobile-sdk'; import { Keys, Password, TwoFactorAuthQR } from '@internxt/sdk'; import { StorageTypes } from '@internxt/sdk/dist/drive'; @@ -154,7 +155,8 @@ class AuthService { } } - public async signout(): Promise { + public async signout(reason: 'manual' | 'unauthorized' | 'token_expired'): Promise { + logger.info(`User logged out - Reason: ${reason}`); analytics.track(AnalyticsEventKey.UserLogout); await asyncStorageService.clearStorage(); await internxtMobileSDKConfig.destroy(); diff --git a/src/services/drive/file/driveFile.service.ts b/src/services/drive/file/driveFile.service.ts index 97649ad9f..984474f07 100644 --- a/src/services/drive/file/driveFile.service.ts +++ b/src/services/drive/file/driveFile.service.ts @@ -259,6 +259,21 @@ class DriveFileService { return measureThumbnail(fileSystemService.pathToUri(destination)); } + /** + * Creates an empty file directly at the destination path + * Used for files with size 0 that don't need network download + * + * @param downloadPath The path where the empty file should be created + * @returns + */ + async createEmptyDownloadedFile(downloadPath: string) { + await fs.createEmptyFile(downloadPath); + + return { + downloadPath, + }; + } + /** * Download and decrypt a file to a given filesystem path * @@ -337,8 +352,10 @@ class DriveFileService { const path = this.getDecryptedFilePath(filename, type); const exists = await fs.exists(path); if (!exists) return false; + const stat = await fs.statRNFS(path); - return exists && stat.size !== 0; + + return exists && stat.isFile(); } getName(filename: string, type?: string) { diff --git a/src/services/drive/trash/driveTrash.service.ts b/src/services/drive/trash/driveTrash.service.ts index 07401019e..eda9d3f25 100644 --- a/src/services/drive/trash/driveTrash.service.ts +++ b/src/services/drive/trash/driveTrash.service.ts @@ -1,6 +1,7 @@ import { SdkManager } from '@internxt-mobile/services/common'; import { AddItemsToTrashPayload, FetchTrashContentResponse } from '@internxt/sdk/dist/drive/storage/types'; import { DeleteItemsPermanentlyPayload } from '@internxt/sdk/dist/drive/trash/types'; +import { mapTrashFile, mapTrashFolder } from '../../../helpers/driveItemMappers'; import { driveFileService } from '../file'; import { driveFolderService } from '../folder'; @@ -27,11 +28,7 @@ class DriveTrashService { ); return { - items: result.map((file) => ({ - ...file, - // Use the plainName as file name - name: file.plainName, - })), + items: result.map(mapTrashFile), hasMore: TRASH_ITEMS_LIMIT === result.length, }; } @@ -45,7 +42,7 @@ class DriveTrashService { ); return { - items: result, + items: result.map(mapTrashFolder), hasMore: TRASH_ITEMS_LIMIT === result.length, }; } diff --git a/src/store/slices/auth/index.ts b/src/store/slices/auth/index.ts index 0bba27e45..72603a2b1 100644 --- a/src/store/slices/auth/index.ts +++ b/src/store/slices/auth/index.ts @@ -173,7 +173,7 @@ export const refreshTokensThunk = createAsyncThunk( - 'auth/signOut', - async (_, { dispatch }) => { - authService.signout().catch(errorService.reportError); - drive.clear().catch(errorService.reportError); - dispatch(uiActions.resetState()); - dispatch(authActions.resetState()); - dispatch(driveActions.resetState()); - dispatch(authActions.setLoggedIn(false)); - authService.emitLogoutEvent(); - }, -); +export const signOutThunk = createAsyncThunk< + void, + { reason: 'manual' | 'unauthorized' | 'token_expired' }, + { state: RootState } +>('auth/signOut', async (payload, { dispatch }) => { + const reason = payload.reason; + authService.signout(reason).catch(errorService.reportError); + drive.clear().catch(errorService.reportError); + dispatch(uiActions.resetState()); + dispatch(authActions.resetState()); + dispatch(driveActions.resetState()); + dispatch(authActions.setLoggedIn(false)); + authService.emitLogoutEvent(); +}); export const refreshUserThunk = createAsyncThunk( 'auth/refreshUser', diff --git a/src/store/slices/drive/index.ts b/src/store/slices/drive/index.ts index 0fa267367..1814e7ce0 100644 --- a/src/store/slices/drive/index.ts +++ b/src/store/slices/drive/index.ts @@ -1,10 +1,10 @@ -import { DriveFileData, DriveFolderData } from '@internxt/sdk/dist/drive/storage/types'; +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { logger } from '@internxt-mobile/services/common'; import drive from '@internxt-mobile/services/drive'; import { items } from '@internxt/lib'; -import { isValidFilename } from 'src/helpers'; +import { checkIsFolder, isValidFilename, mapRecentFile } from 'src/helpers'; import authService from 'src/services/AuthService'; import errorService from 'src/services/ErrorService'; import { ErrorCodes } from 'src/types/errors'; @@ -112,10 +112,7 @@ const initializeThunk = createAsyncThunk( const getRecentsThunk = createAsyncThunk('drive/getRecents', async (_, { dispatch }) => { dispatch(driveActions.setRecentsStatus(ThunkOperationStatus.LOADING)); const recents = await drive.recents.getRecents(); - const recentsParsed = recents.map((recent) => ({ - ...recent, - name: recent.plainName ?? recent.name, - })); + const recentsParsed = recents.map(mapRecentFile); dispatch(driveActions.setRecents(recentsParsed)); }); @@ -124,7 +121,7 @@ const cancelDownloadThunk = createAsyncThunk(' }); const validateDownload = (size: number | undefined): number | null => { - if (!size) return null; + if (!size || size === 0) return null; const sizeInBytes = parseInt(size.toString()); if (sizeInBytes > MAX_SIZE_TO_DOWNLOAD['10GB']) { @@ -251,6 +248,7 @@ const downloadFileThunk = createAsyncThunk< const destinationPath = drive.file.getDecryptedFilePath(name, type); logger.info(`Download destination path: ${destinationPath} `); const fileAlreadyExists = await drive.file.existsDecrypted(name, type); + try { if (!isValidFilename(name)) { throw new Error('This file name is not valid'); @@ -263,7 +261,14 @@ const downloadFileThunk = createAsyncThunk< analytics.trackStart(fileInfo); downloadProgressCallback(0); - await download({ fileId, to: destinationPath }); + const fileSizeNumber = Number(size); + + if (fileSizeNumber === 0) { + logger.info('File has size 0, creating empty file directly'); + await drive.file.createEmptyDownloadedFile(destinationPath); + } else { + await download({ fileId, to: destinationPath }); + } } const uri = fileSystemService.pathToUri(destinationPath); @@ -434,7 +439,7 @@ export const driveSlice = createSlice({ } } }, - selectItem: (state, action: PayloadAction) => { + selectItem: (state, action: PayloadAction) => { const isAlreadySelected = state.selectedItems.filter((element) => { const elementIsFolder = !element.fileId; @@ -444,7 +449,7 @@ export const driveSlice = createSlice({ state.selectedItems = isAlreadySelected ? state.selectedItems : [...state.selectedItems, action.payload]; }, - deselectItem(state, action: PayloadAction) { + deselectItem(state, action: PayloadAction) { const itemsWithoutRemovedItem = state.selectedItems.filter((element) => { const elementIsFolder = !element.fileId; @@ -621,14 +626,18 @@ export const driveSelectors = { }, id: f.id.toString(), })), - items: items.map((f) => ({ - status: DriveItemStatus.Idle, - data: { - ...f, - isFolder: f.fileId ? false : true, - }, - id: f.id.toString(), - })), + items: items.map((f) => { + const isFolder = checkIsFolder(f); + + return { + status: DriveItemStatus.Idle, + data: { + ...f, + isFolder, + }, + id: f.id.toString(), + }; + }), }; }, }; diff --git a/src/types/drive.ts b/src/types/drive.ts index b2a957fb9..094386b50 100644 --- a/src/types/drive.ts +++ b/src/types/drive.ts @@ -1,4 +1,3 @@ -import { DocumentPickerResponse } from 'react-native-document-picker'; import { SharedFiles, SharedFolders } from '@internxt/sdk/dist/drive/share/types'; import { DriveFileData, @@ -7,6 +6,7 @@ import { FolderChild, Thumbnail, } from '@internxt/sdk/dist/drive/storage/types'; +import { DocumentPickerResponse } from 'react-native-document-picker'; const GB = 1024 * 1024 * 1024; export const UPLOAD_FILE_SIZE_LIMIT = 5 * GB; @@ -29,9 +29,9 @@ export interface DriveNavigationStackItem { } export type DriveNavigationStack = DriveNavigationStackItem[]; -export type DriveItemData = DriveFileData & DriveFolderData & { uuid?: string }; +export type DriveItemData = DriveFileData & DriveFolderData & { uuid?: string; isFolder: boolean }; -export type DriveFile = DriveFileData & { uuid?: string }; +export type DriveFile = DriveFileData & { uuid?: string; isFolder: boolean }; export type getModifiedItemsStatus = 'EXISTS' | 'TRASHED' | 'REMOVED'; @@ -296,6 +296,7 @@ export type DriveFileForTree = Omit< > & { uuid: string; plainName: string; + isFolder: boolean; }; export type DriveFolderForTree = Omit< @@ -306,6 +307,7 @@ export type DriveFolderForTree = Omit< folderUuid?: string; plainName: string; status: DriveFileForTree['status']; + isFolder: boolean; }; export interface DownloadedThumbnail { diff --git a/src/useCases/drive/loadSharedItems.ts b/src/useCases/drive/loadSharedItems.ts index 3406bf478..a72fbf765 100644 --- a/src/useCases/drive/loadSharedItems.ts +++ b/src/useCases/drive/loadSharedItems.ts @@ -3,6 +3,7 @@ import { ListShareLinksItem, SharedFiles, SharedFolders } from '@internxt/sdk/di import { DriveFileData, DriveFolderData } from '@internxt/sdk/dist/drive/storage/types'; import { UseCaseResult } from '../../types'; import errorService from '../../services/ErrorService'; +import { mapSharedFile, mapSharedFolder } from '../../helpers/driveItemMappers'; const ITEMS_PER_PAGE = 50; @@ -42,10 +43,10 @@ export const getSharedItems = async ({ shouldGetFiles ? drive.share.getSharedFiles({ page, perPage: ITEMS_PER_PAGE }) : null, ]); - const sharedFoldersList = sharedFolders?.folders ?? []; - const sharedFilesList = sharedFiles?.files ?? []; + const sharedFoldersList = (sharedFolders?.folders ?? []).map(mapSharedFolder); + const sharedFilesList = (sharedFiles?.files ?? []).map(mapSharedFile); - const sharedItems = [...sharedFoldersList, ...sharedFilesList] as (SharedFiles & SharedFolders)[]; + const sharedItems = [...sharedFoldersList, ...sharedFilesList] as (SharedFiles & SharedFolders & { isFolder: boolean })[]; return { success: true, diff --git a/src/useCases/drive/trash.ts b/src/useCases/drive/trash.ts index 76835d20c..366a49c65 100644 --- a/src/useCases/drive/trash.ts +++ b/src/useCases/drive/trash.ts @@ -7,6 +7,7 @@ import { notifications } from '@internxt-mobile/services/NotificationsService'; import { DriveEventKey, DriveItemStatus, DriveListItem } from '@internxt-mobile/types/drive'; import { NotificationType, UseCaseResult } from '@internxt-mobile/types/index'; import strings from 'assets/lang/strings'; +import { checkIsFolder } from '../../helpers'; type GetDriveTrashItemsOptions = { page: number; @@ -36,7 +37,7 @@ export const getDriveTrashItems = async ({ ]); const trashItems = trashFolders.items.concat(trashFiles.items).map((trashItem) => { - const isFolder = !trashItem.fileId ? true : false; + const isFolder = checkIsFolder(trashItem); return { status: DriveItemStatus.Idle, diff --git a/yarn.lock b/yarn.lock index aba68fd40..fc7eefe66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3668,6 +3668,11 @@ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -4614,7 +4619,7 @@ cacache@^15.3.0: tar "^6.0.2" unique-filename "^1.1.1" -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -4633,6 +4638,24 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -4814,7 +4837,7 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.2.0, ci-info@^3.3.0: +ci-info@^3.2.0, ci-info@^3.3.0, ci-info@^3.7.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== @@ -5171,7 +5194,7 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.5.3" -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: +create-hash@^1.1.0, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -5182,7 +5205,7 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hmac@^1.1.0, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -6781,7 +6804,7 @@ find-up@^5.0.0, find-up@~5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-yarn-workspace-root@~2.0.0: +find-yarn-workspace-root@^2.0.0, find-yarn-workspace-root@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== @@ -6839,6 +6862,13 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -6944,6 +6974,15 @@ fs-extra@9.0.0: jsonfile "^6.0.1" universalify "^1.0.0" +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^4.0.2, fs-extra@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" @@ -7035,7 +7074,7 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.6: +get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -7354,6 +7393,16 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" +hash-base@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.2.tgz#79d72def7611c3f6e3c3b5730652638001b10a74" + integrity sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg== + dependencies: + inherits "^2.0.4" + readable-stream "^2.3.8" + safe-buffer "^5.2.1" + to-buffer "^1.2.1" + hash-base@~3.0: version "3.0.4" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" @@ -7948,6 +7997,13 @@ is-typed-array@^1.1.13: dependencies: which-typed-array "^1.1.14" +is-typed-array@^1.1.14: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -8670,6 +8726,17 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stable-stringify@^1.0.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70" + integrity sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -8696,6 +8763,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -8737,6 +8809,13 @@ kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -10001,7 +10080,7 @@ open@^6.2.0: dependencies: is-wsl "^1.1.0" -open@^7.0.3: +open@^7.0.3, open@^7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== @@ -10232,6 +10311,26 @@ password-prompt@^1.0.4: ansi-escapes "^4.3.2" cross-spawn "^7.0.3" +patch-package@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.1.tgz#79d02f953f711e06d1f8949c8a13e5d3d7ba1a60" + integrity sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^10.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.2.4" + yaml "^2.2.2" + path-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" @@ -10300,23 +10399,17 @@ path@^0.12.7: process "^0.11.1" util "^0.10.3" -pbkdf2@3.0.8: - version "3.0.8" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.8.tgz#2f8abf16ebecc82277945d748aba1d78761f61e2" - integrity sha512-Bf7yBd61ChnMqPqf+PxHm34Iiq9M9Bkd/+JqzosPOqwG6FiTixtkpCs4PNd38+6/VYRvAxGe/GgPb4Q4GktFzg== +pbkdf2@3.0.8, pbkdf2@^3.1.2, pbkdf2@^3.1.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.5.tgz#444a59d7a259a95536c56e80c89de31cc01ed366" + integrity sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ== dependencies: - create-hmac "^1.1.2" - -pbkdf2@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" + create-hash "^1.2.0" + create-hmac "^1.1.7" + ripemd160 "^2.0.3" + safe-buffer "^5.2.1" + sha.js "^2.4.12" + to-buffer "^1.2.1" performance-now@^2.1.0: version "2.1.0" @@ -10458,6 +10551,11 @@ postcss@^8.2.15, postcss@^8.4.23, postcss@~8.4.32: picocolors "^1.1.0" source-map-js "^1.2.1" +postinstall-postinstall@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== + prebuild-install@^7.0.1: version "7.1.2" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" @@ -11619,6 +11717,14 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +ripemd160@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.3.tgz#9be54e4ba5e3559c8eee06a25cd7648bbccdf5a8" + integrity sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA== + dependencies: + hash-base "^3.1.2" + inherits "^2.0.4" + rn-fetch-blob@=0.11.2: version "0.11.2" resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.11.2.tgz#bdc483bf1b0c3810d457983494a11fbada446679" @@ -11826,7 +11932,7 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -set-function-length@^1.2.1: +set-function-length@^1.2.1, set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -11866,6 +11972,15 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +sha.js@^2.4.12: + version "2.4.12" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" + integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== + dependencies: + inherits "^2.0.4" + safe-buffer "^5.2.1" + to-buffer "^1.2.0" + shallow-clone@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" @@ -11991,6 +12106,11 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -12745,6 +12865,11 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -12755,6 +12880,15 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA== +to-buffer@^1.2.0, to-buffer@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133" + integrity sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw== + dependencies: + isarray "^2.0.5" + safe-buffer "^5.2.1" + typed-array-buffer "^1.0.3" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -12977,6 +13111,15 @@ typed-array-buffer@^1.0.2: es-errors "^1.3.0" is-typed-array "^1.1.13" +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + typed-array-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" @@ -13426,6 +13569,19 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.15: gopd "^1.0.1" has-tostringtag "^1.0.2" +which-typed-array@^1.1.16: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -13640,6 +13796,11 @@ yaml@^2.2.1, yaml@^2.3.4: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== +yaml@^2.2.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79" + integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== + yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"