Skip to content

PE-9132: Turbo free-tier support — allowance-aware upload UX and typed payment failures - #2166

Merged
vilenarios merged 20 commits into
devfrom
feat/turbo-free-tier
Jul 23, 2026
Merged

vilenarios merged 20 commits into
devfrom
feat/turbo-free-tier

Conversation

@vilenarios

@vilenarios vilenarios commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Client readiness for Turbo's free tier (10 MiB per-wallet pool, 105 KiB per-item cap; see docs/implementation_plan_turbo_free_tier.md). The app now makes an honest free-vs-paid promise on every upload surface, backed by the GET /v1/account/free endpoint, and never leaves a payment rejection hanging.

Two bodies of work:

1. Typed payment failures & honest failure UX

  • Typed errors: HTTP 402TurboPaymentRequiredException (app) / UnderFundException (uploader package); 429 → typed rate-limit. Both are excluded from the uploader's retry loops — retrying a payment rejection just multiplies metered load.
  • No more hangs: every upload-touching surface (9 metadata ops, file/folder upload, snapshot, manifest, standalone + post-upload ArNS, single/multi thumbnail, bulk import) dismisses its progress UI on failure and, on a 402, shows the shared "Free allowance used up → Buy Credits" dialog instead of hanging or showing a generic error.
  • Move fixed: gains a real failure state (it previously emitted Success after errors) and is reordered post-then-commit so a rejected move can't leave the local DB claiming a move the chain never saw.
  • Surfaced payment errors that the shared WorkerPool was silently swallowing (bulk import, multi-thumbnail).

2. Allowance-aware free-vs-paid (this endpoint's payoff)

Previously "free" was decided from item size alone, so a user whose pool was exhausted was told an upload was free, had the payment selector hidden, and then hit a 402. Now free requires both size-eligibility and that the wallet's remaining allowance covers the upload.

Situation What the user sees
Covered by allowance "…free thanks to Turbo"
Size-eligible, upload exceeds a non-zero allowance payment selector + "This upload exceeds your free allowance and will need Credits or AR."
Allowance used up (zero) payment selector + "Free allowance used up. This upload requires Credits or AR."
Item too large for the free tier payment selector, no free message

Wired across the file/folder, snapshot, and manifest paths (including manifest re-upload, which previously skipped payment selection entirely when it wrongly judged everything free). The four states are modeled as one FreeUploadStatus value rendered by one widget, so the surfaces can't drift apart.

Design principles

  • Advisory, never a gate. bytesRemaining is a point-in-time, wallet-level snapshot. It only decides what we promise; Turbo's 402 on upload remains the sole authority on what's actually charged.
  • Fails open. An unreachable endpoint → unknown → the previous size-only behavior. A failed check never tells a user with allowance left that they must pay.
  • Honest, not precise. The "exceeds" copy states the fact (upload > remaining) and the outcome (needs payment) without predicting how much ends up free — the client can't know that (Turbo bills server-side, granularity unexposed, plus a second per-IP pool /v1/account/free doesn't report). True whether Turbo frees part of the upload or none of it.

Endpoint host confirmed against prod: GET payment.ardrive.io/v1/account/free200 {"bytesRemaining":10485760}.

Tests

733 passing. New coverage for the status-code → exception mapping, /v1/account/free parsing (including null = unlimited, 0 = off, unparseable = unknown/fail-open), the freeUploadStatusFor derivation (boundaries + fail-open), and a widget test asserting each status renders the right message. Core logic and widget both mutation-verified.

Notes for review

  • The three new free-tier l10n keys currently carry English text in all six locales (branch convention); Localizely handles translation as a separate pass.
  • /v1/account/free is placed on turboPaymentUri (verified in prod). Confirming with the Turbo team that the endpoint is a committed, stable contract would let us later tighten "exceeds your allowance" into an exact count — nothing depends on it otherwise.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW

Summary by CodeRabbit

  • New Features
    • Added server-driven Turbo free-upload eligibility (server max item size) plus a “free allowance used up” flow for Turbo 402/429.
    • Introduced Turbo payment-required dialogs with localized copy and a “Buy Credits” action.
    • Added “free tier status” messaging above upload method selection.
  • Bug Fixes
    • Improved payment-related error classification across create/rename/move, licensing, pinning, hide/unhide, upload, thumbnails, bulk import, and ArNS assignment so the correct dialog is shown.
    • Prevented blind retries for Turbo payment/rate-limit conditions.
  • Documentation
    • Added a Turbo free-tier implementation plan.
  • Tests
    • Added coverage for Turbo HTTP status-code → exception mapping and free-allowance parsing/behavior.

vilenarios and others added 2 commits July 15, 2026 20:20
10 MiB free pool per wallet, 105 KiB per-item eligibility, paid-only
after exhaustion, credits never replenish free. Inventories the 15
silent-free posting paths, the current failure behavior on payment
rejection, and phases the work: failure honesty (unblocked now),
pool-aware eligibility (needs turbo API contract), surfacing UX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…E-9132

Prepares the client for the restricted free tier (10 MiB pool, 105 KiB
per-item, paid-only after exhaustion). Policy-independent hardening:

- decode HTTP 402 into TurboPaymentRequiredException and 429 into
  TurboRateLimitException in the app-side TurboUploadService (pure
  turboExceptionForStatusCode mapping, unit tested); the uploader
  package maps 402 to its existing UnderFundException and excludes
  402/429 from its 8-attempt retry loops (retrying payment rejections
  multiplies load and metered usage)
- rename (file/folder) failures now dismiss the progress dialog and
  show an honest error - payment-specific copy when the rejection was
  402 (previously: spinner forever)
- move gains a failure state and dialog handling, no longer emits
  Success after an error (removes the TODO admitting it), and is
  reordered to post-then-commit: data items are prepared and posted
  BEFORE the local database transaction, so a rejected move can no
  longer leave local state claiming a move the chain never saw
- TurboUploadService fetches maxItemBytes from GET /v1/info once at
  construction (maxFreeItemSizeBytes, config fallback) - the
  server-driven per-item free threshold per the descoped plan; wiring
  it into UploadPaymentEvaluator is the next commit
- implementation plan doc updated with the decision: no pool tracking,
  no balance-endpoint dependency, static free-tier messaging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Turbo upload handling now decodes payment and rate-limit responses, uses server-provided free-item limits and wallet allowance status, propagates payment failures through operation states, prevents premature move database commits, records worker errors, supports migration transport fallback, and presents localized payment-specific dialogs.

Changes

Turbo free-tier handling

