Skip to content

Mobile: add trade history CSV export - #30839

Open
jbazant wants to merge 4 commits into
developfrom
sofia-local
Open

Mobile: add trade history CSV export#30839
jbazant wants to merge 4 commits into
developfrom
sofia-local

Conversation

@jbazant

@jbazant jbazant commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a CSV export action to the mobile Trade history screen (a share icon in the header, hidden when there are no trades). Pressing it builds a CSV of all trades for the active device and opens the native share sheet (iOS) / directory picker (Android). The CSV-preparation logic lives in @suite-common/trading (prepareTradingHistoryCsv + a platform-agnostic buildTradingHistoryCsv core with injected provider/ticker resolvers) so the desktop app can reuse it (#29956). Columns follow the Figma spec and normalize buy/sell/exchange into a common spend/receive shape: Type maps exchangeswap, fiat legs leave ticker=currency-code and network empty, provider is shown in human-readable form, and values are formula-injection-safe.

Notes for QA

Open Trading → Trade history on mobile and tap the share icon in the header. Verify the exported .csv opens with the 14 spec columns and correct values across buy, sell and swap trades; confirm the button is hidden when there is no trade history. Note: for swap (exchange) trades the "Spend transaction ID" column is intentionally empty (the exchange trade record has no dedicated user-send tx hash).

Related Issue

Resolve #29954

Screenshots:

Simulator.Screen.Recording.-.iPhone.17.-.2026-08-04.at.23.21.38.mov

🌐 Preview deployments

🌐 Suite Web preview: https://dev.suite.sldev.cz/suite-web/sofia-local/web/

🔍 Currents Test Results

🔍 Suite web test results: View in Currents

🔍 Suite desktop test results: View in Currents

🔍 Suite native android test results: View in Currents

🔒 Quarantined E2E Tests

Trezor Suite (desktop) — 2 test(s)
Test Type
Quarantine test: "Recovery - dry run,Recovery after partial recovery" 🙋 manual
Quarantine test: "Recovery - dry run,Recovery with device reconnection" 🙋 manual

Updated: 2026-08-05T08:37:51.378Z • 2 test(s) total

Trezor Suite (web) — 3 test(s)
Test Type
Quarantine test: "Recovery - dry run,Recovery with device reconnection" 🙋 manual
Quarantine test: "TrezorConnect webextension -> Suite Web,second call after popup was closed by user should work" 🙋 manual
Quarantine test: "Recovery T2T1 - dry run,Recovery after partial recovery" 🙋 manual

Updated: 2026-08-05T08:37:56.936Z • 3 test(s) total

@jbazant
jbazant requested a review from a team as a code owner August 4, 2026 21:22
@jbazant

jbazant commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jbazant jbazant changed the title Sofia local Mobile: add trade history CSV export Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The changes add downloadSimple to mobile icon generation and update affected icon codepoints. They add shared trading-history CSV schemas, sanitization, row conversion, CSV generation, and state preparation. Native code saves or shares CSV files by platform. A translated column-label hook and localized messages support exports. A trading-history header button starts the flow and reports export results. Tests cover shared CSV utilities and native export interactions.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation satisfies issue #29954, including mobile CSV export, required trade fields, reusable preparation logic, and native platform handling.
Out of Scope Changes check ✅ Passed The changes are related to the export feature, including the required share icon, reusable utilities, tests, dependencies, and platform integrations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The description covers the change, QA steps, related issue, and screenshot, with sufficient detail about supported platforms and trade types.
Title check ✅ Passed The title clearly identifies the mobile trade history CSV export, which is the primary change in the pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sofia-local

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e96ab734-15c9-47b5-bedd-aca2f5191f7b

📥 Commits

Reviewing files that changed from the base of the PR and between 8d3e9f0 and ef7d549.

⛔ Files ignored due to path filters (7)
  • packages/icons/src/generated/icons/CoinSlash.tsx is excluded by !**/generated/**
  • suite-common/icons/cryptoAssets/cryptoIcons/hype.svg is excluded by !**/*.svg
  • suite-common/icons/cryptoAssets/cryptoIcons/rhc.svg is excluded by !**/*.svg
  • suite-common/icons/cryptoAssets/networkIcons/hype.svg is excluded by !**/*.svg
  • suite-common/icons/cryptoAssets/networkIcons/rhc.svg is excluded by !**/*.svg
  • suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf is excluded by !**/*.ttf
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (13)
  • suite-common/icons/generateIconFont.ts
  • suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json
  • suite-common/trading/src/index.ts
  • suite-common/trading/src/utils/tradeHistoryExportUtils.test.ts
  • suite-common/trading/src/utils/tradeHistoryExportUtils.ts
  • suite-native/intl/src/messages.ts
  • suite-native/module-trading/src/screens/TradingHistoryScreen.tsx
  • suite-native/trading-history/package.json
  • suite-native/trading-history/src/components/TradingHistoryExportButton.test.tsx
  • suite-native/trading-history/src/components/TradingHistoryExportButton.tsx
  • suite-native/trading-history/src/exportTradingHistoryCsv.ts
  • suite-native/trading-history/src/hooks/useTradingHistoryCsvColumnLabels.ts
  • suite-native/trading-history/src/index.ts

Comment thread suite-common/trading/src/utils/tradeHistoryExportUtils.ts
Comment on lines +16 to +18
jest.mock('@suite-native/alerts', () => ({
useAlert: () => ({ showAlert: mockShowAlert, hideAlert: mockHideAlert }),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Spread requireActual in the @suite-native/alerts mock.

This mock replaces the whole module and exposes only useAlert. The @suite-native/toasts mock on lines 20-23 spreads jest.requireActual. If renderWithTradingHistoryProvider or any component in the rendered tree later imports another member of @suite-native/alerts, that import resolves to undefined and the render fails with an error that does not point at this mock. Match the toasts pattern.

♻️ Proposed change
 jest.mock('`@suite-native/alerts`', () => ({
+    ...jest.requireActual('`@suite-native/alerts`'),
     useAlert: () => ({ showAlert: mockShowAlert, hideAlert: mockHideAlert }),
 }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jest.mock('@suite-native/alerts', () => ({
useAlert: () => ({ showAlert: mockShowAlert, hideAlert: mockHideAlert }),
}));
jest.mock('`@suite-native/alerts`', () => ({
...jest.requireActual('`@suite-native/alerts`'),
useAlert: () => ({ showAlert: mockShowAlert, hideAlert: mockHideAlert }),
}));

Comment thread suite-native/trading-history/src/components/TradingHistoryExportButton.tsx Outdated
Comment thread suite-native/trading-history/src/exportTradingHistoryCsv.ts
newFile.write(content);
} else if (Platform.OS === 'ios') {
await Sharing.shareAsync(cachedFile.uri, {
mimeType: CSV_MIME_TYPE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤓 mimeType is android only

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It seems that this is copy of exportBip329. Fixed in 2063cc9. Fixed for exportBip329 as well in f94f5c4.


const result = await exportTradingHistoryCsv(csvContent);

setIsExporting(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧠 Resetting the loading state in finally block, just to be sure as rabbit suggested

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2063cc9.

const newFile = dir.createFile(fileName, CSV_MIME_TYPE);
newFile.write(content);
} else if (Platform.OS === 'ios') {
await Sharing.shareAsync(cachedFile.uri, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🥲 Expo sharing is incomplete and they are missing flag when user cancel the sharing. Currently when I press export and then cancel the sharing dialog the app shows toast Trade History Exported

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should be fixed in 2063cc9.

setIsExporting(true);

// wait for alert animation to finish before starting the export, 500 ms ought to be enough for anybody.
await new Promise(resolve => setTimeout(resolve, 500));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧠 This one is interesting and i have no idea why its acting weird without it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I even tried to slow animations on emulator to see whats happening. It was ... interesting experience.

@trezor-bot

trezor-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

✅ Previously successful run of [Test] suite-native Android E2E workflow has been found.
⏭️ Skipping tests for this run.
💡 If you are unsure about your latest changes, please rerun the workflow manually. (Use the Re-run all jobs option)

if (cachedFile.exists) {
cachedFile.delete();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This creates a timestamped cache file containing exact amounts and transaction IDs for every export and never deletes it. Android does not use this cached file at all, so successful and cancelled attempts both leave unnecessary confidential copies behind; iOS also retains its temporary share file after completion. Branch on the platform before creating the cache file, and delete the iOS temporary file in finally after shareAsync settles.

const trades = selectDeviceTradingTradesOrderedByDate(state);
const csvContent = prepareTradingHistoryCsv(columnLabels)(state, trades);

result = await exportTradingHistoryCsv(csvContent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The finally block resets loading, but it does not convert a thrown selector/CSV-preparation error into the initialized exportFailed result. Any exception in this try skips the result handling below and rejects the alert callback, so no failure toast appears. Add a catch that assigns { success: false, reason: 'exportFailed' }, then retain this finally. Please add a rejection test as well.

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.

📱 Add EXPORT into history

3 participants