fix: handle blob URL downloads for history CSV export#158
Conversation
The download button in the history tab was not working because blob URLs cannot be opened via Linking.openURL on iOS. This implements native file download/sharing by intercepting blob URL creation in the WebView and using expo-file-system and expo-sharing to save and share the file. Closes shapeshift/web#10731 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughA download mechanism for mobile was implemented by intercepting blob URL creation through injected JavaScript, routing the data to native message handlers that write files to cache and invoke native sharing APIs. Two Expo dependencies (file-system and sharing) were added to support this functionality. Changes
Sequence Diagram(s)sequenceDiagram
participant WebView as WebView<br/>(JavaScript)
participant Bridge as Native Bridge<br/>(Message Handler)
participant FileAPI as Expo FileSystem
participant ShareAPI as Expo Sharing
rect rgb(240, 248, 255)
Note over WebView: Injected Download Script
WebView->>WebView: createObjectURL(blob)<br/>called for CSV/JSON/text
WebView->>WebView: Override intercepts<br/>blob creation
WebView->>WebView: Extract base64<br/>from blob data
end
rect rgb(230, 245, 230)
Note over WebView,Bridge: Cross-Platform Communication
WebView->>Bridge: postMessage({<br/> command: 'downloadFile',<br/> base64, filename, mimeType<br/>})
end
rect rgb(255, 250, 230)
Note over Bridge: File & Share Operations
Bridge->>FileAPI: Write base64 to<br/>cache directory
FileAPI-->>Bridge: File URI
Bridge->>ShareAPI: Share file<br/>(if available)
ShareAPI-->>Bridge: Share success
end
Bridge-->>WebView: Success response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI Agents
In @package.json:
- Line 30: The package.json pins "expo-file-system" to ~19.0.21 which shipped
breaking API changes for Expo SDK 54; audit all imports of "expo-file-system"
(search for import statements referencing expo-file-system) and either migrate
call sites to the new API surface in v19 or change imports to
"expo-file-system/legacy" where you need the old behavior, then update any
affected functions (e.g., readAsStringAsync/writeAsStringAsync/fileSystem
constants) to match the chosen API; ensure tests/build run after switching to
confirm there are no unresolved API mismatches.
🧹 Nitpick comments (4)
src/lib/getMessageManager.ts (1)
77-79: Consider user experience when sharing is unavailable.If
Sharing.isAvailableAsync()returns false, the file is written to cache but not shared with the user. Consider whether this is the intended behavior or if an error should be returned to inform the user.💡 Potential improvement
if (await Sharing.isAvailableAsync()) { await Sharing.shareAsync(file.uri, { mimeType }) + } else { + console.warn('[downloadFile] Sharing not available on this device') + return { success: false, error: 'Sharing not available' } }src/lib/download.ts (3)
8-53: Consider potential side effects of global override.The override of
URL.createObjectURLis never restored to the original implementation. While this works for the current use case, it could potentially affect other code that relies on the original behavior. For this specific fix targeting history CSV export, this is acceptable, but be aware of the global scope impact.
22-49: Consider adding blob URL cleanup.After the blob data is extracted and posted to the native side, consider revoking the blob URL using
URL.revokeObjectURL(url)to prevent potential memory leaks from accumulating blob URLs that are no longer needed.💡 Suggested improvement
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' })); + + // Clean up blob URL after extraction + URL.revokeObjectURL(url); } } }; reader.readAsDataURL(blob); }
34-34: Consider making filename and MIME type mapping more flexible.The hardcoded filename
'transaction-history'and the assumption thatapplication/octet-streammaps to CSV are specific to the current use case. If this code is intended to handle other download scenarios in the future, consider making these more flexible or configurable.Also applies to: 37-37
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (4)
package.jsonsrc/lib/download.tssrc/lib/getMessageManager.tssrc/lib/navigationFilter.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: NeOMakinG
Repo: shapeshift/mobile-app PR: 131
File: app.json:3-3
Timestamp: 2025-08-14T09:58:07.026Z
Learning: In the shapeshift/mobile-app repository, they use Expo's remote managed builds for Android and iOS. Only the version and runtimeVersion fields in app.json are important - build numbers like android.versionCode are managed automatically by the build system and don't need manual updates.
📚 Learning: 2025-08-14T09:58:07.026Z
Learnt from: NeOMakinG
Repo: shapeshift/mobile-app PR: 131
File: app.json:3-3
Timestamp: 2025-08-14T09:58:07.026Z
Learning: In the shapeshift/mobile-app repository, they use Expo's remote managed builds for Android and iOS. Only the version and runtimeVersion fields in app.json are important - build numbers like android.versionCode are managed automatically by the build system and don't need manual updates.
Applied to files:
package.json
📚 Learning: 2025-08-11T08:19:29.564Z
Learnt from: NeOMakinG
Repo: shapeshift/mobile-app PR: 126
File: package.json:27-27
Timestamp: 2025-08-11T08:19:29.564Z
Learning: expo-haptics version ^14.1.4 is compatible with Expo SDK 53 and has been tested to work correctly, despite documentation suggesting ~13.0.0 as the recommended version.
Applied to files:
package.json
🔇 Additional comments (2)
src/lib/navigationFilter.ts (1)
41-45: LGTM!The blob URL filtering logic correctly prevents navigation to blob: URLs and delegates handling to the injected download JavaScript. This ensures the native file handling flow is used instead of attempting to navigate to blob URLs directly.
src/lib/getMessageManager.ts (1)
74-75: TheExpoFileclass constructor andwrite()method are valid for expo-file-system ~19.0.21. This is part of the modern File API (the recommended object-oriented approach), not deprecated code. The File class with its write() method is documented in v19 and is the preferred approach over the legacy FileSystem.writeAsStringAsync() function.
Description
Closes #159.
Fixes history CSV export on mobile by handling
blob:downloads in the native shell.URL.createObjectURL()in the WebView.postMessage.expo-file-systemand opens the share sheet withexpo-sharing.Manual checks
transaction-history.csv