Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ melos run test

| Path | Owns |
|---|---|
| `app/` | Flutter application shell |
| `app/` | Flutter shell: Identity, Avatar, Store, Share, Passport tabs |
| `packages/identity_core/` | Identity model, privacy flags, local persistence |
| `packages/avatar_core/` | Avatar definition model |
| `packages/avatar_renderer/` | 3D avatar rendering (Filament via thermion_flutter) |
| `packages/store/` | Cosmetic catalog, entitlements (stubbed purchases), avatar valuation |
| `packages/sharing/`, `nfc/`, `qr/` | Stubs — future milestones |
| `rust/core/` | Rust identity domain struct + serialization |
| `packages/store/` | Cosmetic catalog, RevenueCat entitlements, valuation |
| `packages/sharing/` | Share payloads, `amiro://encounter` / legacy `amiro://share` codecs |
| `packages/nfc/` | NFC read (NDEF) and Android HCE emulate |
| `packages/qr/` | QR widget for share URIs; scan UI lives in `app/` |
| `packages/discovery/` | Encounter ledger, passport, encounter state machine (see README) |
| `rust/core/` | Rust identity domain struct + serialization (not wired to Dart yet) |

License: MIT.
14 changes: 7 additions & 7 deletions TODOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ of `app/lib/avatar/avatar_screen.dart` + cosmetic asset swap.
`app/lib/app.dart`'s `_RootTabs` has five destinations: Identity, Avatar, Store, Share, and Passport. The original note said Share was missing; that screen and the later Passport tab are both wired.

