Raised from device QA: the super app's Coins tab has expenses, budgets, and groups/split, while the standalone Airo Coins app has only the vault. Same product name, two different feature sets. Here is why, measured on origin/main, and what it takes to fix.
Where Coins code actually lives
| Location |
Files |
Contents |
app/lib/features/coins/ |
81 |
dashboard, add_expense, add_budget, budget_management, groups_list, group_detail, add_split_expense — app-only |
app/lib/features/bill_split/ |
~10 |
split domain + services + receipt OCR — app-only |
packages/feature_coin/ |
21 |
vault only |
packages/feature_coins_core/ |
59 |
services (ingestion, review, …) — already packaged ✓ |
packages/feature_billsplit_core/ |
2 |
migration started and abandoned |
main_coins.dart registers exactly one module, CoinVaultModule, so the standalone app is vault-only by construction. And CoinVaultModule itself lives in app/lib/core/coins/coin_vault_module.dart, not in the package it registers — packages/feature_coin declares zero GoRoutes of its own. So even the wiring is app-owned.
Per ADR-0010 app/lib/features/coins is a frozen migration source, so this is a migration that stalled after the services layer, not a deliberate split.
The constraint any plan has to respect
app/lib/features/bill_split imports google_mlkit_text_recognition and image_picker for receipt OCR. The Coins profile has a 25 MB enforced release budget against a 19.78 MB baseline — about 5 MB of headroom. MLKit text recognition alone is well past that, which is exactly why the TV profile swaps it for packages/stubs/mlkit_stub.
So "move everything into the package and register it everywhere" would break the Coins budget on the first build. Split has to arrive as an optional capability, with OCR behind the existing stub seam.
Suggested order
- Move
CoinVaultModule into packages/feature_coin. A module belongs with the feature it registers. Small, mechanical, unblocks the rest.
- Move dashboard / add_expense / add_budget / budget_management into
packages/feature_coin and add them to the module's routes. Both shells pick them up from one place; the super app keeps mounting at /money, the standalone at /vault's sibling routes.
- Finish
packages/feature_billsplit_core: split domain + services out of app/lib/features/bill_split, UI into a feature package, exposed as a BillSplitModule. Register it unconditionally in the super app. Register it in the Coins flavour only with OCR resolved through mlkit_stub, and verify against the 25 MB gate before it lands.
- Delete
app/lib/features/coins once empty — ADR-0010 already names removal as the end state.
Each step is independently shippable and leaves both apps working.
Why it is worth doing
This is the same class of problem as #1362's forced-portrait bug, where feature_iptv had the fix and the super app's own shell copy did not. IPTV is the one domain that finished its migration, and it is the one where a fix reaches both apps for free. Coins currently needs every fix applied twice, and the standalone app silently lacks features users expect from the name.
Filed from a QA pass rather than started as code: steps 2 and 3 move ~90 files across a module boundary and need a coins/architecture owner on the route contract and on whether split ships in the standalone flavour at all.
Raised from device QA: the super app's Coins tab has expenses, budgets, and groups/split, while the standalone Airo Coins app has only the vault. Same product name, two different feature sets. Here is why, measured on
origin/main, and what it takes to fix.Where Coins code actually lives
app/lib/features/coins/app/lib/features/bill_split/packages/feature_coin/packages/feature_coins_core/packages/feature_billsplit_core/main_coins.dartregisters exactly one module,CoinVaultModule, so the standalone app is vault-only by construction. AndCoinVaultModuleitself lives inapp/lib/core/coins/coin_vault_module.dart, not in the package it registers —packages/feature_coindeclares zeroGoRoutes of its own. So even the wiring is app-owned.Per ADR-0010
app/lib/features/coinsis a frozen migration source, so this is a migration that stalled after the services layer, not a deliberate split.The constraint any plan has to respect
app/lib/features/bill_splitimportsgoogle_mlkit_text_recognitionandimage_pickerfor receipt OCR. The Coins profile has a 25 MB enforced release budget against a 19.78 MB baseline — about 5 MB of headroom. MLKit text recognition alone is well past that, which is exactly why the TV profile swaps it forpackages/stubs/mlkit_stub.So "move everything into the package and register it everywhere" would break the Coins budget on the first build. Split has to arrive as an optional capability, with OCR behind the existing stub seam.
Suggested order
CoinVaultModuleintopackages/feature_coin. A module belongs with the feature it registers. Small, mechanical, unblocks the rest.packages/feature_coinand add them to the module's routes. Both shells pick them up from one place; the super app keeps mounting at/money, the standalone at/vault's sibling routes.packages/feature_billsplit_core: split domain + services out ofapp/lib/features/bill_split, UI into a feature package, exposed as aBillSplitModule. Register it unconditionally in the super app. Register it in the Coins flavour only with OCR resolved throughmlkit_stub, and verify against the 25 MB gate before it lands.app/lib/features/coinsonce empty — ADR-0010 already names removal as the end state.Each step is independently shippable and leaves both apps working.
Why it is worth doing
This is the same class of problem as #1362's forced-portrait bug, where
feature_iptvhad the fix and the super app's own shell copy did not. IPTV is the one domain that finished its migration, and it is the one where a fix reaches both apps for free. Coins currently needs every fix applied twice, and the standalone app silently lacks features users expect from the name.Filed from a QA pass rather than started as code: steps 2 and 3 move ~90 files across a module boundary and need a coins/architecture owner on the route contract and on whether split ships in the standalone flavour at all.