Layer / File(s) Summary
Typed Turbo errors and allowance contracts
lib/turbo/..., lib/core/upload/uploader.dart, packages/ardrive_uploader/...
HTTP 402, 408, and 429 responses map to typed exceptions, retries exclude payment and rate-limit failures, and upload eligibility uses server-reported limits plus structured allowance status.
Payment failure propagation
lib/blocs/..., lib/arns/..., lib/drive_explorer/..., lib/manifest/...
Failure states carry isPaymentError, including wrapped task and metadata-upload failures.
Upload and worker reporting
lib/blocs/upload/..., lib/core/arfs/..., lib/drive_explorer/thumbnail/..., packages/ardrive_utils/...
Upload paths classify payment failures, preserve worker exceptions, tolerate nonessential ArNS assignment failures, and complete thumbnail futures on finalization errors.
Move failure and persistence ordering
lib/blocs/fs_entry_move/*
Move operations emit explicit failures, post prepared entities before local mutation, and commit local records after network acceptance.
Payment-specific dialogs and localization
lib/components/..., lib/arns/..., lib/drive_explorer/..., lib/l10n/*.arb
Operation UIs distinguish payment failures from generic failures and provide localized allowance messaging with credits top-up actions.
Migration transport fallback
lib/shared/blocs/private_drive_migration/..., lib/pages/app_router_delegate.dart
Private drive migration posts through Turbo when enabled and otherwise submits an Arweave data bundle transaction.
Free-tier implementation plan
docs/implementation_plan_turbo_free_tier.md
The document records the free-tier policy, Phase 1 scope, server-driven eligibility decision, superseded future phases, non-goals, and sequencing.
Allowance and exception validation
test/turbo/..., test/core/upload/..., test/blocs/...
Tests cover typed HTTP mappings, allowance parsing and decisions, and free-versus-paid upload behavior.

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

Possibly related PRs

Suggested reviewers: arielmelendez

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: Turbo free-tier support, allowance-aware upload UX, and typed payment failures.
✨ 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 feat/turbo-free-tier

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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit a60a001):

https://ardrive-web--pr2166-feat-turbo-free-tier-7cde8kwx.web.app

(expires Thu, 30 Jul 2026 20:35:00 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: a224ebaee2f0939e7665e7630e7d3d6cd7d0f8b0

vilenarios and others added 2 commits July 16, 2026 11:53
…hold PE-9132

Completes the free-tier client readiness: every operation that posts to
Turbo now recognizes a payment rejection and shows one consistent,
actionable message instead of a generic error or (previously) a hang.

- new shared TurboPaymentRequired dialog (ArDriveStandardModalNew with a
  "Buy Credits" action into the existing top-up flow) as the single
  source of truth for the free-allowance-used-up UX; localized strings
  freeAllowanceUsedUpTitle/Description added to all six ARB files
- new isTurboPaymentError() classifier; each op's failure state carries
  an isPaymentError flag set from the caught exception:
  rename, move, drive rename, folder create, drive create, hide/unhide,
  pin, license, ghost fixer
- every corresponding form/dialog branches to the shared payment dialog
  on payment errors and keeps its existing generic error otherwise;
  folder-create, drive-rename and ghost-fixer gained the failure
  handling they previously lacked
- UploadPaymentEvaluator now resolves the free per-item threshold from
  the server (TurboUploadService.maxFreeItemSizeBytes via /v1/info),
  falling back to allowedDataItemSizeForTurbo; wired through DI for the
  main upload flow (metadata/manifest paths keep the config fallback)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vilenarios
vilenarios marked this pull request as ready for review July 16, 2026 16:17

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/blocs/fs_entry_move/fs_entry_move_bloc.dart (1)

274-310: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Handle partial Turbo upload success before deferring every local commit.

postDataItem writes each item independently. If item N fails after earlier items succeeded, those moves exist remotely, but the transaction at Lines 293-310 commits none locally. Retrying then publishes duplicate move revisions from stale local state.

Persist each successfully accepted Turbo item, track resumable progress, or use an atomic batch operation before treating this as all-or-nothing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/blocs/fs_entry_move/fs_entry_move_bloc.dart` around lines 274 - 310,
Update the Turbo upload path in the move transaction flow around postDataItem so
partial success is recoverable: persist each successfully accepted item or
equivalent resumable progress before continuing, or replace the per-item calls
with an atomic batch operation. Ensure a failure after item N does not leave
earlier remote moves absent from local state or cause retries to publish
duplicate move revisions, while preserving the existing local commit behavior
for successful non-Turbo uploads.
🧹 Nitpick comments (1)
lib/turbo/services/upload_service.dart (1)

228-235: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename one of the TurboRateLimitException types — the same class name is defined in both lib/turbo/services/upload_service.dart and packages/ardrive_uploader/lib/src/exceptions.dart, which makes unprefixed is TurboRateLimitException checks easy to confuse when both libraries are in scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/turbo/services/upload_service.dart` around lines 228 - 235, Rename the
TurboRateLimitException declaration in upload_service.dart to a distinct,
upload-service-specific exception name, and update all references in that file
and related upload handling to use the new name. Keep the existing rate-limit
semantics and leave packages/ardrive_uploader’s TurboRateLimitException
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/implementation_plan_turbo_free_tier.md`:
- Line 23: Update the multi-item move implementation around
fs_entry_move_bloc.dart’s move flow to define and implement explicit
partial-failure semantics, choosing atomic, resumable, or rollback behavior for
cases where an earlier item commits locally and a later remote post fails.
Document the chosen contract in the implementation plan and add tests covering
the partial-failure scenario.

In `@lib/blocs/drive_create/drive_create_state.dart`:
- Around line 54-60: Update the failure-state equality props to include
isPaymentError in DriveCreateFailure at
lib/blocs/drive_create/drive_create_state.dart:54-60, DriveRenameFailure at
lib/blocs/drive_rename/drive_rename_state.dart:16-18, and FolderCreateFailure at
lib/blocs/folder_create/folder_create_state.dart:15-18, preserving each state’s
existing equality fields.

In `@lib/blocs/fs_entry_license/fs_entry_license_bloc.dart`:
- Around line 165-168: Update the catch block in the licensing flow to include
the captured error in the addError call, preserving the original exception type
and message alongside the existing context and trace. Keep the
FsEntryLicenseFailure emission and isTurboPaymentError(error) handling
unchanged.

In `@lib/blocs/ghost_fixer/ghost_fixer_state.dart`:
- Around line 41-44: Include isPaymentError in Equatable equality for
GhostFixerFailure in lib/blocs/ghost_fixer/ghost_fixer_state.dart lines 41-44 by
overriding props with the superclass properties plus isPaymentError. Apply the
same props override to the corresponding hide failure state in
lib/blocs/hide/hide_state.dart lines 68-73.

In `@lib/components/drive_rename_form.dart`:
- Around line 94-95: Localize the hardcoded error descriptions by adding
corresponding ARB entries under lib/l10n/ and retrieving them through
appLocalizationsOf(context), matching the existing localized dialog-title
pattern. Apply this to lib/components/drive_rename_form.dart lines 94-95,
lib/components/folder_create_form.dart lines 92-93,
lib/components/fs_entry_rename_form.dart lines 112-113, and
lib/components/ghost_fixer_form.dart lines 84-85, preserving each message’s
meaning.

In `@lib/components/fs_entry_license_form.dart`:
- Around line 572-586: Update the payment-error UI in the licensing form so it
offers a recovery action for adding Credits instead of the existing immediate
Try Again retry. When state.isPaymentError is true, invoke the shared Turbo
payment dialog or reuse the established Add Credits action; preserve the current
retry behavior for non-payment failures.

In `@lib/l10n/app_es.arb`:
- Around line 172-173: Translate both free-tier message values in
lib/l10n/app_es.arb lines 172-173 into Spanish, and translate the corresponding
values in lib/l10n/app_hi.arb lines 172-173 into Hindi; preserve the existing
ARB keys and formatting.

In `@lib/l10n/app_ja.arb`:
- Around line 172-173: Replace the English values for freeAllowanceUsedUpTitle
and freeAllowanceUsedUpDescription in app_ja.arb with reviewed, natural Japanese
translations, preserving both message keys and their payment-related meaning.

In `@lib/l10n/app_zh-HK.arb`:
- Around line 172-173: Localize freeAllowanceUsedUpTitle and
freeAllowanceUsedUpDescription in lib/l10n/app_zh-HK.arb lines 172-173 with
Traditional Chinese (Hong Kong) text, and apply Simplified Chinese translations
for the same keys in lib/l10n/app_zh.arb lines 172-173; replace the English
fallback values while preserving the existing ARB keys and structure.

---

Outside diff comments:
In `@lib/blocs/fs_entry_move/fs_entry_move_bloc.dart`:
- Around line 274-310: Update the Turbo upload path in the move transaction flow
around postDataItem so partial success is recoverable: persist each successfully
accepted item or equivalent resumable progress before continuing, or replace the
per-item calls with an atomic batch operation. Ensure a failure after item N
does not leave earlier remote moves absent from local state or cause retries to
publish duplicate move revisions, while preserving the existing local commit
behavior for successful non-Turbo uploads.

---

Nitpick comments:
In `@lib/turbo/services/upload_service.dart`:
- Around line 228-235: Rename the TurboRateLimitException declaration in
upload_service.dart to a distinct, upload-service-specific exception name, and
update all references in that file and related upload handling to use the new
name. Keep the existing rate-limit semantics and leave
packages/ardrive_uploader’s TurboRateLimitException unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8d07440-f6a6-435b-9599-96bbb1bcd997

📥 Commits

Reviewing files that changed from the base of the PR and between 0860808 and 047645e.

📒 Files selected for processing (41)
  • docs/implementation_plan_turbo_free_tier.md
  • lib/blocs/drive_create/drive_create_cubit.dart
  • lib/blocs/drive_create/drive_create_state.dart
  • lib/blocs/drive_rename/drive_rename_cubit.dart
  • lib/blocs/drive_rename/drive_rename_state.dart
  • lib/blocs/folder_create/folder_create_cubit.dart
  • lib/blocs/folder_create/folder_create_state.dart
  • lib/blocs/fs_entry_license/fs_entry_license_bloc.dart
  • lib/blocs/fs_entry_license/fs_entry_license_state.dart
  • lib/blocs/fs_entry_move/fs_entry_move_bloc.dart
  • lib/blocs/fs_entry_move/fs_entry_move_state.dart
  • lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart
  • lib/blocs/fs_entry_rename/fs_entry_rename_state.dart
  • lib/blocs/ghost_fixer/ghost_fixer_cubit.dart
  • lib/blocs/ghost_fixer/ghost_fixer_state.dart
  • lib/blocs/hide/hide_bloc.dart
  • lib/blocs/hide/hide_state.dart
  • lib/blocs/pin_file/pin_file_bloc.dart
  • lib/blocs/pin_file/pin_file_state.dart
  • lib/components/drive_create_form.dart
  • lib/components/drive_rename_form.dart
  • lib/components/folder_create_form.dart
  • lib/components/fs_entry_license_form.dart
  • lib/components/fs_entry_move_form.dart
  • lib/components/fs_entry_rename_form.dart
  • lib/components/ghost_fixer_form.dart
  • lib/components/hide_dialog.dart
  • lib/components/pin_file_dialog.dart
  • lib/components/turbo_payment_required_dialog.dart
  • lib/core/upload/uploader.dart
  • lib/l10n/app_en.arb
  • lib/l10n/app_es.arb
  • lib/l10n/app_hi.arb
  • lib/l10n/app_ja.arb
  • lib/l10n/app_zh-HK.arb
  • lib/l10n/app_zh.arb
  • lib/turbo/services/upload_service.dart
  • lib/utils/dependency_injection_utils.dart
  • packages/ardrive_uploader/lib/src/exceptions.dart
  • packages/ardrive_uploader/lib/src/turbo_upload_service.dart
  • test/turbo/services/turbo_exception_mapping_test.dart

|---|---|
| File/folder rename | `lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart:176/:101` |
| Drive rename | `lib/blocs/drive_rename/drive_rename_cubit.dart:71` |
| Move (per item!) | `lib/blocs/fs_entry_move/fs_entry_move_bloc.dart:275` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define partial-failure semantics for multi-item moves.

The inventory says moves post one item at a time. Posting before committing prevents one-item divergence, but item 1 can still commit locally while item 2 fails remotely. Specify whether the operation is atomic, resumable, or rolled back, and test that behavior.

Also applies to: 63-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/implementation_plan_turbo_free_tier.md` at line 23, Update the
multi-item move implementation around fs_entry_move_bloc.dart’s move flow to
define and implement explicit partial-failure semantics, choosing atomic,
resumable, or rollback behavior for cases where an earlier item commits locally
and a later remote post fails. Document the chosen contract in the
implementation plan and add tests covering the partial-failure scenario.

Comment on lines +54 to +60
final bool isPaymentError;
const DriveCreateFailure({required super.privacy, this.isPaymentError = false});

@override
DriveCreateFailure copyWith({DrivePrivacy? privacy}) {
return DriveCreateFailure(privacy: privacy ?? this.privacy);
return DriveCreateFailure(
privacy: privacy ?? this.privacy, isPaymentError: isPaymentError);

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

Include isPaymentError in failure-state equality.

The new payment classification is not included in Equatable equality, so payment and non-payment failures can compare equal and suppress distinct state transitions.

  • lib/blocs/drive_create/drive_create_state.dart#L54-L60: override failure-state props to include isPaymentError.
  • lib/blocs/drive_rename/drive_rename_state.dart#L16-L18: override failure-state props to include isPaymentError.
  • lib/blocs/folder_create/folder_create_state.dart#L15-L18: override failure-state props to include isPaymentError.
📍 Affects 3 files
  • lib/blocs/drive_create/drive_create_state.dart#L54-L60 (this comment)
  • lib/blocs/drive_rename/drive_rename_state.dart#L16-L18
  • lib/blocs/folder_create/folder_create_state.dart#L15-L18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/blocs/drive_create/drive_create_state.dart` around lines 54 - 60, Update
the failure-state equality props to include isPaymentError in DriveCreateFailure
at lib/blocs/drive_create/drive_create_state.dart:54-60, DriveRenameFailure at
lib/blocs/drive_rename/drive_rename_state.dart:16-18, and FolderCreateFailure at
lib/blocs/folder_create/folder_create_state.dart:15-18, preserving each state’s
existing equality fields.

Comment on lines +165 to +168
} catch (error, trace) {
addError('Error licensing entities', trace);
emit(const FsEntryLicenseFailure());
emit(FsEntryLicenseFailure(
isPaymentError: isTurboPaymentError(error)));

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 | 🟡 Minor | ⚡ Quick win

Preserve the original exception in diagnostics.

The catch block captures error, but addError records only 'Error licensing entities'. The Bloc’s error log therefore loses the exception type and message, making payment/network failures difficult to diagnose.

-      addError('Error licensing entities', trace);
+      addError(error, trace);

Based on the changed exception-handling path, the original error is available but discarded from diagnostics.

📝 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
} catch (error, trace) {
addError('Error licensing entities', trace);
emit(const FsEntryLicenseFailure());
emit(FsEntryLicenseFailure(
isPaymentError: isTurboPaymentError(error)));
} catch (error, trace) {
addError(error, trace);
emit(FsEntryLicenseFailure(
isPaymentError: isTurboPaymentError(error)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/blocs/fs_entry_license/fs_entry_license_bloc.dart` around lines 165 -
168, Update the catch block in the licensing flow to include the captured error
in the addError call, preserving the original exception type and message
alongside the existing context and trace. Keep the FsEntryLicenseFailure
emission and isTurboPaymentError(error) handling unchanged.

Comment on lines +41 to +44
class GhostFixerFailure extends GhostFixerState {
final bool isPaymentError;
GhostFixerFailure({this.isPaymentError = 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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include the new payment flags in Equatable equality.

Both state subclasses add isPaymentError without overriding props, so payment and non-payment failures can compare equal and suppress state updates.

  • lib/blocs/ghost_fixer/ghost_fixer_state.dart#L41-L44: override props with [...super.props, isPaymentError].
  • lib/blocs/hide/hide_state.dart#L68-L73: override props with [...super.props, isPaymentError].
📍 Affects 2 files
  • lib/blocs/ghost_fixer/ghost_fixer_state.dart#L41-L44 (this comment)
  • lib/blocs/hide/hide_state.dart#L68-L73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/blocs/ghost_fixer/ghost_fixer_state.dart` around lines 41 - 44, Include
isPaymentError in Equatable equality for GhostFixerFailure in
lib/blocs/ghost_fixer/ghost_fixer_state.dart lines 41-44 by overriding props
with the superclass properties plus isPaymentError. Apply the same props
override to the corresponding hide failure state in
lib/blocs/hide/hide_state.dart lines 68-73.

Comment thread lib/components/drive_rename_form.dart Outdated
Comment thread lib/components/fs_entry_license_form.dart
Comment thread lib/l10n/app_es.arb
Comment on lines +172 to +173
"freeAllowanceUsedUpTitle": "Free allowance used up",
"freeAllowanceUsedUpDescription": "Your free upload allowance has been used up, so this action now requires Credits. Add Credits and try again.",

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

Translate the new free-tier messages in every locale.

The new entries are English in both localized ARB files, causing Spanish and Hindi users to see untranslated payment-required messaging.

  • lib/l10n/app_es.arb#L172-L173: replace both values with Spanish translations.
  • lib/l10n/app_hi.arb#L172-L173: replace both values with Hindi translations.
📍 Affects 2 files
  • lib/l10n/app_es.arb#L172-L173 (this comment)
  • lib/l10n/app_hi.arb#L172-L173
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/l10n/app_es.arb` around lines 172 - 173, Translate both free-tier message
values in lib/l10n/app_es.arb lines 172-173 into Spanish, and translate the
corresponding values in lib/l10n/app_hi.arb lines 172-173 into Hindi; preserve
the existing ARB keys and formatting.

Source: Coding guidelines

Comment thread lib/l10n/app_ja.arb
Comment on lines +172 to +173
"freeAllowanceUsedUpTitle": "Free allowance used up",
"freeAllowanceUsedUpDescription": "Your free upload allowance has been used up, so this action now requires Credits. Add Credits and try again.",

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

Provide Japanese translations for the new messages.

These values are English in app_ja.arb, so Japanese users will see untranslated payment-required dialog text. Replace both strings with reviewed Japanese translations.

As per coding guidelines, lib/l10n/** must provide localized ARB content for the supported languages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/l10n/app_ja.arb` around lines 172 - 173, Replace the English values for
freeAllowanceUsedUpTitle and freeAllowanceUsedUpDescription in app_ja.arb with
reviewed, natural Japanese translations, preserving both message keys and their
payment-related meaning.

Source: Coding guidelines

Comment thread lib/l10n/app_zh-HK.arb
Comment on lines +172 to +173
"freeAllowanceUsedUpTitle": "Free allowance used up",
"freeAllowanceUsedUpDescription": "Your free upload allowance has been used up, so this action now requires Credits. Add Credits and try again.",

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

Localize the new free-allowance messages for both Chinese locales.

Both Chinese ARB files currently display the English fallback text.

  • lib/l10n/app_zh-HK.arb#L172-L173: add Traditional Chinese (Hong Kong) translations.
  • lib/l10n/app_zh.arb#L172-L173: add Simplified Chinese translations.

As per coding guidelines, localization must support Simplified and Hong Kong Chinese through ARB files in lib/l10n.

📍 Affects 2 files
  • lib/l10n/app_zh-HK.arb#L172-L173 (this comment)
  • lib/l10n/app_zh.arb#L172-L173
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/l10n/app_zh-HK.arb` around lines 172 - 173, Localize
freeAllowanceUsedUpTitle and freeAllowanceUsedUpDescription in
lib/l10n/app_zh-HK.arb lines 172-173 with Traditional Chinese (Hong Kong) text,
and apply Simplified Chinese translations for the same keys in
lib/l10n/app_zh.arb lines 172-173; replace the English fallback values while
preserving the existing ARB keys and structure.

Source: Coding guidelines

- include isPaymentError in Equatable props on every failure state
  (drive/folder create, drive/folder/file rename, hide, ghost fixer,
  license) so a payment failure emitted after a generic one is not
  treated as an equal state and actually re-triggers the listener
- license: preserve the original exception via logger.e before addError
- license failure card: on a payment error the action becomes "Buy
  Credits" (opens the shared payment dialog) instead of retrying the
  same rejected operation
- localize the generic metadata-op failure message via a shared
  actionFailedTryAgain key across all locales, replacing hardcoded
  English descriptions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vilenarios

Copy link
Copy Markdown
Collaborator Author

Addressed the review findings in f40205b:

  • Equatable propsisPaymentError now included in props on all failure states (the highest-impact one: without it a payment failure emitted after a generic failure compared equal and the dialog wouldn't re-trigger).
  • License — original exception preserved via logger.e; the failure card's action becomes "Buy Credits" → shared payment dialog on payment errors instead of retrying the rejected op.
  • Localization — hardcoded generic error strings replaced with a shared actionFailedTryAgain key across all locales.

Deferred (tracked, not in this PR):

  • Multi-item move partial-failure semantics (the Major): if one item in a multi-item move is rejected mid-batch, earlier items are already on-chain. This PR's post-then-commit reorder already removes the local-state corruption risk (nothing commits locally unless the network accepted it). True transactional/resumable multi-item moves are part of the burst-preflight follow-up noted in docs/implementation_plan_turbo_free_tier.md, not this PR's scope.
  • Per-locale translations: new keys ship as English interim and are translated via the Localizely pipeline (localizely.yml), per repo convention.

…t paths PE-9132

Audit found the metadata ops were covered but the highest-visibility
upload surfaces were not. Closes those gaps.

Cross-cutting fix — the two upload services throw different payment
exceptions (app-side TurboPaymentRequiredException vs ardrive_uploader
package UnderFundException, sometimes wrapped in UploadStrategyException).
isTurboPaymentError() now recognizes all of them; ardrive_uploader
exports its exceptions so the app can classify them.

Newly covered:
- main file upload and folder upload: UploadCubit classifies a payment
  rejection from the failed-task list into UploadErrors.turboPaymentRequired
  (UploadFailure gains isPaymentError-carrying props); the failure widget
  shows the Buy-Credits dialog instead of a "Re-Upload" that would 402 again
- post-upload ArNS name assignment: wrapped in try/catch so a rejected
  name data item can no longer hang an already-successful upload (the
  name can be reassigned later)
- snapshot creation, manifest creation (unwrapping the task-list error
  through ManifestCreationException), standalone ArNS assignment, bulk
  import (unwrapping FileMetadataUploadException.originalError), and
  single/multi thumbnail creation all classify payment errors and show
  the shared dialog

Deferred (documented): private-drive migration and login verification
posts — tiny, effectively always-free signature items where a payment
dialog mid-flow would be worse UX than the near-impossible failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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

♻️ Duplicate comments (1)
lib/l10n/app_ja.arb (1)

172-174: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Provide Japanese translations for the new messages.

These values are English in app_ja.arb, so Japanese users will see untranslated text. Replace these strings with natural Japanese translations.

🌐 Proposed translations
-  "actionFailedTryAgain": "Something went wrong. Please check your connection and try again.",
-  "freeAllowanceUsedUpTitle": "Free allowance used up",
-  "freeAllowanceUsedUpDescription": "Your free upload allowance has been used up, so this action now requires Credits. Add Credits and try again.",
+  "actionFailedTryAgain": "問題が発生しました。接続を確認してもう一度お試しください。",
+  "freeAllowanceUsedUpTitle": "無料の許容量を使い切りました",
+  "freeAllowanceUsedUpDescription": "無料のアップロード許容量を使い切ったため、この操作にはクレジットが必要です。クレジットを追加して、もう一度お試しください。",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/l10n/app_ja.arb` around lines 172 - 174, Replace the English values for
actionFailedTryAgain, freeAllowanceUsedUpTitle, and
freeAllowanceUsedUpDescription in app_ja.arb with natural Japanese translations,
preserving the existing message meanings and ARB structure.
🧹 Nitpick comments (2)
lib/arns/presentation/assign_name_modal.dart (1)

421-436: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Hide the "Try again" action for payment errors.

Since the operation cannot succeed without adding credits, the "Try again" button should be hidden when a payment error occurs. This aligns with the PR objective to avoid retrying operations that have been rejected due to insufficient credits.

♻️ Proposed fix
     if (state is SelectionFailed) {
       return [
         ModalAction(
           action: () {
             Navigator.of(context).pop();
           },
           title: 'Cancel',
         ),
-        ModalAction(
-          action: () {
-            context.read<AssignNameBloc>().add(ConfirmSelectionAndUpload());
-          },
-          title: 'Try again',
-        ),
+        if (!state.isPaymentError)
+          ModalAction(
+            action: () {
+              context.read<AssignNameBloc>().add(ConfirmSelectionAndUpload());
+            },
+            title: 'Try again',
+          ),
       ];
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/arns/presentation/assign_name_modal.dart` around lines 421 - 436, Update
the SelectionFailed action construction in the assign-name modal to detect
payment or insufficient-credit errors and omit the “Try again” ModalAction for
those failures. Preserve the Cancel action and existing retry behavior for other
SelectionFailed cases.
lib/blocs/create_manifest/create_manifest_state.dart (1)

230-236: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use const constructors for state object instantiations.

When adding the isPaymentError field to these state classes, the const modifier was omitted from their constructors. Based on learnings, you should prefer using const constructors whenever the constructor is declared as const and all arguments are compile-time constants. This enables canonicalized instances and potential compile-time optimizations.

  • lib/blocs/create_manifest/create_manifest_state.dart#L230-L236: Add const to CreateManifestFailure({this.isPaymentError = false});.
  • lib/drive_explorer/multi_thumbnail_creation/bloc/multi_thumbnail_creation_state.dart#L73-L77: Add const to MultiThumbnailCreationError({this.isPaymentError = false});.
  • lib/drive_explorer/thumbnail_creation/bloc/thumbnail_creation_state.dart#L16-L22: Add const to ThumbnailCreationError({this.isPaymentError = false});.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/blocs/create_manifest/create_manifest_state.dart` around lines 230 - 236,
Update the constructors for CreateManifestFailure in
lib/blocs/create_manifest/create_manifest_state.dart (lines 230-236),
MultiThumbnailCreationError in
lib/drive_explorer/multi_thumbnail_creation/bloc/multi_thumbnail_creation_state.dart
(lines 73-77), and ThumbnailCreationError in
lib/drive_explorer/thumbnail_creation/bloc/thumbnail_creation_state.dart (lines
16-22) to be const, preserving their existing isPaymentError defaults and props
behavior.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/arns/presentation/assign_name_modal.dart`:
- Around line 347-351: Remove the showTurboPaymentRequiredDialog call and
post-frame callback from the builder’s state.isPaymentError branch, and handle
the payment-error side effect in the BlocConsumer listener instead. In the
listener, detect SelectionFailed states with isPaymentError and invoke
showTurboPaymentRequiredDialog(context) once per emitted state.

In `@lib/components/create_snapshot_dialog.dart`:
- Around line 97-103: Move payment-error dialog side effects from each
BlocConsumer builder into its listener: in
lib/components/create_snapshot_dialog.dart#L97-L103, handle
SnapshotUploadFailure with isPaymentError by popping the current dialog and
showing showTurboPaymentRequiredDialog; in
lib/components/create_manifest_form.dart#L175-L180, move the
CreateManifestFailure payment handling and existing Navigator.pop; and in
lib/drive_explorer/multi_thumbnail_creation/multi_thumbnail_creation_modal.dart#L164-L169,
handle the MultiThumbnailCreationError payment case in the listener by popping
and showing the payment dialog. Remove these builder branches so each builder
falls back to its normal content or close behavior without returning an empty
modal.

In `@lib/drive_explorer/thumbnail_creation/page/thumbnail_creation_modal.dart`:
- Around line 52-58: Move showTurboPaymentRequiredDialog from the BlocConsumer
builder into its listener, triggering it when the payment-error state changes,
and leave the builder responsible only for returning the free-allowance
description. Also inspect related components such as CreateSnapshotDialog and
CreateManifestForm for dialog or Navigator.pop calls inside builders, moving
those side effects into their listeners while preserving their existing
state-dependent UI.

In `@lib/l10n/app_zh.arb`:
- Around line 172-174: Replace the English values for actionFailedTryAgain,
freeAllowanceUsedUpTitle, and freeAllowanceUsedUpDescription in app_zh.arb with
natural Chinese translations, preserving the existing keys and message meanings.

---

Duplicate comments:
In `@lib/l10n/app_ja.arb`:
- Around line 172-174: Replace the English values for actionFailedTryAgain,
freeAllowanceUsedUpTitle, and freeAllowanceUsedUpDescription in app_ja.arb with
natural Japanese translations, preserving the existing message meanings and ARB
structure.

---

Nitpick comments:
In `@lib/arns/presentation/assign_name_modal.dart`:
- Around line 421-436: Update the SelectionFailed action construction in the
assign-name modal to detect payment or insufficient-credit errors and omit the
“Try again” ModalAction for those failures. Preserve the Cancel action and
existing retry behavior for other SelectionFailed cases.

In `@lib/blocs/create_manifest/create_manifest_state.dart`:
- Around line 230-236: Update the constructors for CreateManifestFailure in
lib/blocs/create_manifest/create_manifest_state.dart (lines 230-236),
MultiThumbnailCreationError in
lib/drive_explorer/multi_thumbnail_creation/bloc/multi_thumbnail_creation_state.dart
(lines 73-77), and ThumbnailCreationError in
lib/drive_explorer/thumbnail_creation/bloc/thumbnail_creation_state.dart (lines
16-22) to be const, preserving their existing isPaymentError defaults and props
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6cab44dc-0d6e-42dc-b7a7-da69460c0d2b

📥 Commits

Reviewing files that changed from the base of the PR and between 047645e and dd38535.

📒 Files selected for processing (44)
  • lib/arns/presentation/assign_name_bloc/assign_name_bloc.dart
  • lib/arns/presentation/assign_name_bloc/assign_name_state.dart
  • lib/arns/presentation/assign_name_modal.dart
  • lib/blocs/bulk_import/bulk_import_bloc.dart
  • lib/blocs/bulk_import/bulk_import_state.dart
  • lib/blocs/create_manifest/create_manifest_cubit.dart
  • lib/blocs/create_manifest/create_manifest_state.dart
  • lib/blocs/create_snapshot/create_snapshot_cubit.dart
  • lib/blocs/create_snapshot/create_snapshot_state.dart
  • lib/blocs/drive_create/drive_create_state.dart
  • lib/blocs/drive_rename/drive_rename_state.dart
  • lib/blocs/folder_create/folder_create_state.dart
  • lib/blocs/fs_entry_license/fs_entry_license_bloc.dart
  • lib/blocs/fs_entry_license/fs_entry_license_state.dart
  • lib/blocs/fs_entry_rename/fs_entry_rename_state.dart
  • lib/blocs/ghost_fixer/ghost_fixer_state.dart
  • lib/blocs/hide/hide_state.dart
  • lib/blocs/upload/upload_cubit.dart
  • lib/blocs/upload/upload_state.dart
  • lib/components/create_manifest_form.dart
  • lib/components/create_snapshot_dialog.dart
  • lib/components/drive_rename_form.dart
  • lib/components/folder_create_form.dart
  • lib/components/fs_entry_license_form.dart
  • lib/components/fs_entry_move_form.dart
  • lib/components/fs_entry_rename_form.dart
  • lib/components/ghost_fixer_form.dart
  • lib/components/upload_form.dart
  • lib/drive_explorer/multi_thumbnail_creation/bloc/multi_thumbnail_creation_bloc.dart
  • lib/drive_explorer/multi_thumbnail_creation/bloc/multi_thumbnail_creation_state.dart
  • lib/drive_explorer/multi_thumbnail_creation/multi_thumbnail_creation_modal.dart
  • lib/drive_explorer/thumbnail_creation/bloc/thumbnail_creation_bloc.dart
  • lib/drive_explorer/thumbnail_creation/bloc/thumbnail_creation_state.dart
  • lib/drive_explorer/thumbnail_creation/page/thumbnail_creation_modal.dart
  • lib/l10n/app_en.arb
  • lib/l10n/app_es.arb
  • lib/l10n/app_hi.arb
  • lib/l10n/app_ja.arb
  • lib/l10n/app_zh-HK.arb
  • lib/l10n/app_zh.arb
  • lib/manifest/domain/manifest_repository.dart
  • lib/pages/drive_detail/components/bulk_import_modal.dart
  • lib/turbo/services/upload_service.dart
  • packages/ardrive_uploader/lib/ardrive_uploader.dart
🚧 Files skipped from review as they are similar to previous changes (15)
  • lib/blocs/fs_entry_license/fs_entry_license_state.dart
  • lib/l10n/app_hi.arb
  • lib/l10n/app_es.arb
  • lib/blocs/ghost_fixer/ghost_fixer_state.dart
  • lib/blocs/drive_rename/drive_rename_state.dart
  • lib/blocs/folder_create/folder_create_state.dart
  • lib/blocs/drive_create/drive_create_state.dart
  • lib/components/folder_create_form.dart
  • lib/l10n/app_zh-HK.arb
  • lib/blocs/hide/hide_state.dart
  • lib/components/ghost_fixer_form.dart
  • lib/components/fs_entry_move_form.dart
  • lib/blocs/fs_entry_license/fs_entry_license_bloc.dart
  • lib/components/drive_rename_form.dart
  • lib/components/fs_entry_rename_form.dart

Comment thread lib/arns/presentation/assign_name_modal.dart Outdated
Comment thread lib/components/create_snapshot_dialog.dart Outdated
Comment thread lib/l10n/app_zh.arb
Comment on lines +172 to +174
"actionFailedTryAgain": "Something went wrong. Please check your connection and try again.",
"freeAllowanceUsedUpTitle": "Free allowance used up",
"freeAllowanceUsedUpDescription": "Your free upload allowance has been used up, so this action now requires Credits. Add Credits and try again.",

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

Provide Chinese translations for the new messages.

These values are English in app_zh.arb, so Chinese users will see untranslated text. Replace these strings with natural Chinese translations.

🌐 Proposed translations
-  "actionFailedTryAgain": "Something went wrong. Please check your connection and try again.",
-  "freeAllowanceUsedUpTitle": "Free allowance used up",
-  "freeAllowanceUsedUpDescription": "Your free upload allowance has been used up, so this action now requires Credits. Add Credits and try again.",
+  "actionFailedTryAgain": "出现问题。请检查您的连接并重试。",
+  "freeAllowanceUsedUpTitle": "免费额度已用完",
+  "freeAllowanceUsedUpDescription": "您的免费上传额度已用完,因此此操作现在需要积分。请添加积分并重试。",
📝 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
"actionFailedTryAgain": "Something went wrong. Please check your connection and try again.",
"freeAllowanceUsedUpTitle": "Free allowance used up",
"freeAllowanceUsedUpDescription": "Your free upload allowance has been used up, so this action now requires Credits. Add Credits and try again.",
"actionFailedTryAgain": "出现问题。请检查您的连接并重试。",
"freeAllowanceUsedUpTitle": "免费额度已用完",
"freeAllowanceUsedUpDescription": "您的免费上传额度已用完,因此此操作现在需要积分。请添加积分并重试。",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/l10n/app_zh.arb` around lines 172 - 174, Replace the English values for
actionFailedTryAgain, freeAllowanceUsedUpTitle, and
freeAllowanceUsedUpDescription in app_zh.arb with natural Chinese translations,
preserving the existing keys and message meanings.

vilenarios and others added 3 commits July 17, 2026 14:07
- hide the ardrive_uploader package's TurboUploadTimeoutException /
  TurboRateLimitException in upload_cubit (they collide with the app-side
  classes of the same name now that the package exports its exceptions);
  the app-side types are the ones _emitError intends
- add missing app_localizations imports to assign_name and thumbnail
  creation modals
- drop the unused shared-dialog import in upload_form (its failure widget
  builds the payment modal inline)
- const the thumbnail error state constructors
- remove now-redundant direct exceptions.dart imports in three
  ardrive_uploader files (the barrel provides them)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…E-9132

Verification audit found my earlier thumbnail/bulk-import fixes were in
the wrong place — the failure never reached the bloc catch I'd wired.

- thumbnail_repository: the upload controller's onError only logged and
  never completed the completer, so a 402 (which fires onError, not
  onDone) hung single AND multi thumbnail creation in Loading forever.
  onError now errors the completer, unwrapping the task list via
  anyTaskIsTurboPaymentError (previously dead) into the typed exception
  so the blocs classify it. The onDone body (which posts the thumbnail
  metadata data item) is also guarded so a payment rejection there errors
  the completer instead of hanging.
- bulk_import_bloc: the actual import-execution catch swallowed the error
  and emitted a const BulkImportError (isPaymentError always false), so a
  402 during real bulk import showed a generic dialog. The error is now
  captured and classified via _isBulkImportPaymentError (unwrapping
  FileMetadataUploadException.originalError).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final verification found multi-thumbnail and bulk import still swallowed
402s: the ardrive_utils WorkerPool caught task exceptions in
Worker._execute and passed only the task (not the exception) to
onWorkerError, so the pool completed normally and the awaiting bloc
never saw the failure.

- WorkerPool.onWorkerError now receives the exception (Function(T, Object))
- multi thumbnail: onWorkerError captures a payment rejection into a flag
  and, after onAllTasksCompleted, emits MultiThumbnailCreationError(
  isPaymentError: true) instead of reporting completion
- bulk import: FileImportFailure now preserves originalError; the worker
  records failures into BulkImportResult (was: only logged); the bloc
  captures the result and classifies the terminal error from the
  failures' originalError as well as the outer catch
- fixes a scope bug: importResult is declared before the try so it is
  visible in the post-catch classification

This closes the last two surfaces; single-thumbnail and the other seven
were already verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/blocs/bulk_import/bulk_import_bloc.dart (1)

259-284: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix inaccurate failedFiles count and preserve swallowed errors.

There are two issues in this block:

  1. Inaccurate failure count on partial success: failedFiles is assigned from failedPaths. However, failedPaths is only populated if the overall _bulkImportFiles call throws an exception. When _bulkImportFiles completes with individual file failures, it returns them in importResult.failures without throwing. This results in failedPaths.length evaluating to 0, which incorrectly reports no failed files in BulkImportSuccess.
  2. Swallowed errors on total failure: When all files fail and _bulkImportFiles returns those failures in importResult.failures (meaning successfulFiles == 0), lastImportError evaluates to null. Passing null to BulkImportError swallows the root cause of the failure.

Use importResult.failures to accurately count failures and extract the original error.

🐛 Proposed fix
     final totalFiles = files.length;
     final successfulFiles = processedFiles;
-    final failedFiles = failedPaths;
+    final failedFilesCount = importResult?.failures.length ?? (totalFiles - successfulFiles);

     if (successfulFiles == 0) {
       final paymentError = _isBulkImportPaymentError(lastImportError) ||
           (importResult?.failures.any(
                   (f) => _isBulkImportPaymentError(f.originalError)) ??
               false);
+
+      Object? errorToReport = lastImportError;
+      if (errorToReport == null && importResult != null && importResult.failures.isNotEmpty) {
+        errorToReport = importResult.failures.first.originalError;
+      }
+
       emit(BulkImportError(
         paymentError
             ? 'Your free upload allowance has been used up. Add Credits to '
                 'continue importing.'
             : 'Failed to import any files. Please check the manifest and '
                 'try again.',
-        lastImportError,
+        errorToReport,
         paymentError,
       ));
     } else {
       emit(BulkImportSuccess(
         manifestTxId: manifestTxId,
         totalFiles: totalFiles,
         successfulFiles: successfulFiles,
-        failedFiles: failedFiles.length,
+        failedFiles: failedFilesCount,
       ));
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/blocs/bulk_import/bulk_import_bloc.dart` around lines 259 - 284, Update
the bulk import result handling around _bulkImportFiles, importResult, and the
successfulFiles branches to derive failedFiles from importResult.failures,
falling back to failedPaths only when appropriate. When all files fail and
returned failures exist, extract an original error from importResult.failures
and pass it to BulkImportError instead of leaving lastImportError null; preserve
thrown-error handling and payment-error detection.
🧹 Nitpick comments (3)
lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart (1)

234-241: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer try/catch over .catchError() in async functions.

In Dart, when working within an async function, using a try/catch block around the awaited Future is more idiomatic and robust than appending .catchError(). It also ensures that any synchronous exceptions thrown prior to the Future generation are safely caught.

♻️ Proposed refactor

Wrap the await _driveDao.transaction(...) call (starting on line 201) in a try block, and replace .catchError with a catch block at the end:

-      }).catchError((Object e) {
-        // A failure while posting the thumbnail metadata (e.g. a payment
-        // rejection) must error the completer, not hang the awaiting bloc.
-        logger.e('Error finalizing thumbnail upload', e);
-        if (!completer.isCompleted) {
-          completer.completeError(e);
-        }
-      });
+      });
+    } catch (e) {
+      // A failure while posting the thumbnail metadata (e.g. a payment
+      // rejection) must error the completer, not hang the awaiting bloc.
+      logger.e('Error finalizing thumbnail upload', e);
+      if (!completer.isCompleted) {
+        completer.completeError(e);
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart` around
lines 234 - 241, Replace the .catchError handler on the awaited
_driveDao.transaction call with a surrounding try/catch in the containing async
method. Preserve the existing error logging and guarded
completer.completeError(e) behavior, while ensuring synchronous and asynchronous
transaction failures are both caught.
lib/core/arfs/use_cases/bulk_import_files.dart (1)

422-422: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid using StackTrace.current for logging caught exceptions.

Passing StackTrace.current to the logger inside an error callback captures the stack trace of the callback's execution, not the origin of the actual exception. This obscures the root cause and creates misleading logs during debugging. If the upstream API does not provide a stack trace, it is better to omit it entirely.

  • lib/core/arfs/use_cases/bulk_import_files.dart#L422-L422: Remove StackTrace.current from the logger.e call in onWorkerError.
  • lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart#L183-L184: Remove StackTrace.current from the logger.e call in controller.onError.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/core/arfs/use_cases/bulk_import_files.dart` at line 422, Remove
StackTrace.current from the logger.e call in onWorkerError in
lib/core/arfs/use_cases/bulk_import_files.dart at lines 422-422, and from the
logger.e call in controller.onError in
lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart at lines
183-184. Keep logging the existing error details without supplying a fabricated
stack trace.
packages/ardrive_utils/lib/src/worker.dart (1)

51-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider propagating StackTrace alongside the exception.

The onWorkerError callback successfully exposes the underlying Object error, but it lacks the original StackTrace. Propagating the stack trace here would significantly improve debuggability for consumers of WorkerPool and prevent them from resorting to StackTrace.current (which masks the original error's trace).

💡 Suggested enhancement

Consider updating WorkerPool and the internal Worker execution block to capture and pass the stack trace in a future iteration:

// In WorkerPool:
final Function(T, Object, [StackTrace?]) onWorkerError;

// In Worker (internal execute block):
} catch (e, st) {
  onError(task, e, st);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ardrive_utils/lib/src/worker.dart` at line 51, Update the WorkerPool
onWorkerError callback contract to accept the original StackTrace alongside the
task and error, then modify the internal Worker execution catch block to capture
the thrown trace and pass it through onError. Propagate the updated signature
consistently to all callback declarations and invocations while preserving
existing error handling behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@lib/blocs/bulk_import/bulk_import_bloc.dart`:
- Around line 259-284: Update the bulk import result handling around
_bulkImportFiles, importResult, and the successfulFiles branches to derive
failedFiles from importResult.failures, falling back to failedPaths only when
appropriate. When all files fail and returned failures exist, extract an
original error from importResult.failures and pass it to BulkImportError instead
of leaving lastImportError null; preserve thrown-error handling and
payment-error detection.

---

Nitpick comments:
In `@lib/core/arfs/use_cases/bulk_import_files.dart`:
- Line 422: Remove StackTrace.current from the logger.e call in onWorkerError in
lib/core/arfs/use_cases/bulk_import_files.dart at lines 422-422, and from the
logger.e call in controller.onError in
lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart at lines
183-184. Keep logging the existing error details without supplying a fabricated
stack trace.

In `@lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart`:
- Around line 234-241: Replace the .catchError handler on the awaited
_driveDao.transaction call with a surrounding try/catch in the containing async
method. Preserve the existing error logging and guarded
completer.completeError(e) behavior, while ensuring synchronous and asynchronous
transaction failures are both caught.

In `@packages/ardrive_utils/lib/src/worker.dart`:
- Line 51: Update the WorkerPool onWorkerError callback contract to accept the
original StackTrace alongside the task and error, then modify the internal
Worker execution catch block to capture the thrown trace and pass it through
onError. Propagate the updated signature consistently to all callback
declarations and invocations while preserving existing error handling behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3564f7e-cd63-4d62-a85a-33c2e4a280be

📥 Commits

Reviewing files that changed from the base of the PR and between dd38535 and db0443c.

📒 Files selected for processing (14)
  • lib/arns/presentation/assign_name_modal.dart
  • lib/blocs/bulk_import/bulk_import_bloc.dart
  • lib/blocs/upload/upload_cubit.dart
  • lib/components/upload_form.dart
  • lib/core/arfs/use_cases/bulk_import_files.dart
  • lib/drive_explorer/multi_thumbnail_creation/bloc/multi_thumbnail_creation_bloc.dart
  • lib/drive_explorer/multi_thumbnail_creation/bloc/multi_thumbnail_creation_state.dart
  • lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart
  • lib/drive_explorer/thumbnail_creation/bloc/thumbnail_creation_state.dart
  • lib/drive_explorer/thumbnail_creation/page/thumbnail_creation_modal.dart
  • packages/ardrive_uploader/lib/src/turbo_streamed_upload.dart
  • packages/ardrive_uploader/lib/src/upload_controller.dart
  • packages/ardrive_uploader/lib/src/upload_strategy.dart
  • packages/ardrive_utils/lib/src/worker.dart
💤 Files with no reviewable changes (4)
  • packages/ardrive_uploader/lib/src/upload_controller.dart
  • packages/ardrive_uploader/lib/src/turbo_streamed_upload.dart
  • packages/ardrive_uploader/lib/src/upload_strategy.dart
  • lib/components/upload_form.dart
🚧 Files skipped from review as they are similar to previous changes (5)
  • lib/drive_explorer/thumbnail_creation/page/thumbnail_creation_modal.dart
  • lib/drive_explorer/thumbnail_creation/bloc/thumbnail_creation_state.dart
  • lib/drive_explorer/multi_thumbnail_creation/bloc/multi_thumbnail_creation_state.dart
  • lib/arns/presentation/assign_name_modal.dart
  • lib/blocs/upload/upload_cubit.dart

vilenarios and others added 7 commits July 17, 2026 15:06
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion PE-9132

Private-drive migration was the one ArFS metadata op with no L1
fallback — it posted the drive-signature data item only via Turbo. It
now uses the same config-based branch every other op has: post via Turbo
when useTurboUpload is enabled, otherwise wrap the already-signed data
item in a DataBundle and post directly to the network via
ArweaveService.postTx (pays AR from the wallet).

Closes the deferred migration item from the free-tier work. Note this is
a config switch (useTurboUpload=false), not an automatic on-402 fallback;
migration signature items are ~1 KB and effectively always free-eligible,
so pool exhaustion here is negligible.

Login wallet-creation verification posts are intentionally NOT given this
branch: the wallet is brand-new with no AR during creation, and the ETH
path involves cross-chain signing — an L1 fallback there would fail, not
help.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…E-9132

CodeRabbit (Critical/Major) caught the payment dialog being triggered
from inside the builder via addPostFrameCallback in five modals. A
builder can run many times for the same state (repaints, resizes,
ancestor rebuilds), each queuing another dialog → duplicate/stacked
modals. Moved every trigger to the BlocConsumer listener, which fires
once per state transition: snapshot, single + multi thumbnail, standalone
ArNS assign, and manifest (the last two CodeRabbit didn't flag but had
the identical bug). Builders now render only the static fallback content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…PE-9132

Turbo's new GET /v1/account/free?address=<wallet> reports how many free
bytes a wallet has left. Until now isFreeThanksToTurbo was derived purely
from item size, so a user whose free pool was used up was shown "this
transaction is free thanks to Turbo", had the payment method selector
hidden, and then hit a 402 mid-upload. The 402 handling recovered
gracefully but the promise should never have been made.

- add TurboFreeAllowance, modelling unlimited / limited / disabled /
  unknown, plus covers() and isExhaustedFor()
- add PaymentService.getFreeAllowance and a non-throwing
  TurboBalanceRetriever.getFreeAllowance wrapper
- require both size eligibility and allowance coverage before treating an
  upload as free, in the entity, bundle and snapshot paths
- explain the switch to a payment selector when the allowance ran out,
  instead of silently swapping the UI
- fetch the allowance per preparation, unlike the static item-size limit

The value is advisory: Turbo's response stays the authority on whether an
upload was free, and an unreachable endpoint falls back to the previous
size-only behaviour rather than telling a user with allowance left to pay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW
An audit of every surface that promises a free upload found two the first
pass missed, both in the manifest flow:

- UploadManifestModel.freeThanksToTurbo was still derived from item size
  alone, in both prepareManifestUpload and prepareUploadPlanAndCostEstimates.
  This is worse than a cosmetic false promise: when every manifest is marked
  free, UploadCubit skips the payment method selection entirely, so an
  exhausted wallet went straight to an upload that 402s.
- create_manifest_form showed the payment options with no explanation when
  the allowance ran out, unlike the upload and snapshot dialogs.

Also stores arDriveUploadManager, until now a required UploadCubit
constructor parameter that was never assigned to a field, so the cubit can
reach the allowance without a new dependency.

Adds a regression test for the manifest path, verified to fail without the
fix, plus the missing allowance stubs for the shared setUpAll mocks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW
…get PE-9132

The free-tier UX was carried by two parallel booleans (isFreeThanksToTurbo
and isFreeAllowanceExhausted) threaded through four state classes, where
"free" and "allowance used up" could both be true, and by six hand-rolled
conditionals across three dialogs. That duplication is how the manifest
form ended up promising "free" without ever explaining what happened when
it stopped being free.

- add FreeUploadStatus (free / allowanceUsedUp / notEligible) and a
  freeUploadStatusFor helper holding the two rules in one place
- store that single value in UploadPaymentInfo, UploadPaymentMethodInfo,
  ConfirmingSnapshotCreation and CreateManifestUploadReview, keeping the
  existing booleans as derived getters so no consumer changes
- add TurboFreeStatusMessage, the one widget that renders the one status
  line, collapsing entirely when there is nothing to say
- tighten the used-up copy to lead with the fact

Behaviour is unchanged: same 720 tests pass, including the ~35 existing
isFreeUploadPossibleUsingTurbo assertions untouched, and the manifest
regression test still fails when the underlying fix is reverted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/blocs/upload/upload_cubit.dart (1)

145-190: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the server-fetched Turbo item-size limit for manifest free eligibility. These checks use the static configService.config.allowedDataItemSizeForTurbo while the shared upload payment path uses UploadPaymentEvaluator._maxFreeItemBytes, which prefers Turbo’s /v1/info value. Add an accessor such as ArDriveUploadPreparationManager.getMaxFreeItemBytes() and use it here in prepareManifestUpload and the existing-manifest-entries loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/blocs/upload/upload_cubit.dart` around lines 145 - 190, The manifest
free-eligibility checks in prepareManifestUpload and the
existing-manifest-entries loop must use the server-fetched Turbo limit from
ArDriveUploadPreparationManager instead of
configService.config.allowedDataItemSizeForTurbo. Add or reuse a
getMaxFreeItemBytes() accessor backed by the same value used by
UploadPaymentEvaluator._maxFreeItemBytes, and apply it at both affected sites in
lib/blocs/upload/upload_cubit.dart:145-190 and
lib/blocs/upload/upload_cubit.dart:1068-1080.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/blocs/create_snapshot/create_snapshot_cubit.dart`:
- Around line 81-83: Update the _useTurboUpload getter and related upload-button
logic to honor appConfig.useTurboUpload before selecting Turbo based on
_freeStatus == FreeUploadStatus.free. Ensure freeStatus cannot bypass the
configured Turbo disablement, including the flows covered by
_computeIsFreeThanksToTurbo and the referenced upload handling sections.
- Around line 564-571: Update _computeIsFreeThanksToTurbo around
getFreeAllowance so allowance lookup exceptions are caught locally; on failure,
mark the item as not free and continue returning the paid AR/Turbo choices
instead of propagating the error to confirmDriveAndHeighRange().

In `@lib/core/upload/uploader.dart`:
- Around line 390-408: Update the _determineUploadMethod call in
getUploadPaymentInfoForEntities to pass allowedDataItemSizeForTurbo as the
allowedSizeForTurbo argument instead of dataItemSize. Preserve dataItemSize as
the turboBundleSizes value so the free-tier per-item size cap is enforced
consistently with freeStatus.

In
`@lib/drive_explorer/multi_thumbnail_creation/multi_thumbnail_creation_modal.dart`:
- Around line 111-115: Dismiss the current route before opening the payment
dialog in both payment-error listeners: the listener around
multi_thumbnail_creation_modal.dart lines 111-115 and the listener around
create_snapshot_dialog.dart lines 88-90. Once dismissal is guaranteed, remove
the empty placeholder branches at multi_thumbnail_creation_modal.dart lines
168-171 and create_snapshot_dialog.dart lines 101-103.

---

Outside diff comments:
In `@lib/blocs/upload/upload_cubit.dart`:
- Around line 145-190: The manifest free-eligibility checks in
prepareManifestUpload and the existing-manifest-entries loop must use the
server-fetched Turbo limit from ArDriveUploadPreparationManager instead of
configService.config.allowedDataItemSizeForTurbo. Add or reuse a
getMaxFreeItemBytes() accessor backed by the same value used by
UploadPaymentEvaluator._maxFreeItemBytes, and apply it at both affected sites in
lib/blocs/upload/upload_cubit.dart:145-190 and
lib/blocs/upload/upload_cubit.dart:1068-1080.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 01f215cf-8d84-4191-9f19-4f251cce9114

📥 Commits

Reviewing files that changed from the base of the PR and between 58b560e and eadd175.

📒 Files selected for processing (30)
  • lib/arns/presentation/assign_name_modal.dart
  • lib/blocs/create_manifest/create_manifest_cubit.dart
  • lib/blocs/create_manifest/create_manifest_state.dart
  • lib/blocs/create_snapshot/create_snapshot_cubit.dart
  • lib/blocs/create_snapshot/create_snapshot_state.dart
  • lib/blocs/upload/models/payment_method_info.dart
  • lib/blocs/upload/payment_method/bloc/upload_payment_method_bloc.dart
  • lib/blocs/upload/upload_cubit.dart
  • lib/components/create_manifest_form.dart
  • lib/components/create_snapshot_dialog.dart
  • lib/components/turbo_free_status_message.dart
  • lib/components/upload_form.dart
  • lib/core/upload/uploader.dart
  • lib/drive_explorer/multi_thumbnail_creation/multi_thumbnail_creation_modal.dart
  • lib/drive_explorer/thumbnail_creation/page/thumbnail_creation_modal.dart
  • lib/l10n/app_en.arb
  • lib/l10n/app_es.arb
  • lib/l10n/app_hi.arb
  • lib/l10n/app_ja.arb
  • lib/l10n/app_zh-HK.arb
  • lib/l10n/app_zh.arb
  • lib/pages/app_router_delegate.dart
  • lib/turbo/models/free_upload_status.dart
  • lib/turbo/models/turbo_free_allowance.dart
  • lib/turbo/services/payment_service.dart
  • lib/turbo/turbo.dart
  • test/blocs/create_snapshot_cubit_test.dart
  • test/blocs/upload_cubit_test.dart
  • test/core/upload/uploader_test.dart
  • test/turbo/models/turbo_free_allowance_test.dart
💤 Files with no reviewable changes (1)
  • lib/pages/app_router_delegate.dart
🚧 Files skipped from review as they are similar to previous changes (9)
  • lib/l10n/app_zh.arb
  • lib/l10n/app_ja.arb
  • lib/l10n/app_es.arb
  • lib/arns/presentation/assign_name_modal.dart
  • lib/l10n/app_en.arb
  • lib/l10n/app_zh-HK.arb
  • lib/l10n/app_hi.arb
  • lib/drive_explorer/thumbnail_creation/page/thumbnail_creation_modal.dart
  • lib/components/upload_form.dart

Comment thread lib/blocs/create_snapshot/create_snapshot_cubit.dart
Comment on lines +564 to +571
final freeAllowance =
await turboBalanceRetriever.getFreeAllowance(auth.currentUser.wallet);

_freeStatus = freeUploadStatusFor(
isSizeEligible: true,
byteCount: snapshotSize,
allowance: freeAllowance,
);

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

Do not let allowance lookup failures block paid uploads.

If getFreeAllowance() throws, the exception escapes _computeIsFreeThanksToTurbo() and is caught by confirmDriveAndHeighRange(), which emits ComputeSnapshotDataFailure before ConfirmingSnapshotCreation. A transient allowance-service failure therefore prevents both paid AR and paid Turbo options, even though only free eligibility is unknown. Treat the item as not free and continue to the paid choices.

Proposed fix
-    final freeAllowance =
-        await turboBalanceRetriever.getFreeAllowance(auth.currentUser.wallet);
-
-    _freeStatus = freeUploadStatusFor(
-      isSizeEligible: true,
-      byteCount: snapshotSize,
-      allowance: freeAllowance,
-    );
+    try {
+      final freeAllowance = await turboBalanceRetriever
+          .getFreeAllowance(auth.currentUser.wallet);
+      _freeStatus = freeUploadStatusFor(
+        isSizeEligible: true,
+        byteCount: snapshotSize,
+        allowance: freeAllowance,
+      );
+    } catch (e) {
+      logger.w('Free allowance lookup failed; continuing as paid: $e');
+      _freeStatus = FreeUploadStatus.notEligible;
+    }
📝 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
final freeAllowance =
await turboBalanceRetriever.getFreeAllowance(auth.currentUser.wallet);
_freeStatus = freeUploadStatusFor(
isSizeEligible: true,
byteCount: snapshotSize,
allowance: freeAllowance,
);
try {
final freeAllowance = await turboBalanceRetriever
.getFreeAllowance(auth.currentUser.wallet);
_freeStatus = freeUploadStatusFor(
isSizeEligible: true,
byteCount: snapshotSize,
allowance: freeAllowance,
);
} catch (e) {
logger.w('Free allowance lookup failed; continuing as paid: $e');
_freeStatus = FreeUploadStatus.notEligible;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/blocs/create_snapshot/create_snapshot_cubit.dart` around lines 564 - 571,
Update _computeIsFreeThanksToTurbo around getFreeAllowance so allowance lookup
exceptions are caught locally; on failure, mark the item as not free and
continue returning the paid AR/Turbo choices instead of propagating the error to
confirmDriveAndHeighRange().

Comment thread lib/core/upload/uploader.dart
vilenarios and others added 3 commits July 23, 2026 12:31
Three of the five findings were valid:

- manifest free-eligibility used the static config item-size limit while the
  shared upload path prefers Turbo's /v1/info value. Adds
  ArDriveUploadPreparationManager.getMaxFreeItemBytes(), alongside the
  existing getFreeAllowance(), and uses it at both manifest sites. This also
  removes UploadCubit's last read of the global configService from main.dart.
- getUploadPaymentInfoForEntities passed the item size as its own size limit,
  so the free-tier per-item cap was vacuously satisfied (x <= x). Passes the
  actual limit. No practical change for metadata items, which are far below
  it, but the cap is now real.
- the snapshot dialog is shown with barrierDismissible: false and did not pop
  itself before opening the payment dialog, leaving an invisible undismissable
  barrier over the app once that dialog was closed. It now pops first, like
  create_manifest_form already did.

Declined, with reasons:

- gating snapshot free-status on appConfig.useTurboUpload: free uploads
  deliberately bypass that flag ("Even if this feature flag is off, it will be
  possible to upload using turbo for free files"), and gating it would restore
  the false "free" promise this PR exists to remove.
- catching getFreeAllowance exceptions in the snapshot cubit: the retriever
  wrapper already catches everything and returns unknown, and auth.currentUser
  is read earlier in the same try by _computeBalanceEstimate.
- popping the route in multi_thumbnail_creation_modal: it is an OverlayEntry,
  not a route, so Navigator.pop would dismiss the drive page underneath it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW
… overlay PE-9132

Resolves the two CodeRabbit findings I had previously declined, correctly:

- The free allowance was fetched only when the useTurboUpload flag was on,
  but free uploads deliberately bypass that flag. So with the flag off a
  small item still uploaded via Turbo yet was promised "free" without ever
  checking the allowance — the exact bug this PR removes, hidden behind a
  flag — and the snapshot path (which checks unconditionally) disagreed.
  CodeRabbit proposed making snapshot honor the flag; that is the wrong
  direction, as it would send free-eligible items down the paid path against
  the documented intent. Instead getFreeAllowance is now unconditional, so
  free-ness is always verified. The paid-turbo gate on _getTurboBalance is
  unchanged. No runtime effect today (useTurboUpload is true in all flavors).

- The multi-thumbnail modal is an OverlayEntry, not a route, so Navigator.pop
  would have dismissed the drive page underneath — which is why popping was
  declined. But the overlay was still left behind the payment dialog. It now
  dismisses through its own CloseMultiThumbnailCreation event, the mechanism
  the modal already uses for closing.

Adds an assertion that the allowance is consulted even with the flag off.

Endpoint host confirmed empirically: GET payment.ardrive.io/v1/account/free
returns 200 {"bytesRemaining":10485760} (10 MiB), and upload.ardrive.io 404s,
so turboPaymentUri is the correct host. An unknown address returns the full
allowance rather than 404, so new wallets correctly read as free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW
…g used up PE-9132

A bulk/folder upload of small files whose total is larger than the wallet's
remaining free pool was labelled "Free allowance used up" — wrong when the
user still has most of their allowance and the upload simply exceeds it.

Adds FreeUploadStatus.exceedsAllowance, distinct from allowanceUsedUp, chosen
when the wallet still has a positive allowance but the upload is bigger than
it. TurboFreeStatusMessage now shows an honest note for that case: "This
upload exceeds your free allowance and will need Credits or AR."

Deliberately robust rather than precise. The message states the fact (upload
> remaining) and the outcome (needs payment) without predicting how many
bytes end up free, because the client cannot know that: Turbo applies the
free tier server-side, does not expose whether it bills per-item or
per-bundle, and enforces a second per-IP pool that /v1/account/free does not
report. So it is true whether Turbo frees part of the upload or none of it,
and the 402 remains the authority on what is actually charged. This also
corrects a stale comment that asserted all-or-nothing billing as fact.

Behaviour is otherwise unchanged: exceedsAllowance is not free, so the
payment selector still shows and upload-method selection is untouched. Single
item / snapshot / manifest paths are unaffected in practice. The one existing
assertion that expected "used up" for a partial multi-item upload is updated
to expect the new, more accurate status; the derived getters and the widget's
now-exhaustive switch keep every other consumer compiling unchanged.

Adds unit coverage for freeUploadStatusFor (including boundaries and
fail-open) and a widget test asserting each status renders the right message,
both verified to fail under a collapsing mutation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW
@vilenarios vilenarios changed the title PE-9132: Turbo free-tier readiness — typed payment failures and honest dialogs PE-9132: Turbo free-tier support — allowance-aware upload UX and typed payment failures Jul 23, 2026
@vilenarios
vilenarios merged commit 53d0b73 into dev Jul 23, 2026
8 checks passed
@vilenarios
vilenarios deleted the feat/turbo-free-tier branch July 23, 2026 20:43
vilenarios added a commit that referenced this pull request Jul 27, 2026
…oad prep PE-9132 (#2169)

getFreeAllowance() (added in #2166) was awaited late in each upload-prep
method, after the balance and cost round-trips that already run serially. The
allowance call is independent — it only needs the wallet — so it was adding an
extra sequential Turbo round-trip to every upload-modal open, and doubling the
worst-case wait when payment.ardrive.io is unavailable (two 8s timeouts back
to back instead of one) before it fails open.

Start the future up front in both getUploadPaymentInfoForEntities and
getUploadPaymentInfoForUploadPlans and await it where the free status is
computed, so it overlaps the balance, size and cost work. Timing only — no
value changes; getFreeAllowance is a non-throwing wrapper, so the in-flight
future cannot become an unhandled rejection. 733 tests unchanged.


Claude-Session: https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW

Co-authored-by: vilenarios <philip.mataras@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
vilenarios added a commit that referenced this pull request Aug 25, 2026
… sync resilience (#2173)

* perf: prefetch next snapshot + streaming JSON parse PE-9103

Two optimizations for snapshot loading during sync:

1. Prefetch: start downloading the next snapshot body while the
   current one is being parsed and written to DB. Only 1 ahead to
   avoid gateway 429s. Overlaps network I/O with CPU work.

2. Streaming parse: instead of jsonDecode on the entire snapshot
   (which builds a massive Map with all entries in memory), scan
   the JSON string for individual txSnapshot object boundaries
   using brace counting and parse each one separately. This:
   - Avoids building the full nested Map (lower peak memory)
   - Starts yielding transactions immediately
   - Still needs the full string downloaded, but parsing is
     incremental

For a drive with 3 snapshots of ~15MB, ~10MB, ~5MB:
  Before: download1 + parse1 + download2 + parse2 + download3 + parse3
  After:  download1 + [parse1 || download2] + [parse2 || download3] + parse3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "perf: prefetch next snapshot + streaming JSON parse PE-9103"

This reverts commit 78331785e351f7dc5c02079ff82d0db6cfc76e6e.

* PE-9103: Bulk-load revision lookups instead of per-entity DB queries (#2142)

* perf: bulk-load revision lookups instead of per-entity DB queries PE-9103

During sync, for each file/folder entity, the code issued individual
SELECT queries to find the latest and oldest revisions. For a drive
with 19k files, that was 38k+ individual DB queries (19k latest +
19k oldest for files, plus similar for folders).

Changes:
- Add 4 bulk SQL queries to drive_queries.drift (latest/oldest ×
  files/folders) using GROUP BY with MAX/MIN dateCreated
- Add 4 helper methods to DriveDao returning Map<entityId, Revision>
- Pre-load all revision maps at the start of each transaction chunk
- Pass maps through to _addNewFileEntityRevisions,
  _addNewFolderEntityRevisions, _computeRefreshedFileEntriesFromRevisions,
  _computeRefreshedFolderEntriesFromRevisions
- Update latestRevisionsCache as new revisions are inserted so
  subsequent sub-batches see fresh data
- First-sync shortcut: skip all bulk queries when drive.lastBlockHeight
  is 0/null (every entity is new, no previous revisions exist)

Performance: 38,000+ individual SELECTs → 4 bulk SELECTs per chunk.
First sync: 0 queries (all skipped).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: add 5s request timeout + reduce retries on data gateway PE-9103

Entity metadata fetches had NO timeout — a slow gateway could hang
indefinitely. With 2 retries × 5 gateways = 10 attempts with no
timeout, a single failed entity could take minutes.

Changes:
- Add 5-second timeout per HTTP request (metadata is tiny JSON)
- Reduce retries per gateway from 2 to 1 (move on, don't retry slow)
- Reduce GAR fallbacks from 3 to 2

Worst case per entity: 4 attempts × 5s = 20s max (was: 10 attempts,
unlimited time)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add 15s total timeout on data fetch fallback chain PE-9103 (#2143)

A single missing/broken tx could block sync for minutes as the
fallback chain tried every gateway with CORS timeouts and 504s.

Added a 15-second total timeout wrapping the entire fallback chain
(primary → GAR gateways → arweave.net). Combined with the existing
5s per-request timeout, worst case for any single tx is now 15s max.

This prevents the sync from appearing "stuck" — even if metadata
fetches fail, the sync completes within a bounded time and the
periodic sync can trigger again.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: prefetch next snapshot + streaming JSON parse PE-9103 (#2141)

Two optimizations for snapshot loading during sync:

1. Prefetch: start downloading the next snapshot body while the
   current one is being parsed and written to DB. Only 1 ahead to
   avoid gateway 429s. Overlaps network I/O with CPU work.

2. Streaming parse: instead of jsonDecode on the entire snapshot
   (which builds a massive Map with all entries in memory), scan
   the JSON string for individual txSnapshot object boundaries
   using brace counting and parse each one separately. This:
   - Avoids building the full nested Map (lower peak memory)
   - Starts yielding transactions immediately
   - Still needs the full string downloaded, but parsing is
     incremental

For a drive with 3 snapshots of ~15MB, ~10MB, ~5MB:
  Before: download1 + parse1 + download2 + parse2 + download3 + parse3
  After:  download1 + [parse1 || download2] + [parse2 || download3] + parse3

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: retry gateway price fetch and handle AR cost failure gracefully PE-9103

- getPrice now retries 3 times with backoff instead of failing on a
  single 502 from the gateway
- AR cost calculation in the upload modal falls back to zero estimate
  on failure so the modal still opens with Turbo available instead of
  crashing entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: dateCreated null crash in file/folder entry refresh on first sync PE-9103

toEntryCompanion() on file/folder revision companions produces entry
companions with dateCreated = Value.absent() (schema has DEFAULT).
On first sync, oldestRevisionsCache is empty, so the fallback
.dateCreated.value returns null → JSNull crash on web.

Fixed by keeping a separate map of revision dateCreated values and
using those as the fallback instead of the entry companion's field.

Same bug pattern as the drive revision fix (line 1887), but for
files (line 1838) and folders (line 1871).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: add drive owner to tx status and license gql queries PE-9126

Add an optional owners filter to the by-ID GraphQL queries that the
gateway struggles with (large ClickHouse row scans). Scoping by owner
lets the gateway prune its search space. The owner variable is nullable,
so when an owner can't be determined the query behaves exactly as before.

- TransactionStatuses, LicenseComposed, LicenseAssertions: add $owners
- getTransactionConfirmations / getLicenseComposed / getLicenseAssertions:
  accept an optional owner and pass it through
- tx status (per-drive): scope to the drive's ownerAddress
- tx status (global): scope to the logged-in wallet's address
- licenses: scope to the best-known tx owner (revision/file owner)

Edge case: data txs of files pinned from other authors are owned by
those authors and won't match the owner filter; treated as best-effort.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: re-query owner-mismatched txs unscoped to avoid false failures PE-9126

Owner-scoped getTransactionConfirmations leaves cross-owner txs (e.g. the
data tx of a file pinned from another author's upload) at -1, which the
sync status logic would turn into 'failed' after the pending timeout.

Add a fallback pass: after the selective owner-scoped query, re-query any
ids still unresolved (-1) without the owner filter to determine their true
status. The residual set is normally tiny, so the selectivity win of the
first pass is preserved while confirmed cross-owner txs are no longer
misclassified as failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: resolve owner-mismatched txs via pin owner, not unscoped re-query PE-9126

The previous fallback re-queried unresolved (-1) txs without an owner
filter, which can re-trigger the gateway's expensive row scan that the
owner scoping was meant to avoid — and, because the call is wrapped in a
5s timeout, a slow/erroring fallback discards the whole batch's results
and makes no progress, repeating every sync.

Replace it with a selective approach: only re-query unresolved ids whose
real on-chain owner is known locally. Currently that's pinned files,
whose data tx is owned by the original uploader (pinnedDataOwnerAddress).
These are re-queried scoped to that owner, so every query stays selective.
Genuinely missing txs have no override and are left unresolved, handled by
the caller's existing pending/failed logic — no unscoped scan, no retry
storm on the common "not found yet" case.

- add pinnedFileRevisions drift query
- getTransactionConfirmations: ownerOverrides param replaces unscoped pass
- sync repo: build pin owner overrides and pass them through

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: make pinned-owner confirmation recovery best-effort PE-9126

The second (pinned-owner) pass merges into the already-populated first-pass
map, so its failure must never discard first-pass progress. Wrap it in a
try/catch and bound it with its own 3s timeout: even if the pin re-queries
error or stall, the confirmations resolved by the owner-scoped first pass
are still returned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf: preserve resolved confirmations across a timeout via verified sink PE-9126

getTransactionConfirmations is wrapped in a 5s timeout by the sync status
update; on expiry it previously returned an empty map, discarding every
confirmation already resolved that cycle (the whole 5000-tx page).

Add an optional caller-owned verifiedSink that the method populates with
each resolved confirmation (>= 0 only) as queries complete. The callers pass
one in and, on timeout, fall back to it instead of an empty map — so work
done before the deadline (including a fully-completed first pass when only
the pinned-owner pass is slow) is applied rather than thrown away.

Because the sink holds only positive verifications and never the -1
"not found" placeholders, applying it after a partial/timed-out run only
upgrades txs to confirmed and never marks anything failed off incomplete data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: bound confirmation fan-out and use type-safe id filtering PE-9126

- Cap the per-chunk GraphQL fan-out in getTransactionConfirmations to
  maxConcurrentDataFetches instead of launching every chunk at once, so a
  large pending-tx page can't burst into a concurrent-retry storm against
  the gateway (matches the throttling on the other gateway-heavy paths).
- Replace `sublist(...) as List<String>?` with `.whereType<String>().toList()`.
  The cast threw a TypeError for the pinned-owner pass (whose ids list is
  typed List<String?>), which the best-effort catch swallowed — silently
  disabling pin recovery. The filter yields a real List<String> regardless
  of input type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PE-9103: Fix empty explorer after drive attach (#2145)

* fix: empty explorer after drive attach PE-9103

Two bugs caused the explorer to show a permanent spinner after
attaching a drive (data was in DB but UI didn't update):

1. buildWhen guard in drive_detail_page.dart blocked BlocBuilder
   rebuilds when SyncCubit was SyncInProgress. If DriveDetailCubit
   emitted DriveDetailLoadSuccess during sync, the emission was
   permanently skipped with no replay mechanism. Removed the sync
   check — DriveDetailCubit already gates emissions via
   waitCurrentSync() in the Rx.combineLatest3 callback.

2. startSyncForDrive silently aborted when a sync was in progress.
   The .then(selectDrive) still fired, selecting a drive whose
   content was never synced. Changed to await waitCurrentSync()
   so the single-drive sync runs after the current sync finishes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: faster snapshot validation — skip fallback on 404, reduce timeouts PE-9103

Snapshot validation was slow on localhost (and anywhere Solana RPC is
unavailable): 2 HEAD retries × 10s timeout + GAR fallback via Solana
= 25+ seconds per snapshot. With 16 drives having multiple snapshots,
sync appeared stuck.

Changes:
- 1 HEAD attempt instead of 2 (fail fast, fall back to GQL)
- HEAD timeout 10s → 5s
- GAR list timeout 5s → 3s
- Skip GAR fallback entirely when primary returned 404 (snapshot
  doesn't exist, no point trying other gateways via Solana RPC)
- Only try GAR fallback for transient errors (timeout, 5xx)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: guard startSyncForDrive race after waitCurrentSync PE-9103

Two callers could both pass the SyncInProgress guard after
waitCurrentSync() returned, causing concurrent single-drive syncs.
Re-check state after wait — if another sync started, bail out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: bump arweave-dart to v4.0.2 to fix file download crash

Pulls in the TransactionData null-field fix so downloads no longer crash
on web with "type 'JSNull' is not a subtype of type 'String'" when a
gateway returns null for optional tx fields (e.g. anchor on
turbo-gateway.com). Updates both the dependency and the override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(version): bump version to 2.84.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* PE-9103: Modernize share file and download all modals (#2149)

* ui: modernize share file and download all modals PE-9103

Both modals used the old ArDriveStandardModal with deprecated
typography and color tokens. Updated to match the current design
system used by drive attach, upload, and other modern modals.

Share File modal:
- ArDriveStandardModal → ArDriveStandardModalNew (adds red header bar)
- Old typography (buttonNormalBold, buttonLargeRegular) → semantic
  ArDriveTypographyNew (paragraphSmall, paragraphNormal)
- Old colors (themeFgDefault, themeWarningEmphasis) → colorTokens
  (textHigh, textMid, textLow, strokeRed)
- ArDriveTextField → ArDriveTextFieldNew
- Warning banner wrapped in styled container with containerL1 bg

Download All Files modal:
- All 3 ArDriveStandardModal instances → ArDriveStandardModalNew
- File list items wrapped in styled containers (containerL1 bg,
  rounded corners, file/folder icons, proper spacing)
- Old typography (smallBold, smallRegular) → semantic typography
- Old colors (themeFgSubtle, themeFgMuted) → colorTokens
- Error state text styled with new typography

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review issues in share/download modals PE-9103

- add explicit FileShareLoadedPendingFile state branch with localized body text
- add TODO comment for hardcoded pending warning string (needs ARB extraction)
- add close action to fallback modal in multiple file download

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope tx-status query per-tx by drive owner, not logged-in wallet PE-9126

The global tx-status update assumed the logged-in wallet owns every pending
tx and scoped the whole batch by walletAddress. That breaks for ATTACHED
drives owned by other wallets: their pending data txs were queried under the
wrong owner, and when no wallet is present (browsing a public/attached drive)
walletAddress is null, so TransactionStatuses went out unscoped — hitting the
gateway's expensive full-scan path and timing out.

Resolve each pending tx's owner from the drive it actually belongs to:

- add pendingDataFileRevisions drift query (read-only; no schema change)
- _buildPendingTxDriveOwners: map pending data tx id -> its drive's ownerAddress
- getTransactionConfirmations: new ownersByTxId map; first pass groups txs by
  their resolved per-tx owner (map wins, else the single owner fallback) and
  queries each owner once, so a batch spanning multiple drives stays selective.
  A tx with no resolvable owner is left unresolved rather than queried unscoped.
- global _updateTransactionStatuses passes the per-tx owner map; walletAddress
  remains only as a fallback for unmapped txs.

The per-drive path is unchanged (single drive => single owner). Pinned-owner
recovery (pass 2) still works: pins map to their drive owner in pass 1 (miss)
and are recovered under pinnedDataOwnerAddress in pass 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: drop unscopable txs from confirmations instead of returning -1 PE-9126

A pending tx with no resolvable owner (e.g. global path with no wallet and a
tx not mappable to a drive) is never queried, but was left at its pre-seeded
-1 in the result map. The caller reads -1 as "not found" and can age an old
pending tx into failed.

Remove such txs from the returned map so they're absent rather than -1: the
caller skips absent txs (no status change), leaving them pending to be
retried once their owner is known — the same "unknown => skip, never fail off
incomplete data" rule used by the verified-sink path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: raise tx-status timeouts above gateway upstream ceiling PE-9126

The confirmation query intermittently takes ~10s when turbo-gateway's
indexer-core circuit breaker is open (observed 9.87s with UPSTREAM_CIRCUIT_OPEN
/ "timeout of 9500ms exceeded" warnings, returning valid data). The client's
5s per-batch timeout fired first and discarded the whole batch, leaving
long-confirmed txs stuck as pending even though the gateway returned their
confirmations.

Move both timeouts above that ~9.5s ceiling:
- per-batch getTransactionConfirmations: 5s -> 15s
- overall per-drive/global status update: 10s -> 30s

Extracted as named constants with the rationale. Typical responses are ~0.5s,
so this only lengthens waits while the gateway is degraded (when we want to
wait, not drop the batch); the verified sink still preserves partial progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: generalize tx-status timeout comments to be backend-agnostic PE-9126

Reword the timeout rationale in terms of general gateway slowness rather than a
specific gateway's internal index/circuit-breaker/warning codes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PE-9103: Eliminate redundant GraphQL calls in sync pipeline (#2150)

* perf: sync pipeline performance and resilience optimizations PE-9103

- skip unchanged drives via bulk GQL probe (1 query per owner replaces ~40 queries per idle drive)
- add database indexes on file_revisions.dataTxId and network_transactions.status
- bulk pre-load dateCreated for tx status updates (eliminates N+1 per-tx DB queries)
- bulk pre-load folders and drives in ghost creation (eliminates N+1 per-folder DB queries)
- batch transaction status writes using insertNewNetworkTransactions (replaces individual writes)
- replace full file_revisions table load with filtered query for snapshot tx matching
- fix DataGatewayFallback timeout budget (15s→25s) so arweave.net is reachable
- fix 4 GraphQL queries bypassing GraphQLRetry (missing retry + fallback)
- remove 200ms artificial delay between tx status batches
- bump database schema version 28→29

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: don't skip post-sync ops when all drives are unchanged PE-9103

The early return on numberOfDrivesToSync == 0 skipped transaction status
updates, ghost folder creation, and ARNS record updates. Pending
transactions from recent uploads need confirmation checks even when no
drives have new entities.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: skip GraphQL call for public file downloads PE-9103

Public file downloads were calling getTransactionDetails() via GraphQL
before starting the download — entirely unnecessary since the txId is
already known locally. This GQL call was also one of the four that
bypassed GraphQLRetry, making it the likely cause of downloads failing
to start when the gateway returns 429/5xx.

Now only private/encrypted files call GraphQL (to fetch cipher/IV tags
from the data transaction). Public files start downloading immediately
with no network round-trip.

Refactored ArDriveDownloader interface: replaced TransactionCommonMixin
dataTx parameter with String txId + bool verifyDownload, since only
those two values were ever used from the full transaction object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: hedged gateway requests, download fallback, retry UX, drive attach dedup PE-9103

hedged gateway requests:
- replace serial waterfall with staggered parallel requests in DataGatewayFallback
- fire primary immediately, launch fallbacks every 1.5s if no response
- first 200 response wins, rest ignored — worst case ~5s instead of ~20s
- applies to all metadata fetches automatically (no caller changes)

download resilience:
- add downloadWithFallback() for file downloads with same hedged pattern
- add fetchManifestWithFallback() for manifest downloads (had zero fallback)
- add stall detection: throws DownloadStalledException if no chunk for 60s
- typed exceptions: DownloadFileNotFoundException, DownloadNetworkException,
  DownloadRateLimitException, DownloadStalledException

download UX:
- differentiated error dialogs: network error, file not found, rate limited
- retry button on retryable failures (network, rate limit, unknown)
- file not found shows OK only (retry won't help)
- error classification in both personal and shared download cubits

drive attach dedup:
- getDrivePrivacyForId() now returns DrivePrivacyResult with owner + tx node
- drivePrivacyLoader() passes owner to getLatestDriveEntityWithId(), skipping
  redundant owner lookup — 4 GQL queries reduced to 2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: revert metadata fetches to serial fallback, keep hedged for downloads only PE-9103

Hedged (staggered parallel) requests fire extra gateway requests when the
primary is slow but succeeds. During sync with hundreds of metadata fetches,
this wastes bandwidth and could trigger rate limits on GAR gateways.

Now:
- metadata fetches (fetchData): serial waterfall (primary → GAR → arweave.net)
- file downloads (downloadWithFallback): hedged staggered (latency-sensitive, single request)
- manifest downloads (fetchManifestWithFallback): serial waterfall

Also fixes stall detection for empty files — timer only starts after the
first chunk arrives, so 0-byte files don't trigger DownloadStalledException.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: batch snapshot queries and share gateway cache PE-9103

batch snapshot queries:
- SnapshotEntityHistory.graphql now accepts $driveIds array instead of
  single $driveId — fetches snapshots for all drives in one paginated query
- syncAllDrives() prefetches snapshots for all drives per owner before
  the per-drive sync loop, passes results to _syncDrive()
- reduces N snapshot GQL queries (one per drive) to 1 per unique owner
- also fixes Entity-Type tag syntax: values: "snapshot" → values: ["snapshot"]

share gateway cache:
- DataGatewayFallback.cachedGateways is now public so
  SnapshotValidationService can reuse the same gateway list
- syncAllDrives() passes the cache before sync starts
- eliminates 1 duplicate Solana RPC call per sync cycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review — stall detection + mobile GCM path PE-9103

- forward verifyDownload param to mobile AES-GCM download path
- wrap mobile GCM stream with _withStallDetection (was bypassed)
- cancel upstream subscription when stall timer fires
- guard against adding to closed StreamController in stall detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: eliminate redundant GraphQL calls in sync pipeline PE-9103

HAR analysis of a real sync session (16 drives, no changes) revealed
130 GraphQL calls taking 376s. This commit reduces that to ~3 calls
and <1s for incremental syncs with no changes.

Changes:
- fix DriveActivityProbe: partition drives into never-synced and
  previously-synced before probing. Never-synced drives (lastBlockHeight=0)
  were poisoning the probe's minBlockHeight to 0, causing it to query
  from genesis, overflow the page limit, and fall back to syncing ALL
  drives. Now only previously-synced drives are probed.
- cache UserDriveEntityTxs in ArweaveService with event-based
  invalidation. Auth flow (isExistingUser, _validateUser) and sync
  (updateUserDrives) all call getUniqueUserDriveEntityTxs for the same
  wallet within seconds. Cache is cleared after sync completion.
- cache updateUserDrives in SyncRepository with event-based flag.
  Multiple entry points (syncMetadataOnly, startSync, startSyncForDrive)
  call it redundantly. Flag cleared only when sync processes drives.
- skip genesis block [0,0] range in GQLDriveHistory. Snapshot gaps at
  block 0 produced phantom ranges causing 3 wasted queries per drive.
- add local DB pre-check for PendingDriveEntities. Only query gateway
  when pendingTransactionsForDrive returns local entries. Skip entirely
  for non-owned (read-only) drives.
- stop Solana RPC retry spam in DataGatewayFallback and
  SnapshotValidationService. Cache empty gateway list on first failure
  instead of retrying every fetchData/validation call.
- fix zero-drives-to-sync: when probe skips all drives, return
  emptySyncCompleted instead of falling through to "all failed".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review — race condition, cache sharing, probe gaps PE-9103

- fix updateUserDrives race condition: replace boolean flag with Future
  so concurrent callers await the in-flight request instead of both firing
- fix snapshot prefetch minBlock: exclude never-synced drives from the min
  calculation so they don't drag the batched snapshot query to block 0
- fix gateway cache sharing: pass DataGatewayFallback reference to
  SnapshotValidationService instead of copying the list, so both services
  share one cache and writes propagate bidirectionally
- add cancellation check in probe loop before each owner group

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: cache entity data, drive signatures, and skip redundant balance refresh PE-9103

- cache raw entity data bytes from getUniqueUserDriveEntities so
  getLatestDriveEntityWithId (called during password validation) can
  re-parse without re-downloading from the gateway
- cache drive signatures permanently (immutable on-chain) to avoid
  redundant GQL + data fetch on every login for v1-signed private drives
- skip refreshBalance after no-op sync (drivesSynced == 0) to avoid
  redundant PendingTxFees query when nothing changed
- skip redundant getLatestDriveEntityWithId in drive attach flow when
  drivePrivacyLoader already cached the entity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: use unfiltered GQL strategy for drive history (48 calls → 16) PE-9103

Switch GQLDriveHistory from GetSegmentedTransactionFromDrive-
FilteringByEntityTypeStrategy (3 queries per drive: drive, folder,
file) to the unfiltered strategy (1 query per drive returning all
entity types). The downstream parsing pipeline already separates
entities by type via whereType<DriveEntity/FolderEntity/FileEntity>,
so the per-type filtering at the query level was redundant round trips.

For 16 drives on first sync: 48 → 16 DriveEntityHistory calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: clear cached updateUserDrives future on error to allow retry PE-9103

If the updateUserDrives future completes with an error (transient
network issue), subsequent callers would receive the same cached error
without retrying. Now the cached future is cleared on error so the
next caller gets a fresh attempt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review — infinite loop guard, dead code cleanup PE-9103

- add empty-edges guard in getAllSnapshotsForDrives pagination loop to
  prevent infinite loop when gateway returns hasNextPage=true with 0 edges
- remove unreachable duplicate cachedDriveEntity check in driveNameLoader
- fix misleading comment in drivePrivacyLoader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* PE-9103: Snapshot creation progress reporting, caching, and retry (#2153)

* PE-9103: Sync modal UX improvements (#2154)

* ux: sync modal improvements — retry, drive names, elapsed time, probe status PE-9103

- show "Checking for changes..." during drive activity probe phase
  instead of misleading "0 of 16 Drives Synced"
- include drive names in sync error messages (e.g., "My Drive: Gateway
  timeout (504)") so users know which drive failed
- add "Retry Failed" button in sync error modal to retry only the
  drives that failed without re-syncing everything
- show elapsed time (e.g., "45s elapsed") after 5 seconds during sync
  so users know the sync is progressing on longer first syncs
- expose syncStartTime getter on SyncCubit for elapsed time widget
- add localization keys for all new strings in 6 locales

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: retry only failed drives, not all drives PE-9103

CodeRabbit correctly identified that retryFailedDrives was calling
startSync(deepSync: true) which resyncs ALL drives. Now iterates
over failed drive IDs and syncs each individually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: route retry through syncAllDrives with driveIdsToRetry filter PE-9103

Instead of looping startSyncForDrive (which flashes the modal per
drive), add driveIdsToRetry parameter to syncAllDrives that filters
the drives list. Retry runs as a single sync session with one modal,
proper ghost creation, and transaction status updates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review — safe getter, consistent error names PE-9103

- make _initSync non-late (initialized to DateTime.now()) to prevent
  LateInitializationError if syncStartTime is read before sync starts
- prefix drive name in syncSingleDrive error messages to match
  syncAllDrives format ("Drive Name: error message")
- skip localization finding: statusMessage strings are hardcoded English
  as a pre-existing pattern — repository has no BuildContext access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* PE-9103: Snapshot creation improvements + download UX fixes (#2155)

* perf: sync pipeline performance and resilience optimizations PE-9103

- skip unchanged drives via bulk GQL probe (1 query per owner replaces ~40 queries per idle drive)
- add database indexes on file_revisions.dataTxId and network_transactions.status
- bulk pre-load dateCreated for tx status updates (eliminates N+1 per-tx DB queries)
- bulk pre-load folders and drives in ghost creation (eliminates N+1 per-folder DB queries)
- batch transaction status writes using insertNewNetworkTransactions (replaces individual writes)
- replace full file_revisions table load with filtered query for snapshot tx matching
- fix DataGatewayFallback timeout budget (15s→25s) so arweave.net is reachable
- fix 4 GraphQL queries bypassing GraphQLRetry (missing retry + fallback)
- remove 200ms artificial delay between tx status batches
- bump database schema version 28→29

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: don't skip post-sync ops when all drives are unchanged PE-9103

The early return on numberOfDrivesToSync == 0 skipped transaction status
updates, ghost folder creation, and ARNS record updates. Pending
transactions from recent uploads need confirmation checks even when no
drives have new entities.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: skip GraphQL call for public file downloads PE-9103

Public file downloads were calling getTransactionDetails() via GraphQL
before starting the download — entirely unnecessary since the txId is
already known locally. This GQL call was also one of the four that
bypassed GraphQLRetry, making it the likely cause of downloads failing
to start when the gateway returns 429/5xx.

Now only private/encrypted files call GraphQL (to fetch cipher/IV tags
from the data transaction). Public files start downloading immediately
with no network round-trip.

Refactored ArDriveDownloader interface: replaced TransactionCommonMixin
dataTx parameter with String txId + bool verifyDownload, since only
those two values were ever used from the full transaction object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: hedged gateway requests, download fallback, retry UX, drive attach dedup PE-9103

hedged gateway requests:
- replace serial waterfall with staggered parallel requests in DataGatewayFallback
- fire primary immediately, launch fallbacks every 1.5s if no response
- first 200 response wins, rest ignored — worst case ~5s instead of ~20s
- applies to all metadata fetches automatically (no caller changes)

download resilience:
- add downloadWithFallback() for file downloads with same hedged pattern
- add fetchManifestWithFallback() for manifest downloads (had zero fallback)
- add stall detection: throws DownloadStalledException if no chunk for 60s
- typed exceptions: DownloadFileNotFoundException, DownloadNetworkException,
  DownloadRateLimitException, DownloadStalledException

download UX:
- differentiated error dialogs: network error, file not found, rate limited
- retry button on retryable failures (network, rate limit, unknown)
- file not found shows OK only (retry won't help)
- error classification in both personal and shared download cubits

drive attach dedup:
- getDrivePrivacyForId() now returns DrivePrivacyResult with owner + tx node
- drivePrivacyLoader() passes owner to getLatestDriveEntityWithId(), skipping
  redundant owner lookup — 4 GQL queries reduced to 2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: revert metadata fetches to serial fallback, keep hedged for downloads only PE-9103

Hedged (staggered parallel) requests fire extra gateway requests when the
primary is slow but succeeds. During sync with hundreds of metadata fetches,
this wastes bandwidth and could trigger rate limits on GAR gateways.

Now:
- metadata fetches (fetchData): serial waterfall (primary → GAR → arweave.net)
- file downloads (downloadWithFallback): hedged staggered (latency-sensitive, single request)
- manifest downloads (fetchManifestWithFallback): serial waterfall

Also fixes stall detection for empty files — timer only starts after the
first chunk arrives, so 0-byte files don't trigger DownloadStalledException.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: batch snapshot queries and share gateway cache PE-9103

batch snapshot queries:
- SnapshotEntityHistory.graphql now accepts $driveIds array instead of
  single $driveId — fetches snapshots for all drives in one paginated query
- syncAllDrives() prefetches snapshots for all drives per owner before
  the per-drive sync loop, passes results to _syncDrive()
- reduces N snapshot GQL queries (one per drive) to 1 per unique owner
- also fixes Entity-Type tag syntax: values: "snapshot" → values: ["snapshot"]

share gateway cache:
- DataGatewayFallback.cachedGateways is now public so
  SnapshotValidationService can reuse the same gateway list
- syncAllDrives() passes the cache before sync starts
- eliminates 1 duplicate Solana RPC call per sync cycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review — stall detection + mobile GCM path PE-9103

- forward verifyDownload param to mobile AES-GCM download path
- wrap mobile GCM stream with _withStallDetection (was bypassed)
- cancel upstream subscription when stall timer fires
- guard against adding to closed StreamController in stall detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: eliminate redundant GraphQL calls in sync pipeline PE-9103

HAR analysis of a real sync session (16 drives, no changes) revealed
130 GraphQL calls taking 376s. This commit reduces that to ~3 calls
and <1s for incremental syncs with no changes.

Changes:
- fix DriveActivityProbe: partition drives into never-synced and
  previously-synced before probing. Never-synced drives (lastBlockHeight=0)
  were poisoning the probe's minBlockHeight to 0, causing it to query
  from genesis, overflow the page limit, and fall back to syncing ALL
  drives. Now only previously-synced drives are probed.
- cache UserDriveEntityTxs in ArweaveService with event-based
  invalidation. Auth flow (isExistingUser, _validateUser) and sync
  (updateUserDrives) all call getUniqueUserDriveEntityTxs for the same
  wallet within seconds. Cache is cleared after sync completion.
- cache updateUserDrives in SyncRepository with event-based flag.
  Multiple entry points (syncMetadataOnly, startSync, startSyncForDrive)
  call it redundantly. Flag cleared only when sync processes drives.
- skip genesis block [0,0] range in GQLDriveHistory. Snapshot gaps at
  block 0 produced phantom ranges causing 3 wasted queries per drive.
- add local DB pre-check for PendingDriveEntities. Only query gateway
  when pendingTransactionsForDrive returns local entries. Skip entirely
  for non-owned (read-only) drives.
- stop Solana RPC retry spam in DataGatewayFallback and
  SnapshotValidationService. Cache empty gateway list on first failure
  instead of retrying every fetchData/validation call.
- fix zero-drives-to-sync: when probe skips all drives, return
  emptySyncCompleted instead of falling through to "all failed".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review — race condition, cache sharing, probe gaps PE-9103

- fix updateUserDrives race condition: replace boolean flag with Future
  so concurrent callers await the in-flight request instead of both firing
- fix snapshot prefetch minBlock: exclude never-synced drives from the min
  calculation so they don't drag the batched snapshot query to block 0
- fix gateway cache sharing: pass DataGatewayFallback reference to
  SnapshotValidationService instead of copying the list, so both services
  share one cache and writes propagate bidirectionally
- add cancellation check in probe loop before each owner group

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: cache entity data, drive signatures, and skip redundant balance refresh PE-9103

- cache raw entity data bytes from getUniqueUserDriveEntities so
  getLatestDriveEntityWithId (called during password validation) can
  re-parse without re-downloading from the gateway
- cache drive signatures permanently (immutable on-chain) to avoid
  redundant GQL + data fetch on every login for v1-signed private drives
- skip refreshBalance after no-op sync (drivesSynced == 0) to avoid
  redundant PendingTxFees query when nothing changed
- skip redundant getLatestDriveEntityWithId in drive attach flow when
  drivePrivacyLoader already cached the entity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: use unfiltered GQL strategy for drive history (48 calls → 16) PE-9103

Switch GQLDriveHistory from GetSegmentedTransactionFromDrive-
FilteringByEntityTypeStrategy (3 queries per drive: drive, folder,
file) to the unfiltered strategy (1 query per drive returning all
entity types). The downstream parsing pipeline already separates
entities by type via whereType<DriveEntity/FolderEntity/FileEntity>,
so the per-type filtering at the query level was redundant round trips.

For 16 drives on first sync: 48 → 16 DriveEntityHistory calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: clear cached updateUserDrives future on error to allow retry PE-9103

If the updateUserDrives future completes with an error (transient
network issue), subsequent callers would receive the same cached error
without retrying. Now the cached future is cleared on error so the
next caller gets a fresh attempt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review — infinite loop guard, dead code cleanup PE-9103

- add empty-edges guard in getAllSnapshotsForDrives pagination loop to
  prevent infinite loop when gateway returns hasNextPage=true with 0 edges
- remove unreachable duplicate cachedDriveEntity check in driveNameLoader
- fix misleading comment in drivePrivacyLoader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: snapshot creation progress reporting, caching, and retry PE-9103

progress reporting:
- ComputingSnapshotData state now includes processedTransactions and
  totalTransactions (optional, defaults to 0 for backward compat)
- SnapshotItemToBeCreated accepts onProgress callback, fires after each
  batch of 100 transactions
- dialog shows "Processing X of Y transactions..." instead of just
  "This may take a while"

performance:
- cache drive privacy check once in _reset() instead of querying
  driveDao.driveById() per transaction (eliminates N+1 DB queries)
- cache MetadataCache instance once instead of re-creating per transaction

retry without recompute:
- cache computed snapshot data after _getSnapshotData() completes
- on upload failure, "Try Again" reuses cached data (skips 30-120s
  recomputation)
- cache cleared on success or drive/range change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: snapshot creation cache staleness + missing Drive-Id logging PE-9103

- always refresh MetadataCache on _reset() instead of ??= to avoid
  stale cache references from prior sessions
- clear _cachedSnapshotData on cancellation to prevent reusing
  partially computed data on retry
- log warning when snapshot transaction has no Drive-Id tag during
  batched prefetch (silent skip was hiding malformed snapshots)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: guard progress emit against disposed cubit PE-9103

If the user dismisses the snapshot dialog while computation is running,
the progress callback would call emit() on a closed cubit, crashing
with StateError. Now checks isClosed before emitting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: download success dialog shows filename instead of duplicate title PE-9103

- success dialog was showing "Download Finished" as both title and
  description — now shows the filename as description
- check saveResult in onDone handler so cancelled browser save dialogs
  don't incorrectly show the success modal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: turbo payment failure no longer blocks snapshot creation PE-9103

If the Turbo payment service is unreachable (e.g. payment.ardrive.dev
returns 404/500), the entire snapshot flow failed with
ComputeSnapshotDataFailure — even though AR payment would have worked.

Now wraps Turbo cost calculation in try-catch: on failure, Turbo is
marked unavailable and the confirmation dialog shows with AR as the
only payment option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: lazy-init MetadataCache to fix CI test failures PE-9103

Moving newSharedPreferencesCacheStore() into _reset() broke all 7
create_snapshot_cubit tests in CI — the shared_preferences plugin
isn't available in the test environment (no platform channel).

Now lazily initialized on first use in _jsonMetadataOfTxId() instead
of eagerly in _reset(). Cache is still cleared on reset (set to null)
so stale references are avoided.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: trigger CI run

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(version): bump version to 2.85.0 (#2156)

* PE-9103: Release v2.84.0 (#2148) (#2158)

* perf: prefetch next snapshot + streaming JSON parse PE-9103

Two optimizations for snapshot loading during sync:

1. Prefetch: start downloading the next snapshot body while the
   current one is being parsed and written to DB. Only 1 ahead to
   avoid gateway 429s. Overlaps network I/O with CPU work.

2. Streaming parse: instead of jsonDecode on the entire snapshot
   (which builds a massive Map with all entries in memory), scan
   the JSON string for individual txSnapshot object boundaries
   using brace counting and parse each one separately. This:
   - Avoids building the full nested Map (lower peak memory)
   - Starts yielding transactions immediately
   - Still needs the full string downloaded, but parsing is
     incremental

For a drive with 3 snapshots of ~15MB, ~10MB, ~5MB:
  Before: download1 + parse1 + download2 + parse2 + download3 + parse3
  After:  download1 + [parse1 || download2] + [parse2 || download3] + parse3



* Revert "perf: prefetch next snapshot + streaming JSON parse PE-9103"

This reverts commit 78331785e351f7dc5c02079ff82d0db6cfc76e6e.

* PE-9103: Bulk-load revision lookups instead of per-entity DB queries (#2142)

* perf: bulk-load revision lookups instead of per-entity DB queries PE-9103

During sync, for each file/folder entity, the code issued individual
SELECT queries to find the latest and oldest revisions. For a drive
with 19k files, that was 38k+ individual DB queries (19k latest +
19k oldest for files, plus similar for folders).

Changes:
- Add 4 bulk SQL queries to drive_queries.drift (latest/oldest ×
  files/folders) using GROUP BY with MAX/MIN dateCreated
- Add 4 helper methods to DriveDao returning Map<entityId, Revision>
- Pre-load all revision maps at the start of each transaction chunk
- Pass maps through to _addNewFileEntityRevisions,
  _addNewFolderEntityRevisions, _computeRefreshedFileEntriesFromRevisions,
  _computeRefreshedFolderEntriesFromRevisions
- Update latestRevisionsCache as new revisions are inserted so
  subsequent sub-batches see fresh data
- First-sync shortcut: skip all bulk queries when drive.lastBlockHeight
  is 0/null (every entity is new, no previous revisions exist)

Performance: 38,000+ individual SELECTs → 4 bulk SELECTs per chunk.
First sync: 0 queries (all skipped).



* perf: add 5s request timeout + reduce retries on data gateway PE-9103

Entity metadata fetches had NO timeout — a slow gateway could hang
indefinitely. With 2 retries × 5 gateways = 10 attempts with no
timeout, a single failed entity could take minutes.

Changes:
- Add 5-second timeout per HTTP request (metadata is tiny JSON)
- Reduce retries per gateway from 2 to 1 (move on, don't retry slow)
- Reduce GAR fallbacks from 3 to 2

Worst case per entity: 4 attempts × 5s = 20s max (was: 10 attempts,
unlimited time)



---------



* fix: add 15s total timeout on data fetch fallback chain PE-9103 (#2143)

A single missing/broken tx could block sync for minutes as the
fallback chain tried every gateway with CORS timeouts and 504s.

Added a 15-second total timeout wrapping the entire fallback chain
(primary → GAR gateways → arweave.net). Combined with the existing
5s per-request timeout, worst case for any single tx is now 15s max.

This prevents the sync from appearing "stuck" — even if metadata
fetches fail, the sync completes within a bounded time and the
periodic sync can trigger again.



* perf: prefetch next snapshot + streaming JSON parse PE-9103 (#2141)

Two optimizations for snapshot loading during sync:

1. Prefetch: start downloading the next snapshot body while the
   current one is being parsed and written to DB. Only 1 ahead to
   avoid gateway 429s. Overlaps network I/O with CPU work.

2. Streaming parse: instead of jsonDecode on the entire snapshot
   (which builds a massive Map with all entries in memory), scan
   the JSON string for individual txSnapshot object boundaries
   using brace counting and parse each one separately. This:
   - Avoids building the full nested Map (lower peak memory)
   - Starts yielding transactions immediately
   - Still needs the full string downloaded, but parsing is
     incremental

For a drive with 3 snapshots of ~15MB, ~10MB, ~5MB:
  Before: download1 + parse1 + download2 + parse2 + download3 + parse3
  After:  download1 + [parse1 || download2] + [parse2 || download3] + parse3



* fix: retry gateway price fetch and handle AR cost failure gracefully PE-9103

- getPrice now retries 3 times with backoff instead of failing on a
  single 502 from the gateway
- AR cost calculation in the upload modal falls back to zero estimate
  on failure so the modal still opens with Turbo available instead of
  crashing entirely



* fix: dateCreated null crash in file/folder entry refresh on first sync PE-9103

toEntryCompanion() on file/folder revision companions produces entry
companions with dateCreated = Value.absent() (schema has DEFAULT).
On first sync, oldestRevisionsCache is empty, so the fallback
.dateCreated.value returns null → JSNull crash on web.

Fixed by keeping a separate map of revision dateCreated values and
using those as the fallback instead of the entry companion's field.

Same bug pattern as the drive revision fix (line 1887), but for
files (line 1838) and folders (line 1871).



* perf: add drive owner to tx status and license gql queries PE-9126

Add an optional owners filter to the by-ID GraphQL queries that the
gateway struggles with (large ClickHouse row scans). Scoping by owner
lets the gateway prune its search space. The owner variable is nullable,
so when an owner can't be determined the query behaves exactly as before.

- TransactionStatuses, LicenseComposed, LicenseAssertions: add $owners
- getTransactionConfirmations / getLicenseComposed / getLicenseAssertions:
  accept an optional owner and pass it through
- tx status (per-drive): scope to the drive's ownerAddress
- tx status (global): scope to the logged-in wallet's address
- licenses: scope to the best-known tx owner (revision/file owner)

Edge case: data txs of files pinned from other authors are owned by
those authors and won't match the owner filter; treated as best-effort.



* fix: re-query owner-mismatched txs unscoped to avoid false failures PE-9126

Owner-scoped getTransactionConfirmations leaves cross-owner txs (e.g. the
data tx of a file pinned from another author's upload) at -1, which the
sync status logic would turn into 'failed' after the pending timeout.

Add a fallback pass: after the selective owner-scoped query, re-query any
ids still unresolved (-1) without the owner filter to determine their true
status. The residual set is normally tiny, so the selectivity win of the
first pass is preserved while confirmed cross-owner txs are no longer
misclassified as failed.



* fix: resolve owner-mismatched txs via pin owner, not unscoped re-query PE-9126

The previous fallback re-queried unresolved (-1) txs without an owner
filter, which can re-trigger the gateway's expensive row scan that the
owner scoping was meant to avoid — and, because the call is wrapped in a
5s timeout, a slow/erroring fallback discards the whole batch's results
and makes no progress, repeating every sync.

Replace it with a selective approach: only re-query unresolved ids whose
real on-chain owner is known locally. Currently that's pinned files,
whose data tx is owned by the original uploader (pinnedDataOwnerAddress).
These are re-queried scoped to that owner, so every query stays selective.
Genuinely missing txs have no override and are left unresolved, handled by
the caller's existing pending/failed logic — no unscoped scan, no retry
storm on the common "not found yet" case.

- add pinnedFileRevisions drift query
- getTransactionConfirmations: ownerOverrides param replaces unscoped pass
- sync repo: build pin owner overrides and pass them through



* fix: make pinned-owner confirmation recovery best-effort PE-9126

The second (pinned-owner) pass merges into the already-populated first-pass
map, so its failure must never discard first-pass progress. Wrap it in a
try/catch and bound it with its own 3s timeout: even if the pin re-queries
error or stall, the confirmations resolved by the owner-scoped first pass
are still returned.



* perf: preserve resolved confirmations across a timeout via verified sink PE-9126

getTransactionConfirmations is wrapped in a 5s timeout by the sync status
update; on expiry it previously returned an empty map, discarding every
confirmation already resolved that cycle (the whole 5000-tx page).

Add an optional caller-owned verifiedSink that the method populates with
each resolved confirmation (>= 0 only) as queries complete. The callers pass
one in and, on timeout, fall back to it instead of an empty map — so work
done before the deadline (including a fully-completed first pass when only
the pinned-owner pass is slow) is applied rather than thrown away.

Because the sink holds only positive verifications and never the -1
"not found" placeholders, applying it after a partial/timed-out run only
upgrades txs to confirmed and never marks anything failed off incomplete data.



* fix: bound confirmation fan-out and use type-safe id filtering PE-9126

- Cap the per-chunk GraphQL fan-out in getTransactionConfirmations to
  maxConcurrentDataFetches instead of launching every chunk at once, so a
  large pending-tx page can't burst into a concurrent-retry storm against
  the gateway (matches the throttling on the other gateway-heavy paths).
- Replace `sublist(...) as List<String>?` with `.whereType<String>().toList()`.
  The cast threw a TypeError for the pinned-owner pass (whose ids list is
  typed List<String?>), which the best-effort catch swallowed — silently
  disabling pin recovery. The filter yields a real List<String> regardless
  of input type.



* PE-9103: Fix empty explorer after drive attach (#2145)

* fix: empty explorer after drive attach PE-9103

Two bugs caused the explorer to show a permanent spinner after
attaching a drive (data was in DB but UI didn't update):

1. buildWhen guard in drive_detail_page.dart blocked BlocBuilder
   rebuilds when SyncCubit was SyncInProgress. If DriveDetailCubit
   emitted DriveDetailLoadSuccess during sync, the emission was
   permanently skipped with no replay mechanism. Removed the sync
   check — DriveDetailCubit already gates emissions via
   waitCurrentSync() in the Rx.combineLatest3 callback.

2. startSyncForDrive silently aborted when a sync was in progress.
   The .then(selectDrive) still fired, selecting a drive whose
   content was never synced. Changed to await waitCurrentSync()
   so the single-drive sync runs after the current sync finishes.



* perf: faster snapshot validation — skip fallback on 404, reduce timeouts PE-9103

Snapshot validation was slow on localhost (and anywhere Solana RPC is
unavailable): 2 HEAD retries × 10s timeout + GAR fallback via Solana
= 25+ seconds per snapshot. With 16 drives having multiple snapshots,
sync appeared stuck.

Changes:
- 1 HEAD attempt instead of 2 (fail fast, fall back to GQL)
- HEAD timeout 10s → 5s
- GAR list timeout 5s → 3s
- Skip GAR fallback entirely when primary returned 404 (snapshot
  doesn't exist, no point trying other gateways via Solana RPC)
- Only try GAR fallback for transient errors (timeout, 5xx)



* fix: guard startSyncForDrive race after waitCurrentSync PE-9103

Two callers could both pass the SyncInProgress guard after
waitCurrentSync() returned, causing concurrent single-drive syncs.
Re-check state after wait — if another sync started, bail out.



---------



* fix: bump arweave-dart to v4.0.2 to fix file download crash

Pulls in the TransactionData null-field fix so downloads no longer crash
on web with "type 'JSNull' is not a subtype of type 'String'" when a
gateway returns null for optional tx fields (e.g. anchor on
turbo-gateway.com). Updates both the dependency and the override.



* chore(version): bump version to 2.84.0



---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ariel Melendez <ariel@ardrive.io>
Co-authored-by: arielmelendez <ariel.l.melendez@gmail.com>

* PE-9103: Refresh balance after Turbo topup from profile dropdown (#2163)

* fix: refresh AR balance after Turbo topup from profile dropdown PE-9103

The profile card's topup button was not passing an onSuccess callback
to showTurboTopupModal, so no balance refresh happened after a
successful topup. Turbo credits update automatically on next dropdown
open (fresh TurboBalanceCubit), but the AR balance was stale if the
user paid with AR.

Now passes onSuccess to call profileCubit.refreshBalance() after
successful topup, matching the pattern already used in
payment_method_selector_widget.dart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace deprecated EquatableMixin with Equatable extends PE-9103

EquatableMixin is deprecated in favor of using Equatable directly.
Changed IndexedItem from `with EquatableMixin` to `extends Equatable`
with const constructor. This fixes the CI analyze failure in the
ardrive_ui package.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: point development flavor at the ar-io.dev testnet (#2167)

Updates all four service endpoints in the dev (development) flavor to the
new AR.IO testnet:
- GraphQL gateway: ardrive.net -> ar-io.dev
- data gateway: turbo-gateway.com -> ar-io.dev (label "AR.IO Testnet")
- turbo upload: upload.ardrive.dev -> upload.services.ar-io.dev
- turbo payment: payment.ardrive.dev -> payment.services.ar-io.dev

All four verified reachable (HTTP 200, graphql answers a query). Staging
and prod are unchanged; only local `--dart-define=environment=development`
runs hit the testnet.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* chore: remove typeform feedback survey, link new help page PE-9150 (#2168)

The typeform survey (pds-inc.typeform.com/ardrive) is no longer
monitored. The post-share prompt was already disabled at the cubit
level, leaving the sidebar link as the only live entry point.

- remove Resources.surveyFeedbackFormUrl and openFeedbackSurveyUrl()
- delete FeedbackSurveyCubit/State, FeedbackSurveyModal and their test
- unwire openRemindMe() from the drive and file share dialogs, the
  router delegate listeners, and the main.dart provider
- point Resources.helpCenterLink at https://ardrive.io/help (it was an
  unused constant aimed at /contact) and surface it as a "Help Center"
  link at the top of the support modal's Resources list, replacing the
  "Leave Feedback" row
- drop the six now-unused l10n keys from all locales; weWontRemindYou
  is kept since prompt_to_snapshot_dialog still uses it

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* PE-9132: Turbo free-tier support — allowance-aware upload UX and typed payment failures (#2166)

* docs: implementation plan for turbo free-tier restriction PE-9132

10 MiB free pool per wallet, 105 KiB per-item eligibility, paid-only
after exhaustion, credits never replenish free. Inventories the 15
silent-free posting paths, the current failure behavior on payment
rejection, and phases the work: failure honesty (unblocked now),
pool-aware eligibility (needs turbo API contract), surfacing UX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: phase 1 of turbo free-tier readiness - typed payment failures PE-9132

Prepares the client for the restricted free tier (10 MiB pool, 105 KiB
per-item, paid-only after exhaustion). Policy-independent hardening:

- decode HTTP 402 into TurboPaymentRequiredException and 429 into
  TurboRateLimitException in the app-side TurboUploadService (pure
  turboExceptionForStatusCode mapping, unit tested); the uploader
  package maps 402 to its existing UnderFundException and excludes
  402/429 from its 8-attempt retry loops (retrying payment rejections
  multiplies load and metered usage)
- rename (file/folder) failures now dismiss the progress dialog and
  show an honest error - payment-specific copy when the rejection was
  402 (previously: spinner forever)
- move gains a failure state and dialog handling, no longer emits
  Success after an error (removes the TODO admitting it), and is
  reordered to post-then-commit: data items are prepared and posted
  BEFORE the local database transaction, so a rejected move can no
  longer leave local state claiming a move the chain never saw
- TurboUploadService fetches maxItemBytes from GET /v1/info once at
  construction (maxFreeItemSizeBytes, config fallback) - the
  server-driven per-item free threshold per the descoped plan; wiring
  it into UploadPaymentEvaluator is the next commit
- implementation plan doc updated with the decision: no pool tracking,
  no balance-endpoint dependency, static free-tier messaging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: analyzer errors in phase 1 (entities import, stub private member)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: payment-aware failure UX across all metadata ops + server threshold PE-9132

Completes the free-tier client readiness: every operation that posts to
Turbo now recognizes a payment rejection and shows one consistent,
actionable message instead of a generic error or (previously) a hang.

- new shared TurboPaymentRequired dialog (ArDriveStandardModalNew with a
  "Buy Credits" action into the existing top-up flow) as the single
  source of truth for the free-allowance-used-up UX; localized strings
  freeAllowanceUsedUpTitle/Description added to all six ARB files
- new isTurboPaymentError() classifier; each op's failure state carries
  an isPaymentError flag set from the caught exception:
  rename, move, drive rename, folder create, drive create, hide/unhide,
  pin, license, ghost fixer
- every corresponding form/dialog branches to the shared payment dialog
  on payment errors and keeps its existing generic error otherwise;
  folder-create, drive-rename and ghost-fixer gained the failure
  handling they previously lacked
- UploadPaymentEvaluator now resolves the free per-item threshold from
  the server (TurboUploadService.maxFreeItemSizeBytes via /v1/info),
  falling back to allowedDataItemSizeForTurbo; wired through DI for the
  main upload flow (metadata/manifest paths keep the config fallback)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: const DriveRenameFailure constructor, drop unused hide import

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address CodeRabbit review on free-tier UX PE-9132

- include isPaymentError in Equatable props on every failure state
  (drive/folder create, drive/folder/file rename, hide, ghost fixer,
  license) so a payment failure emitted after a generic one is not
  treated as an equal state and actually re-triggers the listener
- license: preserve the original exception via logger.e before addError
- license failure card: on a payment error the action becomes "Buy
  Credits" (opens the shared payment dialog) instead of retrying the
  same rejected operation
- localize the generic metadata-op failure message via a shared
  actionFailedTryAgain key across all locales, replacing hardcoded
  English descriptions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: extend payment-failure UX to file uploads and all remaining post paths PE-9132

Audit found the metadata ops were covered but the highest-visibility
upload surfaces were not. Closes those gaps.

Cross-cutting fix — the two upload services throw different payment
exceptions (app-side TurboPaymentRequiredException vs ardrive_uploader
package UnderFundException, sometimes wrapped in UploadStrategyException).
isTurboPaymentError() now recognizes all of them; ardrive_uploader
exports its exceptions so the app can classify them.

Newly covered:
- main file upload and folder upload: UploadCubit classifies a payment
  rejection from the failed-task list into UploadErrors.turboPaymentRequired
  (UploadFailure gains isPaymentError-carrying props); the failure widget
  shows the Buy-Credits dialog instead of a "Re-Upload" that would 402 again
- post-upload ArNS name assignment: wrapped in try/catch so a rejected
  name data item can no longer hang an already-successful upload (the
  name can be reassigned later)
- snapshot creation, manifest creation (unwrapping the task-list error
  through ManifestCreationException), standalone ArNS assignment, bulk
  import (unwrapping FileMetadataUploadException.originalError), and
  single/multi thumbnail creation all classify payment errors and show
  the shared dialog

Deferred (documented): private-drive migration and login verification
posts — tiny, effectively always-free signature items where a payment
dialog mid-flow would be worse UX than the near-impossible failure.

Co-Authored-By: Claude…
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.

1 participant