Skip to content

✨ app: add card statement - #1192

Draft
franm91 wants to merge 8 commits into
themefrom
statement
Draft

✨ app: add card statement#1192
franm91 wants to merge 8 commits into
themefrom
statement

Conversation

@franm91

@franm91 franm91 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Added in-app statement viewing with installment timelines, payment details, totals, help access, and localized formatting.
    • Added payment history with payment details, breakdowns, statement navigation, and PDF downloads.
    • Added platform-specific statement sharing and download support.
  • Improvements
    • Payment screens now link directly to in-app statements instead of an external dashboard.
    • Added loading, empty, error, refresh, and duplicate-download safeguards.
  • Localization
    • Added Spanish and Portuguese translations for statement and payment content.

@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6160a7e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/mobile Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The mobile app adds maturity-based card statements, localized statement views, payment-history navigation, breakdowns, and guarded PDF downloads for web and native platforms.

Changes

Card statement experience

Layer / File(s) Summary
Statement data and download foundation
.changeset/..., app.config.ts, package.json, src/utils/...
Added maturity-specific activity and PDF queries, statement aggregation utilities, maturity discovery, platform-specific downloads, Expo sharing configuration, and a patch changeset.
Statement route and presentation
src/app/(main)/statement/*, src/components/statement/Statement.tsx, src/i18n/es.json, src/i18n/pt.json
Added the routed statement screen with localized timelines, totals, loading and empty states, navigation, help access, and guarded PDF downloads.
Payment history and statement actions
src/app/(main)/payment-history/*, src/components/pay/*, src/components/shared/ModalSheet.tsx
Added payment-history selection, history and breakdown sheets, statement actions, in-app statement navigation, query invalidation, and configurable sheet animation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Pay
  participant PaymentHistory
  participant HistorySheet
  participant Statement
  participant StatementAPI
  participant FileSharing
  Pay->>PaymentHistory: display past maturities
  PaymentHistory->>Pay: return selected maturity
  Pay->>HistorySheet: open payment history
  HistorySheet->>Statement: navigate with maturity
  Statement->>StatementAPI: fetch statement activity
  Statement->>FileSharing: download and share PDF
Loading

Possibly related PRs

  • exactly/exa#770: Provides the maturity-aware statement API consumed by this client implementation.
  • exactly/exa#893: Introduces the multi-card statement data structure consumed by the statement utilities and UI.

Suggested reviewers: dieguezguille, cruzdanilo

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding card statements to the app.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 statement
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch statement

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: 6


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 393538b7-cc1b-4824-8e1b-55d6bc881ef2

📥 Commits

Reviewing files that changed from the base of the PR and between cd1fd34 and c5f13ee.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .changeset/curvy-jobs-sort.md
  • app.config.ts
  • package.json
  • src/app/(main)/statement/_layout.tsx
  • src/app/(main)/statement/index.tsx
  • src/components/pay/Breakdown.tsx
  • src/components/pay/HistorySheet.tsx
  • src/components/pay/Pay.tsx
  • src/components/pay/PaymentHistory.tsx
  • src/components/pay/PaymentSheet.tsx
  • src/components/pay/StatementActions.tsx
  • src/components/shared/ModalSheet.tsx
  • src/components/statement/Statement.tsx
  • src/i18n/es.json
  • src/i18n/pt.json
  • src/utils/server.ts
  • src/utils/statement.ts
  • src/utils/useStatement.ts
  • src/utils/useStatements.ts

Comment thread src/components/pay/Pay.tsx
Comment on lines +24 to +34
const [downloading, setDownloading] = useState(false);

function download() {
if (downloading) return;
setDownloading(true);
downloadStatement(maturity, `account-statement-${maturity}.pdf`)
.catch(reportError)
.finally(() => {
setDownloading(false);
});
}

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 | 🟠 Major | ⚡ Quick win

The statement download logic is duplicated in two components. Both copies hold the same downloading state, build the same filename, call downloadStatement, catch with reportError, and reset in finally.

  • src/components/pay/StatementActions.tsx#L24-L34: replace the local state and download() with a shared hook, for example useDownloadStatement(maturity).
  • src/components/statement/Statement.tsx#L52-L60: remove the local state and download() and use the same hook, keeping the existing maturity === undefined guard inside the call site.
📍 Affects 2 files
  • src/components/pay/StatementActions.tsx#L24-L34 (this comment)
  • src/components/statement/Statement.tsx#L52-L60

Source: Coding guidelines

Comment thread src/components/statement/Statement.tsx Outdated
Comment on lines +27 to +30
const parameter = useLocalSearchParams().maturity;
const raw = Array.isArray(parameter) ? parameter[0] : parameter;
const maturity = raw && /^\d+$/.test(raw) ? Number(raw) : undefined;
const { data, isLoading } = useStatement(maturity);

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

Handle an invalid maturity parameter explicitly.

If maturity is missing or not numeric, useStatement receives undefined, the query is skipped, and the screen renders "No statement for this period". The user cannot distinguish a broken link from an empty period. Render an error state, or redirect back, when raw is present but does not match /^\d+$/.

Comment thread src/i18n/es.json
"{{currency}} via {{methods}}": "{{currency}} vía {{methods}}",
"{{discount}} off": "{{discount}} off",
"{{network}} deposit address": "Dirección de depósito de {{network}}",
"{{percent}} late fee": "{{percent}} de interés",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use late-payment fee terminology in both translations.

Both values translate late fee as interest. This changes the financial meaning.

  • src/i18n/es.json#L20-L20: replace the interest term with a late-payment fee term.
  • src/i18n/pt.json#L20-L20: replace the interest term with a late-payment fee term.
📍 Affects 2 files
  • src/i18n/es.json#L20-L20 (this comment)
  • src/i18n/pt.json#L20-L20

Comment thread src/utils/statement.ts
Comment on lines +30 to +34
const key = item.timestamp.slice(0, 10);
const dates = card.dates.get(key) ?? { label: item.timestamp, rows: [] };
dates.rows.push(...lines.map((line) => ({ merchant: item.merchant.name, ...line })));
card.dates.set(key, dates);
cards.set(item.cardId, card);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Row identity is lost in group(), so the statement list has unstable keys. group() builds each Row from merchant, installment index, and amount, and drops the activity item id. The consumer then has no stable key.

  • src/utils/statement.ts#L30-L34: include the source item id in each pushed row, for example { id: item.id, merchant: item.merchant.name, ...line }, and add id: string to the Row type.
  • src/components/statement/Statement.tsx#L145-L146: replace the composite key with key={row.id}, or with `${row.id}-${row.current}` when one purchase yields several installment rows.
📍 Affects 2 files
  • src/utils/statement.ts#L30-L34 (this comment)
  • src/components/statement/Statement.tsx#L145-L146

Comment thread src/utils/statement.ts
Comment on lines +60 to +67
const url = pdf(bytes);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
document.body.append(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Defer URL.revokeObjectURL so the web download is not cancelled.

The code revokes the object URL in the same task as anchor.click(). Some browsers, notably Safari, start the download asynchronously and then fail because the blob URL is already revoked. Revoke the URL after the current task.

🛠️ Proposed fix
   anchor.click();
   anchor.remove();
-  URL.revokeObjectURL(url);
+  setTimeout(() => {
+    URL.revokeObjectURL(url);
+  }, 0);
📝 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
const url = pdf(bytes);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
document.body.append(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
const url = pdf(bytes);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
document.body.append(anchor);
anchor.click();
anchor.remove();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 0);

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1174 1 1173 1
View the top 1 failed test(s) by shortest run time
web::web
Stack Traces | 98s run time
Element not found: Text matching regex: DeFi

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@cruzdanilo cruzdanilo changed the title Statement ✨ app: add card statement Aug 4, 2026
@cruzdanilo cruzdanilo linked an issue Aug 4, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 3

♻️ Duplicate comments (1)
src/components/pay/Pay.tsx (1)

188-191: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep payment history available after all positions are settled.

Line 188 renders PaymentHistory only when hasPayments is true. Line 191 also removes HistorySheet in the empty state. hasPayments only represents active positions, but useStatements() can still return historical maturities. Render both components outside this conditional, or render payment history with Empty.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9b1997f1-d9df-46e4-9a91-796544dbf19f

📥 Commits

Reviewing files that changed from the base of the PR and between 72658f6 and b469c84.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .changeset/curvy-jobs-sort.md
  • app.config.ts
  • package.json
  • src/app/(main)/statement/_layout.tsx
  • src/app/(main)/statement/index.tsx
  • src/components/pay/Breakdown.tsx
  • src/components/pay/HistorySheet.tsx
  • src/components/pay/Pay.tsx
  • src/components/pay/PaymentHistory.tsx
  • src/components/pay/PaymentSheet.tsx
  • src/components/pay/StatementActions.tsx
  • src/components/shared/ModalSheet.tsx
  • src/components/statement/Statement.tsx
  • src/i18n/es.json
  • src/i18n/pt.json
  • src/utils/server.ts
  • src/utils/statement.ts
  • src/utils/useStatement.ts
  • src/utils/useStatements.ts

Comment thread src/components/statement/Statement.tsx Outdated
Comment thread src/utils/statement.ts
Comment on lines +70 to +76
async function share(bytes: Uint8Array, filename: string) {
const file = new File(Paths.cache, filename);
if (file.exists) file.delete();
file.write(bytes);
if (!(await isAvailableAsync())) throw new Error("sharing unavailable");
await shareAsync(file.uri, { mimeType: "application/pdf", UTI: "com.adobe.pdf", dialogTitle: filename });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

expo-file-system 55 File write creates file if missing create() required

💡 Result:

In the modern expo-file-system API (part of the new file-system implementation), the behavior of file creation depends on which methods you use [1][2]. If you are using the new File object API (e.g., const file = new File(path);), you must explicitly call file.create before performing write operations [1][2]. By default, create will throw an error if the file already exists, unless you pass the { idempotent: true } option [2][3]. If you are using the legacy FileSystem.writeAsStringAsync method, it does not strictly require you to manually "create" the file first in the same way, but it also does not inherently perform safe "if missing" creation logic in a single atomic step without potentially overwriting existing content [4][5]. Key details: - File object API: Requires manual creation. You can use file.create({ idempotent: true }) to ensure the file exists without throwing an error if it is already there [2][3]. - Legacy API: FileSystem.writeAsStringAsync typically writes/overwrites the contents of the file at the specified URI [4][5]. If your goal is to ensure a file exists before writing, the recommended approach in the new API is: const file = new File(directory, 'filename.txt'); file.create({ idempotent: true }); // Ensures file exists; succeeds silently if already there file.write('Your content here'); [1][2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate statement.ts =="
fd -a 'statement\.ts$' . | sed 's#^\./##'

echo "== relevant file outline =="
file="$(fd 'statement\.ts$' . | head -n 1)"
if [ -n "${file:-}" ]; then
  ast-grep outline "$file" --view compact || true
  echo "== relevant file excerpt =="
  cat -n "$file" | sed -n '1,130p'
fi

echo "== dependency pins =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
  [ -e "$f" ] && { echo "--- $f ---" ; sed -n '1,180p' "$f" | rg -n "expo-file-system|@" -C 2 | head -n 80 || true; }
done

echo "== usages of share/isAvailableAsync/File =="
rg -n "function share|share\(bytes|isAvailableAsync|new File|let File|const File|export .*File|expo-file-system" .

Repository: exactly/exa

Length of output: 12988


🌐 Web query:

expo-file-system 55 File write writeAsync missing file create ensureFileAsync idempotent

💡 Result:

In the current version of expo-file-system, there is no method named ensureFileAsync [1][2]. If you are using the modern File API introduced in recent SDK versions, you should explicitly manage file creation using the file.create method before attempting to write to the file [1][3][4]. Here is how the modern File API handles file operations: 1. File Creation: You can instantiate a File object for any path; the file does not need to exist upon instantiation [3][5][6]. To ensure the file exists on the filesystem, you must call the file.create method [1][7]. Note that file.create will throw an error if the file already exists or if permissions are insufficient [1][3]. 2. Writing Content: The file.write method is used to write content to the file [1][2]. Unlike some other APIs, it does not implicitly create the file if it is missing; you must have created it via file.create first [1][4]. 3. Idempotency: Because file.create throws an error if the file already exists, it is not inherently idempotent [1][3]. To achieve idempotent behavior (i.e., ensuring the file exists without throwing an error if it is already there), you should manually check for its existence first: // Example of idempotent-like creation const file = new File(Paths.document, 'example.txt'); const info = await FileSystem.getInfoAsync(file.uri); if (!info.exists) { await file.create; } await file.write('Your content here'); For legacy support or specific use cases, FileSystem.writeAsStringAsync can also write to files [8][2], but it generally requires the file (or the parent directory) to be properly prepared, depending on the platform and URI type [8]. For most new development, the File and Directory classes are the recommended approach [7][4].

Citations:


Ensure the cache file exists before writing.

share writes before isAvailableAsync() can reject or shareAsync can fail, so the cached PDF can remain on the device. Also use if (file.exists) file.delete(); await file.create(); file.write(bytes); because expo-file-system File.write does not create a missing file.

Comment thread src/utils/useStatements.ts
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 1

♻️ Duplicate comments (2)
src/utils/statement.ts (1)

70-76: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Delete the cached statement after sharing.

share() writes sensitive statement data before it checks availability, then never removes the cache file. Failed and completed share flows leave the PDF in Paths.cache.

Check isAvailableAsync() before writing. Delete file in a finally block after shareAsync() resolves or rejects.

This repeats the prior cache-retention finding.

src/i18n/es.json (1)

20-20: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate “late fee” as a late-payment charge.

"{{percent}} de interés" means interest. It changes the financial meaning of "{{percent}} late fee".

Use a late-payment fee term, such as "{{percent}} de recargo por mora".

This repeats the prior translation finding.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 96143a4e-1466-4038-bd37-419c2788b7fa

📥 Commits

Reviewing files that changed from the base of the PR and between 2290865 and 6160a7e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • .changeset/curvy-jobs-sort.md
  • app.config.ts
  • package.json
  • src/app/(main)/payment-history/_layout.tsx
  • src/app/(main)/payment-history/index.tsx
  • src/app/(main)/statement/_layout.tsx
  • src/app/(main)/statement/index.tsx
  • src/components/pay/Breakdown.tsx
  • src/components/pay/History.tsx
  • src/components/pay/HistorySheet.tsx
  • src/components/pay/Pay.tsx
  • src/components/pay/PaymentHistory.tsx
  • src/components/pay/PaymentRow.tsx
  • src/components/pay/PaymentSheet.tsx
  • src/components/pay/Repay.tsx
  • src/components/pay/StatementActions.tsx
  • src/components/shared/ModalSheet.tsx
  • src/components/statement/Statement.tsx
  • src/i18n/es.json
  • src/i18n/pt.json
  • src/utils/server.ts
  • src/utils/statement.ts
  • src/utils/useStatement.ts
  • src/utils/useStatements.ts

Comment on lines +108 to +118
{!empty && (
<XStack gap="$s2" alignItems="center" cursor="pointer" aria-disabled={downloading} onPress={download}>
<Text emphasized subHeadline color="$interactiveBaseBrandDefault">
{t("Download")}
</Text>
{downloading ? (
<Spinner color="$interactiveBaseBrandDefault" />
) : (
<Download size={20} color="$interactiveBaseBrandDefault" />
)}
</XStack>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Expose each statement action as an accessible button.

These controls attach onPress to presentation components without a button role or accessible name. Keyboard and screen-reader users cannot reliably operate them on web.

  • src/components/statement/Statement.tsx#L108-L118: use an accessible button pattern for the download action. Set its accessible name to t("Download").
  • src/components/pay/Breakdown.tsx#L43-L48: use an accessible button pattern for the view-statement action. Set its accessible name to t("View statement").
  • src/components/pay/Breakdown.tsx#L119-L128: use an accessible button pattern for the back action. Set its accessible name to t("Back").

The Action component in src/components/pay/StatementActions.tsx Lines 73-81 provides the local pattern. This repeats the prior statement-control finding.

#!/bin/bash
set -euo pipefail

rg -n -C 3 'role=.*button|aria-label=.*onPress|onPress=' \
  src/components/statement/Statement.tsx \
  src/components/pay/Breakdown.tsx \
  src/components/pay/StatementActions.tsx
📍 Affects 2 files
  • src/components/statement/Statement.tsx#L108-L118 (this comment)
  • src/components/pay/Breakdown.tsx#L43-L48
  • src/components/pay/Breakdown.tsx#L119-L128

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.

ui: repay statement

2 participants