Skip to content

fix: handle blob URL downloads for history CSV export#158

Open
0xApotheosis wants to merge 1 commit into
mainfrom
fix/history-download-button
Open

fix: handle blob URL downloads for history CSV export#158
0xApotheosis wants to merge 1 commit into
mainfrom
fix/history-download-button

Conversation

@0xApotheosis
Copy link
Copy Markdown
Member

@0xApotheosis 0xApotheosis commented Jan 6, 2026

Description

Closes #159.

Fixes history CSV export on mobile by handling blob: downloads in the native shell.

  • Intercepts URL.createObjectURL() in the WebView.
  • Sends file data to native via postMessage.
  • Writes the file with expo-file-system and opens the share sheet with expo-sharing.

Manual checks

  • Build dev client
  • Tap History download
  • Confirm share sheet opens with transaction-history.csv
  • Confirm saved/shared file contents
History CSV export share sheet

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

A 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

Cohort / File(s) Summary
Dependencies
package.json
Added expo-file-system (~19.0.21) and expo-sharing (~14.0.8) for native file and sharing operations on mobile.
Download Interception
src/lib/download.ts
New module exporting injectedJavaScript—a self-invoking script that overrides URL.createObjectURL to intercept blob creation for CSV, JSON, and text MIME types, extracting base64 data and posting a downloadFile message to the native side.
Message Handler & File Operations
src/lib/getMessageManager.ts
Added imports for Expo file/sharing APIs and injected download script. Introduced downloadFile message handler that decodes base64, writes to cache directory, and shares the file via native APIs.
Navigation Safety
src/lib/navigationFilter.ts
Added blob URL filtering in shouldLoadFilter to prevent navigation to blob URLs, ensuring they are handled by download interception logic instead.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A button clicked, but nothing came,
So we bridged JavaScript's flame—
Blob to base64, cache to share,
Now downloads flow through the air! 📥✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing blob URL download handling for CSV export in the history feature on mobile.
Linked Issues check ✅ Passed The PR fully addresses the linked issue #10731 by implementing a complete mobile download flow using injected JavaScript, Expo file-system, and sharing APIs.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the blob URL download issue; adding dependencies, injecting download interception logic, handling messages, and filtering blob URLs are all necessary components of the solution.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/history-download-button

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@0xApotheosis 0xApotheosis marked this pull request as ready for review January 6, 2026 07:38
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.createObjectURL is 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 that application/octet-stream maps 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.

📥 Commits

Reviewing files that changed from the base of the PR and between fa2a6bf and 7a1842c.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • package.json
  • src/lib/download.ts
  • src/lib/getMessageManager.ts
  • src/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: The ExpoFile class constructor and write() 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.

Comment thread package.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Download button in history tab on mobile is dead click

1 participant