## 6. Store: browsable catalog with on-avatar preview + valuation — v1 DONE (2026-09-23)
`packages/store/` (catalog, entitlements, valuation) and `app/lib/store/` (StoreScreen, providers) built with TDD, wired into the nav shell as the Store tab. Catalog matches assets that actually exist today: 2 free items (Classic Frame glasses, Signature Tee) + 1 paid item (Riviera Optics glasses, $2.99) — original brand-inspired naming, no real trademarks, per the design review's legal call. Tapping any item previews it live on the avatar (`AvatarRenderer.updateSlot`) and persists it, regardless of ownership; priced+unowned items show a "Buy" button that grants a **local, stubbed entitlement** — no real money moves, this is a placeholder for StoreKit/Play Billing (a separate milestone per `docs/product/requirements.md`'s Monetization section). Avatar valuation (sum of equipped items' catalog price) now shows in the AvatarScreen app bar.
`packages/store/` (catalog, entitlements, valuation) and `app/lib/store/` (StoreScreen, providers) built with TDD, wired into the nav shell as the Store tab. Catalog today is 11 items in `packages/store/lib/src/cosmetic_listing.dart` (free basics plus three paid Founders-series cosmetics billed through Play). Tapping any item previews it live on the avatar (`AvatarRenderer.updateSlot`) and persists it, regardless of ownership; priced+unowned items show **Buy**, which runs real Play Billing on Android via RevenueCat (see #15 — `main.dart` must not override `entitlementStoreProvider` with a file store). Avatar valuation (sum of equipped items' catalog price) shows in the AvatarScreen app bar.

**Decisions (2026-10-15):**
- Valuation stays the sum of *equipped* catalog prices, including a paid item the glasses toggle or a Store preview put on the avatar. That matches `avatarValuationCents` and the Avatar screen tests. Owned-only valuation waits until the glasses toggle is a real inventory control and no longer equips `glasses_realistic` by itself.
Expand All @@ -35,9 +35,9 @@ of `app/lib/avatar/avatar_screen.dart` + cosmetic asset swap.
Inspired by the Steam "Egg" market (rarity ladder, Series #N drops). `CosmeticListing` now has `rarity` (`CosmeticRarity`: common..legendary) and optional `series` (`CosmeticSeries`); the 3 paid items are Series #1 "Founders" (Rare / Uncommon / Uncommon), free items stay Common. `avatarRarityScore` reports the highest equipped tier + per-tier counts; StoreScreen shows a rarity label per row and groups series under a header; AvatarScreen shows the highest equipped tier beside the valuation. Plan: `docs/superpowers/plans/2026-09-26-store-rarity-series.md`.

**Known gaps, deliberately deferred:**
- No rarity filter/sort in the store (7 items don't need it; add once the catalog grows).
- No rarity filter/sort in the store (11 catalog items don't need it yet; add once the catalog grows).
- Epic/Legendary tiers exist but no item uses them yet; they need new assets.
- No resale/trading market (Egg's market is a Steam feature; Amiro's purchases are stubbed). A real secondary market needs its own payments, fraud and legal decision.
- No resale/trading market (Egg's market is a Steam feature; primary purchases are real on Android but there is no secondary market). A real resale layer needs its own payments, fraud and legal decision.
- Meme/cultural skins: original themes only. No real memes or trademarks (`docs/legal/asset-policy.md`).
- `docs/product/requirements.md` not updated with a rarity section.

Expand All @@ -50,7 +50,7 @@ Inspired by the Steam "Egg" market (rarity ladder, Series #N drops). `CosmeticLi
- Next per the agreed sequence: Identity Card (QR first, then NFC), then Founding Identity. Real scarcity/editions and drops need server-authoritative issuance and are not started.

## 9. Identity card: collection on the shared profile — v1 DONE (2026-09-26)
The share URI (QR and NFC) now carries the sender's collected cosmetic ids (`owned`, sorted, capped at `maxSharedCosmeticIds` = 32; free items plus purchases via `collectedCosmeticIds`). `SharedProfileScreen` shows a Collection section: rarest item with its rarity label, and per-series progress (`SeriesProgressRow`, shared with the store list). Older payloads without the field, malformed fields, and ids this app version doesn't know all degrade to "no collection section" instead of rejecting the profile. Still payload v1: the field is additive.
In v1 (2026-09-26) the legacy `amiro://share` URI carried the sender's collected ids (`owned`, sorted, capped at `maxSharedCosmeticIds` = 32). Since the PR7 cutover (#18, `289788b`) the app shares `amiro://encounter`, whose payload carries per-series `collectionCompletion` computed by `collectionProgress` (`buildOwnEncounterPayload` in `app/lib/sharing/own_encounter.dart`; emitted from `app/lib/sharing/share_screen.dart` via `buildOwnEncounterUri`); `buildShareUri` / `parseShareUri` in `packages/sharing/lib/src/share_payload_codec.dart` remain only for legacy links (`parseShareUri` in `app/lib/sharing/scan_result.dart`), and the app no longer calls `buildShareUri`. `SharedProfileScreen` shows a Collection section: rarest item with its rarity label, and per-series progress (`SeriesProgressRow`, shared with the store list). Older payloads without the field, malformed fields, and ids this app version doesn't know all degrade to "no collection section" instead of rejecting the profile. Still payload v1: the field is additive.

**Known gaps, deliberately deferred:**
- The collection is self-reported and unverified. It is a show-off surface, not proof of ownership; real provenance needs server-authoritative issuance.
Expand All @@ -67,7 +67,7 @@ PR1 decisions worth remembering:
- The spec's `CollectionProgress` is named `SeriesCompletion` in `sharing`: `store` already exports a `CollectionProgress`, and the app imports both.
- `EquippedCosmeticInfo.seriesId` is nullable (free items sit outside any series).
- Cosmetic name/rarity on the wire are sender-claimed. A receiver that knows the id should prefer its own catalog and treat the wire values as a fallback; this is not proof of ownership.
- **Two formats coexist until PR4:** the existing `amiro://share?d=` URI (`SharedProfile`, has username and contact fields) still drives the app; `AmiroSharingPayload` is not wired in yet. The QR bridge must decide how contact fields and `username` map across (they are absent from the new payload).
- **Wire format (historical note):** PR4 planned two coexisting URIs; PR7 cutover shipped `amiro://encounter` for new shares while receive still accepts legacy `amiro://share` (`app/lib/sharing/scan_result.dart`). Contact fields ride on the optional `c=` parameter (`ContactCard`), not inside the payload JSON.

PR2 decisions (`packages/discovery`, library only, not wired into the app):
- `EncounterRecord` is immutable (spec had mutable fields); an encounter yields a new record via `copyWith`. `localRecordId` (ULID-style, `newLocalRecordId`) is independent of `remoteIdentityId`.
Expand Down Expand Up @@ -105,7 +105,7 @@ PR7-9 decisions (shipped together as one PR; taken autonomously while the owner
- **Only their avatar is 3D** on the comparison screen; yours is a text summary. Two live Filament views side by side is unverified on a device.
- **Contact fallback.** If the contact card is what pushes the link past the QR size limit, `buildOwnEncounterUri` drops it and shares the identity alone rather than failing to share (the library's `encodeEncounterUri` still throws; only the app chooses to fall back).
- **Passport**: 5th tab; newest-met first; entry detail shows the latest known look; "Remove from passport" (confirmed) deletes the entry, after which that person counts as new. No 3D thumbnails in the list (too heavy per row). A corrupt ledger file shows "couldn't be opened" and saving fails visibly; it is never overwritten.
- **Completion engine** (`completedSeries`, `isSetComplete`, `SeriesCompletion.isComplete`): derived from catalog + entitlements, never stored, no remote validation. The v1 reward is a 2px brass **collector's frame** on the identity card and passport entries (DESIGN.md's equipped-card border), because no avatar asset exists to unlock. An avatar-level reward stays open until there is an asset.
- **Completion engine** (`collectionProgress`, `CollectionProgress.isComplete`, `SeriesCompletion.isComplete`): derived from catalog + entitlements, never stored, no remote validation. The v1 reward is a 2px brass **collector's frame** on the identity card and passport entries (DESIGN.md's equipped-card border), because no avatar asset exists to unlock. An avatar-level reward stays open until there is an asset.
- **Not built (deliberately):** Sparks / milestones on the unique-encounter count (`EncounterProcessor.uniqueEncounterCount` is the primitive), encounter history snapshots, real scarcity/editions, signed provenance, a rarity filter in the store, an exportable card image.
- **Not eyeballed on a device**: card and encounter screen layout, the 5-item navigation bar, and NFC end to end.

Expand Down Expand Up @@ -141,7 +141,7 @@ This was the last item from the original critique's "include" list; everything r
## 15. Real payments: RevenueCat + Play Billing — DONE (2026-09-29)
The Store's mock "demo purchase" dialog is gone; `packages/store/lib/src/revenuecat_entitlement_store.dart` now calls real Play Billing through `purchases_flutter`/RevenueCat. Catalog on the RevenueCat side (project `Amiro`, `proj9768d0fc`) is fully set up via `rc` CLI: Android app registered (`app51bedf1b9c`, package `com.developerscoffee.amiro_app`), 3 one-time products (`amiro_riviera_optics`, `amiro_long_flow`, `amiro_full_beard`) each attached to their own entitlement (`cosmetic_<id>`). A webhook (`whintgrb5c7c2a4a3`) posts purchase events to a Cloudflare Worker (`workers/revenuecat-webhook/`, deployed at `amiro-revenuecat-webhook.developerscoffee.workers.dev`, free tier), which logs every event and upserts entitlement grants into a Supabase Postgres project (`amiro`, `srvhqmfjqmxxvrdctbtf`, free tier) — see `supabase/migrations/20260929060138_create_entitlements.sql`. Verified end-to-end with a synthetic webhook POST (row landed in `entitlement_grants`, then cleaned up).

**The app itself never reads Supabase** — it trusts the RevenueCat SDK's own on-device `CustomerInfo` cache (already receipt-validated against Play). Supabase is purely the server-side audit/support record, written only by the Worker via its service-role key (Worker secret, never in git).
**Purchases still never hit Supabase from the app** — entitlements come from the RevenueCat SDK's on-device `CustomerInfo` cache (receipt-validated against Play). Since #43 (`2f9d6c8`), the app **does** read and write Supabase for identity profile backup/restore via `SupabaseProfileCloud` in `app/lib/identity/supabase_profile_cloud.dart` (anonymous session + RLS-scoped profile row; wired from `app/lib/main.dart`). The webhook Worker's Supabase writes (entitlement audit) stay server-side only via its service-role key (Worker secret, never in git).

**Play Console one-time products — DONE (2026-09-29).** The API route (`scripts/play-publish/create_products.mjs`) kept 403ing on writes (propagation lag on the service account's grant), so all 3 were created directly in the Play Console UI instead: `amiro_riviera_optics` ($2.99), `amiro_long_flow` ($1.99), `amiro_full_beard` ($0.99) — each with purchase option id `buy`, bulk-priced across all ~190 regions (auto-converted from a USD base price via Play's own "Bulk edit pricing" tool), matching the RevenueCat catalog's store IDs exactly. Verified: One-time products list shows "1 - 3 of 3". The bulk-price selector's "Select all rows" checkbox is genuinely hard to find in a narrow viewport — it's the first cell of the `Country / region` column header, not visible without scrolling the grid to a precise position; `read_page` with `filter: all` on the column header ref is what surfaced it after coordinate-clicking failed repeatedly.

Expand Down
48 changes: 36 additions & 12 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ rationale is preserved unchanged at
`../superpowers/specs/2026-09-22-amiro-foundation-design.md`; where the two
disagree, this document is correct and the spec is history.

## Current snapshot (default branch, 2026-11)

Sections **1–10** below still describe the **2026-09-22 foundation pass**
only. On today's default branch the product also includes:

- **Store:** real Play Billing on Android via RevenueCat
(`packages/store/lib/src/revenuecat_entitlement_store.dart`); 11 catalog
items in `packages/store/lib/src/cosmetic_listing.dart`.
- **Sharing:** `amiro://encounter` QR/NFC + legacy `amiro://share` receive
(`packages/sharing`, `packages/nfc`, `packages/qr`, `app/lib/sharing/`).
- **Discovery:** encounter ledger and Passport tab (`packages/discovery`,
`app/lib/discovery/`, fifth tab in `app/lib/app.dart`).
- **Identity cloud backup:** Supabase profile sync (merged in #43).

For up-to-date package ownership, see the repository map in `/README.md` and
the package `README.md` files under `packages/sharing`, `packages/nfc`,
`packages/qr`, and `packages/store` (other workspace packages have no
package-level README — this file and their `pubspec.yaml` are the map).
Where §1–§10 below still describe foundation-era stubs, treat **this
snapshot** as correct for what ships today.

# Amiro Architecture (M0 + M1 + thin avatar-render slice)

**Status:** foundation pass implemented
Expand All @@ -24,13 +45,14 @@ sharing, 3D avatar, virtual cosmetics store). The foundation pass does
swappable cosmetic slots) exercising the pipeline — definition JSON →
render → swap → persist.

Everything NFC/QR/store/backend/purchases-related is a stub package with a
`README.md` describing its future contract, so package boundaries exist now
and later milestones fill them in without moving files.
At foundation time, everything NFC/QR/store/backend/purchases-related was a
stub package with a `README.md` describing its future contract, so package
boundaries existed before M3/M4 filled them in without moving files.

## 2. Explicit non-goals for this pass

- No NFC, no QR generation/scanning (packages exist as empty stubs only).
- No NFC, no QR generation/scanning in this pass (those packages were empty
stubs then; see §11 and the snapshot for what ships now).
- No backend, no network calls, no auth, no accounts.
- No purchases/StoreKit/Play Billing.
- No real cosmetic catalog or store UI — only 2 placeholder cosmetic assets
Expand All @@ -53,16 +75,16 @@ amiro/
│ ├── avatar_core/ # AvatarDefinition model (pure Dart) — data only, no rendering
│ ├── avatar_renderer/ # Renderer interface + ThermionAvatarRenderer impl
│ │ └── assets/{avatars,cosmetics}/ # placeholder .glb files live HERE, not at repo root
│ ├── sharing/ nfc/ qr/ store/ # STUBS — pubspec + README + empty lib only
│ ├── sharing/ nfc/ qr/ store/ # foundation: stubs; now implemented (see snapshot)
├── rust/core/ # Rust: Identity struct, validation, serde
├── docs/
│ ├── product/requirements.md
│ ├── architecture/overview.md # this file
│ ├── protocols/identity-link.md # stub — not started
│ ├── protocols/identity-link.md # encounter + legacy share links (implemented)
│ ├── protocols/avatar-format.md # AvatarDefinition JSON schema (real)
│ ├── legal/asset-policy.md
│ ├── legal/third-party-licenses.md
│ └── qa/test-plan.md # stub — not started
│ └── qa/test-plan.md # CI coverage + manual gaps
├── .github/workflows/ci.yml
├── pubspec.yaml # workspace root: pub `workspace:` list + `melos:` config
├── LICENSE (MIT)
Expand Down Expand Up @@ -213,8 +235,9 @@ No network calls anywhere in this pass. No data leaves the device.
iOS has not been attempted — the fixes above were Android-specific
(Gradle/AGP), and the lighting/camera fix, while platform-agnostic in
principle, has only been exercised on Android hardware.
- **`docs/protocols/identity-link.md` and `docs/qa/test-plan.md` are
stubs.** Not started.
- **`docs/protocols/identity-link.md` and `docs/qa/test-plan.md` were
stubs at foundation time.** Both are written now (protocol + CI/manual QA
scope); see those files and the snapshot above.

## 8. Risks

Expand Down Expand Up @@ -259,9 +282,10 @@ pure-Dart packages.

PRD M0 (architecture) fully, M1 (identity) fully, and a thin vertical slice
of M2 (avatar) — base body + 2 cosmetic slots only, no animation, no camera
controls. Full M2 (all 18 slots, animation, camera) and everything from M3
onward (real store, NFC, QR, backend) are deferred and tracked via the stub
package READMEs.
controls. Full M2 (all 18 slots, animation, camera) and later PRD scope
(HTTPS App Links / `id.amiro.app`, server-authoritative scarcity, full
backend) remain deferred; M3 store and M4 NFC/QR sharing are implemented
(see snapshot and package READMEs).

## 11. M4 (Sharing): NFC/QR pass — Task 9 device spike (2026-09-24)

Expand Down
Loading
Loading