-
Notifications
You must be signed in to change notification settings - Fork 20
PE-9132: Turbo free-tier support — allowance-aware upload UX and typed payment failures #2166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
2e40f3b
docs: implementation plan for turbo free-tier restriction PE-9132
vilenarios e6a88ec
feat: phase 1 of turbo free-tier readiness - typed payment failures P…
vilenarios c771fa4
fix: analyzer errors in phase 1 (entities import, stub private member)
vilenarios fe9d9ac
feat: payment-aware failure UX across all metadata ops + server thres…
vilenarios 047645e
fix: const DriveRenameFailure constructor, drop unused hide import
vilenarios f40205b
fix: address CodeRabbit review on free-tier UX PE-9132
vilenarios dd38535
feat: extend payment-failure UX to file uploads and all remaining pos…
vilenarios 853f553
fix: resolve analyzer issues from upload-surface coverage
vilenarios 9b46403
fix: close the two remaining payment-UX holes found in verification P…
vilenarios db0443c
fix: surface payment errors swallowed by the shared WorkerPool PE-9132
vilenarios 2ab0dc3
nit: const MultiThumbnailCreationError emit
vilenarios 58b560e
feat: config-based direct-to-network fallback for private-drive migra…
vilenarios b5fb639
fix: trigger payment dialog from BlocConsumer listener, not builder P…
vilenarios aec1326
Merge remote-tracking branch 'origin/dev' into feat/turbo-free-tier
vilenarios 6fa15ee
feat: only promise a free upload when the wallet allowance covers it …
de85bfb
fix: close two free-allowance gaps in the manifest upload paths PE-9132
eadd175
refactor: model free-tier state as one value and render it in one wid…
90f8e46
fix: address CodeRabbit review on the free-tier work PE-9132
45569a1
fix: check free allowance regardless of turbo flag; dismiss thumbnail…
a60a001
feat: distinguish an upload exceeding the free allowance from it bein…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| # Turbo Free-Tier Restriction — ArDrive Client Implementation Plan | ||
|
|
||
| ## Policy (PE-9132) | ||
|
|
||
| - Each wallet receives a **10 MiB free pool**. | ||
| - An upload is free-eligible only when the data item is **≤ 105 KiB (107,520 bytes)** AND the pool has remaining room. Eligible items draw down the pool by their size. ArFS metadata items (~1–2 KiB: renames, moves, creates, hides, pins, license assertions, bulk-import entries, thumbnails metadata) draw from the same pool. | ||
| - When the pool is exhausted, **every** upload is paid — including metadata operations. Purchasing credits does **not** replenish the pool; free and credits are independent. | ||
| - Reset cadence (monthly vs lifetime) is server policy; the client must treat remaining-free as **server-reported state**, never a client-side computation. | ||
|
|
||
| ## Current client reality (verified inventory, July 2026) | ||
|
|
||
| "Free" today is a client-side guess: anything under `allowedDataItemSizeForTurbo` | ||
| (100,000 bytes; `assets/config/*.json:12`) is posted to `POST /v1/tx` with **no | ||
| cost check, no payment header, no quota awareness** (`lib/turbo/services/upload_service.dart:59-106`). | ||
|
|
||
| **Fifteen operation paths post silently on this assumption** (all wallet-signed, | ||
| none has payment UI): | ||
|
|
||
| | Op | Post site | | ||
| |---|---| | ||
| | 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` | | ||
| | Hide/unhide | `lib/blocs/hide/hide_bloc.dart:330` | | ||
| | Folder create | `lib/blocs/folder_create/folder_create_cubit.dart:85` | | ||
| | Drive create (+root folder) | `lib/blocs/drive_create/drive_create_cubit.dart:134` | | ||
| | Pin file | `lib/blocs/pin_file/pin_file_bloc.dart:337` | | ||
| | License assertion (2/file×rev) | `lib/blocs/fs_entry_license/fs_entry_license_bloc.dart:320` | | ||
| | Ghost fixer | `lib/blocs/ghost_fixer/ghost_fixer_cubit.dart:141` | | ||
| | ArNS name revision | `lib/arns/domain/arns_repository.dart:196` | | ||
| | Thumbnail metadata | `lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart:210` | | ||
| | Private-drive migration | `lib/shared/blocs/private_drive_migration/private_drive_migration_bloc.dart:134` | | ||
| | Bulk import folder meta (per folder) | `lib/core/arfs/use_cases/upload_folder_metadata.dart:81` | | ||
| | Bulk import file meta (per file) | `lib/core/arfs/use_cases/upload_file_metadata.dart:80` | | ||
| | Snapshot create (has payment UI already) | `lib/blocs/create_snapshot/create_snapshot_cubit.dart:693` | | ||
|
|
||
| **Known failure behavior on payment rejection today** (nothing decodes 402/429): | ||
|
|
||
| - Rename: progress dialog never dismisses (`fs_entry_rename_form.dart:76-137` has no failure case). | ||
| - Move: no failure state at all (`fs_entry_move_state.dart`), progress dialog hangs; **DB commits BEFORE the network post** (`fs_entry_move_bloc.dart:261-279`) → local state diverges from chain on rejection. | ||
| - Chunked uploader retries any failure **8×** including payment rejections (`packages/ardrive_uploader/lib/src/turbo_upload_service.dart:28,99-100`). | ||
| - App-side `TurboUploadService._handleException` special-cases only 408 (`upload_service.dart:108-129`); everything else becomes a generic `Exception`. | ||
| - No quota/allowance API is consumed anywhere; balance response (`payment_service.dart:77`) has no free-tier fields today. | ||
|
|
||
| ## Budget math (why per-op UX must be silent) | ||
|
|
||
| - Metadata op ≈ 2 KiB → the 10 MiB pool covers ~5,000 metadata operations. | ||
| - A single 105 KiB file consumes ~50 metadata-ops worth of pool; ~97 max-size files exhaust the pool. | ||
| - Bulk import of a 1,000-file manifest ≈ 2 MiB of pool in one click. Folder | ||
| uploads of many small files can exhaust the pool in one action. | ||
| - Conclusion: prompting per metadata op is unacceptable (dust); prompting per | ||
| BURST (bulk import, folder upload) with a pool-aware preflight is required. | ||
|
|
||
| ## Phase 1 — Failure honesty (policy-independent; build first, no server dependency) | ||
|
|
||
| 1. **Typed payment errors.** In both TurboUploadServices: decode HTTP 402 (and | ||
| distinguish free-exhausted vs insufficient-credits when the server provides a | ||
| reason code) into `TurboPaymentRequiredException`; decode 429 into a typed | ||
| rate-limit error. Exclude both from blind retry (`retryIf`). | ||
| 2. **Fix the stuck dialogs.** Add failure states + dialog handling for Rename | ||
| (both file/folder) and Move; generic message now, payment-specific once | ||
| Phase 2 lands. | ||
| 3. **Move: post-then-commit.** Reorder `fs_entry_move_bloc` so network posts | ||
| succeed before DB writes commit (or wrap in a rollback) — fixes the | ||
| divergence bug independent of any free-tier change. | ||
| 4. **Tests:** unit tests for error decoding; bloc tests for rename/move failure | ||
| states; regression test that a 402 is not retried. | ||
|
|
||
| ## DECISION (2026-07-15, after review with Turbo team) | ||
|
|
||
| Phase 1 green-lit. The balance-endpoint remaining-free dependency and the | ||
| dynamic pool meter are DROPPED: no pool tracking client-side. Instead: | ||
| - **Static free-tier messaging** — when a post is rejected with 402, show a | ||
| static explanation ("free allowance is used up — add Credits") with the | ||
| top-up path. No live "X MiB remaining" anywhere. | ||
| - **`maxItemBytes` from `GET /v1/info`** is kept as the server-driven | ||
| per-item eligibility threshold (implemented: | ||
| `TurboUploadService.maxFreeItemSizeBytes`, fetched once at construction, | ||
| config value as fallback). Follow-up: consume it in | ||
| `UploadPaymentEvaluator` in place of `allowedDataItemSizeForTurbo`. | ||
| - Typed-402 handling everywhere (Phase 1) is the backbone of the UX. | ||
|
|
||
| The original Phase 2/3 below is retained for reference but is NOT the | ||
| current plan; only the pieces named above survive. | ||
|
|
||
| ## Phase 2 — Pool-aware eligibility (SUPERSEDED by decision above) | ||
|
|
||
| **Server asks (blockers for this phase, not Phase 1):** | ||
| - Remaining-free bytes (+ reset timestamp if any) on the existing balance | ||
| endpoint (`GET /v1/account/balance/arweave`) — client already polls it. | ||
| - Deterministic 402 with machine-readable reason: `free_exhausted` | | ||
| `insufficient_credits`. | ||
| - Confirm per-item eligibility threshold (105 KiB) is queryable or stable. | ||
|
|
||
| **Client work:** | ||
| 1. **`TurboConditionsService`** (new, `lib/turbo/`): caches | ||
| `{creditBalance, freeRemainingBytes, freeItemLimit=107520}`; refreshed on | ||
| login, after every upload, and immediately on any 402. Single source of | ||
| truth; all "is this free?" questions go here. | ||
| 2. **Retire the client-side constant as authority.** `allowedDataItemSizeForTurbo` | ||
| (100 KB) becomes the fallback for the item-size gate only (bump to 107,520); | ||
| eligibility = size-gate AND `freeRemainingBytes >= itemSize`. | ||
| 3. **Decision ladder for every post** (file uploads AND all 15 metadata paths): | ||
| - free-covered → post silently (pool decremented server-side; client | ||
| decrements optimistically, reconciles on next balance poll) | ||
| - credits cover it → post silently for dust-sized metadata; show cost line | ||
| for user-perceivable sizes (uploads keep existing payment UI) | ||
| - neither → pre-flight prompt with top-up flow (replaces today's hang) | ||
| - server 402 anyway (stale cache) → typed error → refresh conditions → one | ||
| honest dialog, never blind retry | ||
| 4. **Payment evaluator changes** (`lib/core/upload/uploader.dart:379-382, | ||
| :464-476, :534-564`; `upload_payment_method_bloc.dart:111-137`): | ||
| `isFreeUploadPossibleUsingTurbo` must consult the conditions service, not | ||
| just size. Multi-file: free only if the whole plan fits remaining pool; | ||
| otherwise split display (N free / M paid) or fall to paid entirely | ||
| (simpler v1: all-or-nothing per upload plan). | ||
| 5. **Burst preflight.** Bulk import (`bulk_import_files.dart`) and folder | ||
| uploads estimate total bytes against the pool up front: "This import needs | ||
| 2.1 MiB; you have 0.8 MiB free — the rest uses ~0.0004 credits | ||
| [Continue] [Top up]". On mid-burst exhaustion: pause-and-resume, not | ||
| fail-halfway (bulk import's failure tracking is the foundation). | ||
|
|
||
| ## Phase 3 — Surfacing (UX) | ||
|
|
||
| - FREE badge (`upload_form.dart:1937-1948`, `create_snapshot_dialog.dart:431`) | ||
| becomes dynamic: "Free — X MiB left" / absent when exhausted. | ||
| - Pool meter + low-pool nudge in the profile card next to credits | ||
| (`profile_card.dart:568-592` area); "free used up — everything now uses | ||
| credits" one-time notice on the free→paid transition. | ||
| - Onboarding guard: drive-create is a new wallet's first action (~2 KiB). With | ||
| a fresh 10 MiB pool this always succeeds; the dead-end only exists for | ||
| exhausted wallets — those get the pre-flight prompt, never a hang. | ||
|
|
||
| ## Explicit non-goals / accepted | ||
|
|
||
| - No client-side enforcement (abusers bypass the app; enforcement is Turbo's). | ||
| Client goals: honest UX for legitimate users + never amplifying rejected | ||
| load (no 402 retries). | ||
| - No hardcoded reset cadence; display whatever the server reports. | ||
| - L1 (direct Arweave) fallback for metadata ops remains available where it | ||
| exists today (rows 1–10, 13–14) for users who prefer paying AR — unchanged. | ||
|
|
||
| ## Sequencing | ||
|
|
||
| Phase 1 is unblocked today and ships value regardless of policy timing. | ||
| Phase 2 blocks on the two server asks. Phase 3 rides Phase 2. Estimated: | ||
| Phase 1 ≈ 2–4 days incl. tests; Phase 2 ≈ 1 week client-side once the API | ||
| contract exists; Phase 3 ≈ 2–3 days. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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