diff --git a/package.json b/package.json index 9c0b434..59722d5 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,13 @@ "expo-constants": "~18.0.9", "expo-dev-client": "^6.0.17", "expo-device": "~8.0.9", + "expo-file-system": "~19.0.21", "expo-haptics": "^15.0.7", "expo-local-authentication": "~17.0.7", "expo-navigation-bar": "~5.0.8", "expo-notifications": "~0.32.12", "expo-secure-store": "~15.0.7", + "expo-sharing": "~14.0.8", "expo-splash-screen": "~31.0.10", "expo-status-bar": "^3.0.8", "expo-store-review": "~9.0.8", diff --git a/src/lib/download.ts b/src/lib/download.ts new file mode 100644 index 0000000..2224fd5 --- /dev/null +++ b/src/lib/download.ts @@ -0,0 +1,54 @@ +/** + * Injected JavaScript to intercept blob URL creation for file downloads. + * When the web app creates a blob URL for a downloadable file (like CSV export), + * this script converts the blob to base64 and sends it to the native app + * via postMessage for proper file handling using expo-file-system and expo-sharing. + */ +export const injectedJavaScript = ` + ;(function() { + const originalCreateObjectURL = URL.createObjectURL; + URL.createObjectURL = function(blob) { + const url = originalCreateObjectURL.call(URL, blob); + + // Check if this is a downloadable file type + const downloadableTypes = [ + 'text/csv', + 'application/csv', + 'application/octet-stream', + 'text/plain', + 'application/json' + ]; + + if (blob instanceof Blob && downloadableTypes.includes(blob.type)) { + const reader = new FileReader(); + reader.onloadend = function() { + if (reader.result && typeof reader.result === 'string') { + const base64 = reader.result.split(',')[1]; + if (base64 && window.ReactNativeWebView) { + // Determine filename and extension based on MIME type + const mimeToExt = { + 'text/csv': '.csv', + 'application/csv': '.csv', + 'application/json': '.json', + 'text/plain': '.txt', + 'application/octet-stream': '.csv' // Default to CSV for history export + }; + const ext = mimeToExt[blob.type] || '.csv'; + const filename = 'transaction-history' + ext; + + window.ReactNativeWebView.postMessage(JSON.stringify({ + cmd: 'downloadFile', + data: base64, + filename: filename, + mimeType: blob.type || 'application/octet-stream' + })); + } + } + }; + reader.readAsDataURL(blob); + } + + return url; + }; + })(); +` diff --git a/src/lib/getMessageManager.ts b/src/lib/getMessageManager.ts index 1a3bc85..fff97b7 100644 --- a/src/lib/getMessageManager.ts +++ b/src/lib/getMessageManager.ts @@ -1,7 +1,10 @@ /* Register message handlers and injected JavaScript */ import * as Clipboard from 'expo-clipboard' +import { File as ExpoFile, Paths } from 'expo-file-system' +import * as Sharing from 'expo-sharing' import once from 'lodash.once' import { injectedJavaScript as injectedJavaScriptClipboard } from './clipboard' +import { injectedJavaScript as injectedJavaScriptDownload } from './download' import * as StoreReview from 'expo-store-review' import * as Application from 'expo-application' @@ -32,6 +35,9 @@ export const getMessageManager = once(() => { console.log('[App] Injecting clipboard JavaScript') messageManager.registerInjectedJavaScript(injectedJavaScriptClipboard) + console.log('[App] Injecting download JavaScript') + messageManager.registerInjectedJavaScript(injectedJavaScriptDownload) + const walletManager = getWalletManager() messageManager.on('console', onConsole) @@ -56,6 +62,29 @@ export const getMessageManager = once(() => { // clipboard messageManager.on('setClipboard', evt => Clipboard.setStringAsync(evt.key)) + // file download/sharing + messageManager.on('downloadFile', async evt => { + try { + const { data, filename, mimeType } = evt as unknown as { + data: string + filename: string + mimeType: string + } + + const file = new ExpoFile(Paths.cache, filename) + file.write(data, { encoding: 'base64' }) + + if (await Sharing.isAvailableAsync()) { + await Sharing.shareAsync(file.uri, { mimeType }) + } + + return { success: true } + } catch (error) { + console.error('[downloadFile:Error]', error) + return { success: false, error: String(error) } + } + }) + // expo token for push notifications messageManager.on('getExpoToken', async () => { try { diff --git a/src/lib/navigationFilter.ts b/src/lib/navigationFilter.ts index 15466bc..d31aa1e 100644 --- a/src/lib/navigationFilter.ts +++ b/src/lib/navigationFilter.ts @@ -38,6 +38,12 @@ export const shouldLoadFilter = (request: ShouldStartLoadRequest) => { return true } + // Handle blob URLs - these are handled by injected JavaScript in download.ts + // which intercepts URL.createObjectURL and sends file data via postMessage + if (request.url.startsWith('blob:')) { + return false + } + // External navigation openBrowser(request.url).catch(r => { console.error(`rejection opening in browser url "${request.url}": `, r) diff --git a/yarn.lock b/yarn.lock index 4354c92..558e28d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10458,6 +10458,16 @@ __metadata: languageName: node linkType: hard +"expo-file-system@npm:~19.0.21": + version: 19.0.21 + resolution: "expo-file-system@npm:19.0.21" + peerDependencies: + expo: "*" + react-native: "*" + checksum: 26b4c95c8d30580723995bf8e32287b72eadf0e3442ca14854c15dc47021c4c22eefbc490ab4fadf98b3f0b14847eba3012dfe663c8a8b42de7b1587f6811d8f + languageName: node + linkType: hard + "expo-font@npm:~14.0.9": version: 14.0.9 resolution: "expo-font@npm:14.0.9" @@ -10598,6 +10608,15 @@ __metadata: languageName: node linkType: hard +"expo-sharing@npm:~14.0.8": + version: 14.0.8 + resolution: "expo-sharing@npm:14.0.8" + peerDependencies: + expo: "*" + checksum: 8ac54d82328141dc67add5b9db3f16253b42e0e0aa2d39589fffd4d9cf15067e10262b0ab21837b184caae2799de81d74f8b2d129886c50e25550ef4fc6919f3 + languageName: node + linkType: hard + "expo-splash-screen@npm:~31.0.10": version: 31.0.10 resolution: "expo-splash-screen@npm:31.0.10" @@ -16520,11 +16539,13 @@ __metadata: expo-constants: ~18.0.9 expo-dev-client: ^6.0.17 expo-device: ~8.0.9 + expo-file-system: ~19.0.21 expo-haptics: ^15.0.7 expo-local-authentication: ~17.0.7 expo-navigation-bar: ~5.0.8 expo-notifications: ~0.32.12 expo-secure-store: ~15.0.7 + expo-sharing: ~14.0.8 expo-splash-screen: ~31.0.10 expo-status-bar: ^3.0.8 expo-store-review: ~9.0.8