Fix "Download CAR file" on mobile (#3816)

* download CAR file using AtpAgent instead of building URL

* add loader icon on download car button

* actually save to disk on android

* style nits

* bottom margin nit

* localize toast

* remove fallback so back button works correctly

* keep throwing an error if mime type isn't used

* be more explicit with toasts

* send errors to sentry when encountered

---------

Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
Matthieu Sieben 2024-05-12 23:18:42 +02:00 committed by GitHub
parent 4458b03173
commit 00a57df5b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 144 additions and 47 deletions

View file

@ -1,12 +1,23 @@
import {Image as RNImage, Share as RNShare} from 'react-native'
import {Image} from 'react-native-image-crop-picker'
import uuid from 'react-native-uuid'
import {cacheDirectory, copyAsync, deleteAsync} from 'expo-file-system'
import {
cacheDirectory,
copyAsync,
deleteAsync,
documentDirectory,
EncodingType,
makeDirectoryAsync,
StorageAccessFramework,
writeAsStringAsync,
} from 'expo-file-system'
import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import {Buffer} from 'buffer'
import RNFetchBlob from 'rn-fetch-blob'
import {logger} from '#/logger'
import {isAndroid, isIOS} from 'platform/detection'
import {Dimensions} from './types'
@ -240,3 +251,64 @@ function normalizePath(str: string, allPlatforms = false): string {
}
return str
}
export async function saveBytesToDisk(
filename: string,
bytes: Uint8Array,
type: string,
) {
const encoded = Buffer.from(bytes).toString('base64')
return await saveToDevice(filename, encoded, type)
}
export async function saveToDevice(
filename: string,
encoded: string,
type: string,
) {
try {
if (isIOS) {
const tmpFileUrl = await withTempFile(filename, encoded)
await Sharing.shareAsync(tmpFileUrl, {UTI: type})
safeDeleteAsync(tmpFileUrl)
return true
} else {
const permissions =
await StorageAccessFramework.requestDirectoryPermissionsAsync()
if (!permissions.granted) {
return false
}
const fileUrl = await StorageAccessFramework.createFileAsync(
permissions.directoryUri,
filename,
type,
)
await writeAsStringAsync(fileUrl, encoded, {
encoding: EncodingType.Base64,
})
return true
}
} catch (e) {
logger.error('Error occurred while saving file', {message: e})
return false
}
}
async function withTempFile(
filename: string,
encoded: string,
): Promise<string> {
// Using a directory so that the file name is not a random string
// documentDirectory will always be available on native, so we assert as a string.
const tmpDirUri = joinPath(documentDirectory as string, String(uuid.v4()))
await makeDirectoryAsync(tmpDirUri, {intermediates: true})
const tmpFileUrl = joinPath(tmpDirUri, filename)
await writeAsStringAsync(tmpFileUrl, encoded, {
encoding: EncodingType.Base64,
})
return tmpFileUrl
}

View file

@ -1,6 +1,7 @@
import {Dimensions} from './types'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {getDataUriSize, blobToDataUri} from './util'
import {Dimensions} from './types'
import {blobToDataUri, getDataUriSize} from './util'
export async function compressIfNeeded(
img: RNImage,
@ -138,3 +139,23 @@ function createResizedImage(
img.src = dataUri
})
}
export async function saveBytesToDisk(
filename: string,
bytes: Uint8Array,
type: string,
) {
const blob = new Blob([bytes], {type})
const url = URL.createObjectURL(blob)
await downloadUrl(url, filename)
// Firefox requires a small delay
setTimeout(() => URL.revokeObjectURL(url), 100)
return true
}
async function downloadUrl(href: string, filename: string) {
const a = document.createElement('a')
a.href = href
a.download = filename
a.click()
}