diff --git a/README.md b/README.md index 27b0181..4efbc09 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODOS.md b/TODOS.md index 3ac688b..00ac27d 100644 --- a/TODOS.md +++ b/TODOS.md @@ -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. @@ -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. @@ -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. @@ -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`. @@ -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. @@ -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_`). 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. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 5ae61cc..5ec4f52 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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 @@ -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 @@ -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) @@ -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 @@ -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) diff --git a/docs/legal/data-safety.md b/docs/legal/data-safety.md index f00b31a..d0c4512 100644 --- a/docs/legal/data-safety.md +++ b/docs/legal/data-safety.md @@ -1,26 +1,28 @@ # Amiro — Play Console "Data safety" form, draft answers Fill the actual Play Console form from this. Re-check against the code -before submitting — this reflects the app as of 2026-09-29, after wiring -real Play Billing purchases through RevenueCat + a Cloudflare Worker + -Supabase (see `workers/revenuecat-webhook/` and -`packages/store/lib/src/revenuecat_entitlement_store.dart`). If data -handling changes again, update both this file and the live form together. +before submitting — this reflects the app as of **2026-11-03**, after: -**This form was already submitted once (2026-09-29, before the payments -backend existed) declaring no data collection beyond device-to-device -identity sharing.** It must be re-submitted in Play Console now that a -real purchase-verification backend exists — see "Purchases" below. +- Real Play Billing via RevenueCat + Cloudflare Worker + Supabase entitlement + audit (`workers/revenuecat-webhook/`, + `packages/store/lib/src/revenuecat_entitlement_store.dart`). +- Optional profile backup/restore to Supabase (#43, + `app/lib/identity/supabase_profile_cloud.dart`, + `supabase/migrations/20261016180000_create_profiles.sql`). + +If data handling changes again, update both this file and the live form +together. + +**The form was re-submitted for real payments on 2026-09-29.** Profile +backup shipped later — re-check Play Console if you add new declared data +types for cloud-stored identity fields. ## Does your app collect or share any of the required user data types? -**Yes** — but only in the sense defined below (see "Is this data -processed ephemerally" / "Is data collection optional" for the actual -nuance). Amiro has no server, so nothing is *collected by the developer* -in Play's usual sense (uploaded off-device). What follows documents what -the app **stores on-device** and **transmits directly device-to-device** -during a share (QR/NFC), since Play's questionnaire asks about handling -broadly, not just server-side collection. +**Yes** — for purchases and optional profile backup (HTTPS to Supabase and +RevenueCat). Identity sharing (QR/NFC) and passport data remain +device-local / peer-to-peer and are documented below for Play's broader +wording. ## Data types @@ -28,10 +30,14 @@ broadly, not just server-side collection. | Field | Collected? | Shared? | Processing | Purpose | Optional? | |---|---|---|---|---|---| -| Name (display name) | Yes | Yes (device-to-device, user-initiated) | On-device only, no server | App functionality | No (required at identity creation) | -| Email address | Yes | Yes, if marked public | On-device only | App functionality | Yes | -| Phone number | Yes | Yes, if marked public | On-device only | App functionality | Yes | -| Other user IDs (username, social handles) | Yes | Yes, if marked public | On-device only | App functionality | Yes | +| Name (display name) | Yes | Yes (device-to-device when user shares; optional Supabase backup) | Local Isar; may sync to Supabase over HTTPS with anon auth | App functionality | No (required at identity creation) | +| Email address | Yes | Yes, if marked public on share; optional Supabase backup | Local; optional HTTPS backup | App functionality | Yes (field optional) | +| Phone number | Yes | Yes, if marked public on share; optional Supabase backup | Local; optional HTTPS backup | App functionality | Yes | +| Other user IDs (username, social handles) | Yes | Yes, if marked public on share; optional Supabase backup | Local; optional HTTPS backup | App functionality | Yes | + +Avatar definition JSON (cosmetic choices) follows the same pattern: local +always; included in optional Supabase backup, not in QR/NFC unless the +user shares their card. ### Photos or videos @@ -41,57 +47,47 @@ broadly, not just server-side collection. ### App activity / App info and performance -- None. No analytics, no crash reporting, no in-app search history, no - install SDKs. +- None. No analytics SDKs. Optional local crash log file; HTTP upload only + when built with `AMIRO_CRASH_INGEST_URL` (empty in default release builds). ### Device or other identifiers -- None collected or transmitted. (No advertising ID, no device ID sent - anywhere — there is nowhere to send it.) +| Field | Collected? | Shared? | Processing | Purpose | Optional? | +|---|---|---|---|---|---| +| RevenueCat `$RCAnonymousID` | Yes | Yes (RevenueCat) | SDK-generated on purchase | App functionality (IAP) | No when buying | +| Supabase auth user id | Yes | Yes (Supabase) | Anonymous session for profile backup | App functionality (restore) | Effectively required for cloud restore | + +These identifiers are not used for advertising. ### Purchase history / financial info -RevenueCat's own SDK integration handles this disclosure category -directly — Play Console surfaces RevenueCat as a declared SDK once its -Android app credentials are linked (Setup → API access in Play Console). -In our own words for this draft: +RevenueCat's own SDK integration handles part of this disclosure category +in Play Console. In our own words: | Field | Collected? | Shared? | Processing | Purpose | Optional? | |---|---|---|---|---|---| -| Purchase history (product id, timestamp) | Yes | Yes (RevenueCat, Google Play) | Sent to RevenueCat over HTTPS; RevenueCat webhooks the event to our Cloudflare Worker, which stores it in Supabase | App functionality (purchase verification, restore) | No — only triggered when the user buys a paid Store item; free items never touch the network | -| App-set installation identifier (RevenueCat's `$RCAnonymousID`) | Yes | Yes (RevenueCat) | Generated on-device by the RevenueCat SDK, not derived from any personal field | App functionality | No, same as above | +| Purchase history (product id, timestamp) | Yes | Yes (RevenueCat, Google Play) | HTTPS to RevenueCat; webhook to Worker → Supabase | App functionality (purchase verification, restore) | No — only when user buys a paid Store item | +| App-set installation identifier (RevenueCat's `$RCAnonymousID`) | Yes | Yes (RevenueCat) | Generated on-device by the RevenueCat SDK | App functionality | No, same as above | -This identifier is **not** linked to the profile identity (name, email, -phone, handles) described elsewhere in this file — the purchase path and -the identity-sharing path never share data with each other. +Purchase audit rows in Supabase are separate from the optional profile +backup table; they do not include profile contact fields. ## Is all of the user data collected by your app encrypted in transit? -Two separate answers depending on which data: - -- **Identity/profile sharing (QR/NFC):** not applicable in the usual - sense — there is no network transit to Developer's Coffee. Answered per - Play's guidance for peer-to-peer sharing features; consult current Play - policy wording for the exact checkbox, since this category evolves. -- **Purchase data (RevenueCat/Worker/Supabase):** all of it is HTTPS — - the RevenueCat SDK talks to RevenueCat over TLS, RevenueCat's webhook to - our Cloudflare Worker is HTTPS, and the Worker's write to Supabase is - HTTPS. Answer **Yes** for this category. +- **Identity/profile sharing (QR/NFC):** peer-to-peer; no Developer's Coffee + server in the path. +- **Profile backup, purchases (Supabase/RevenueCat/Worker):** **Yes** — all + HTTPS/TLS. ## Do you provide a way for users to request that their data be deleted? -**Yes** — uninstalling the app deletes all local data. There is no -server-side copy to separately request deletion of, because none exists. -Consider adding an in-app "clear my data" action for a smoother answer -here (not yet implemented — `IdentityRepository.clear()` exists in code -but isn't exposed as a settings/UI action yet). +**Partially.** Uninstalling removes local data. Cloud profile backup and +purchase audit rows may remain until the user emails +coffee.devloper@gmail.com (see `docs/legal/privacy-policy.md`). An in-app +"clear my data" control is not shipped yet (`IdentityRepository.clear()` +exists in code but is not exposed in UI). ## Data safety summary line (shown on the store listing) -Suggested Play auto-generated summary should read approximately: -"This app may share... Name, Email address, Phone number... No data -collected" is NOT quite right since data IS collected (locally) — work -through Play's actual flow carefully, since "collected" in their taxonomy -includes on-device storage in some cases and not others depending on how -each question is phrased. This file is a starting draft, not a substitute -for reading each Play Console question directly against the current app. +Work through Play's actual flow question-by-question against this draft; +taxonomy for "collected" vs on-device-only fields changes over time. diff --git a/docs/legal/privacy-policy.html b/docs/legal/privacy-policy.html index 422566b..4a33deb 100644 --- a/docs/legal/privacy-policy.html +++ b/docs/legal/privacy-policy.html @@ -5,25 +5,26 @@ Amiro Privacy Policy

Amiro Privacy Policy

-

Last updated: 2026-09-29

+

Last updated: 2026-11-03

This policy describes what Amiro ("the App") does with your information. -Amiro is built to keep your identity, avatar, and profile data on your own -device — there is no Amiro account system, and no analytics or advertising -SDK in the App. The only server-side processing Amiro does is limited to -verifying real-money Store purchases (see "Purchases" below); nothing about -your identity, avatar, or who you've shared with ever leaves your device.

+Amiro is local-first: your identity, avatar, and passport (people you've +met) live on your device by default. There is no email-and-password Amiro +account, and no analytics or advertising SDK in the App. The App does use +the network for (1) optional profile backup/restore and (2) verifying +real-money Store purchases — see those sections below. Sharing with another +person (QR/NFC) still goes directly device-to-device and does not pass +through our servers.

Who this covers

Amiro is published by Developer's Coffee. This policy applies to the Amiro @@ -39,11 +40,28 @@

What information the App handles

  • Mobile number, email address
  • Social handles (X/Twitter, Instagram) and a personal website URL
  • -

    All of this is stored only on your device, in a local -database. It is never sent to Developer's Coffee or to any third party by -the App itself.

    -

    Your avatar (body, hair, outfit, and other cosmetic choices) is also -stored only on your device.

    +

    This is stored on your device in a local database. Your avatar (body, +hair, outfit, and other cosmetic choices) is stored the same way.

    + +

    Optional profile backup (Supabase)

    +

    To help you recover your identity after reinstalling or moving to a new +phone, the App can back up your profile to a +Supabase +database we operate:

    + +

    If backup fails (no network), the App keeps working from local storage only.

    Sharing with another person

    Amiro's core feature is sharing your profile with someone else, by:

    @@ -53,9 +71,9 @@

    Sharing with another person

    Before you share, you choose which fields are public. Only the fields you mark public are included in what you show or send — anything you leave -private never leaves your device. The other person's phone reads that -information directly from the QR code or NFC tap; it does not pass -through any server Developer's Coffee operates, because none exists.

    +private is not included in the QR/NFC payload. The other person's phone +reads that information directly from the QR code or NFC tap; it does not +pass through Developer's Coffee servers.

    If you scan someone else's QR code or receive their NFC tap, whatever public profile information they chose to share is stored on your device, the same way your own profile is. You control that data (including @@ -76,7 +94,7 @@

    Purchases

    Google Play Billing. Purchases are processed by RevenueCat, which we use to verify a purchase actually happened before unlocking the -item — this is the one part of Amiro that talks to a server.

    +item.

    What that involves:

    -

    If you don't use the Store's paid items, none of this applies to you — -free cosmetics unlock without any network request.

    +

    If you don't use the Store's paid items, none of the purchase network +traffic applies to you — free cosmetics unlock without a purchase request.

    What the App does not do

    Your control over your data

    -

    Because everything lives on your device:

    Children's privacy

    Amiro is not directed at children and we do not knowingly collect -information from children. Since all data stays on-device and nothing is -transmitted to us, we have no way to collect such information in the -first place.

    +information from children. We do not ask for age. Profile backup and +purchases are initiated by whoever uses the App on the device.

    Changes to this policy

    -

    If Amiro's data practices change (for example, if a future version adds -optional cloud backup), this policy will be updated first, and that -version will describe the change clearly, including whether it is -optional.

    +

    If Amiro's data practices change materially, this policy will be updated +and the "Last updated" date above will change.

    Contact

    Questions about this policy: coffee.devloper@gmail.com

    diff --git a/docs/legal/privacy-policy.md b/docs/legal/privacy-policy.md index c666807..ea0d860 100644 --- a/docs/legal/privacy-policy.md +++ b/docs/legal/privacy-policy.md @@ -1,13 +1,15 @@ # Amiro Privacy Policy -**Last updated:** 2026-09-29 +**Last updated:** 2026-11-03 This policy describes what Amiro ("the App") does with your information. -Amiro is built to keep your identity, avatar, and profile data on your own -device — there is no Amiro account system, and no analytics or advertising -SDK in the App. The only server-side processing Amiro does is limited to -verifying real-money Store purchases (see "Purchases" below); nothing about -your identity, avatar, or who you've shared with ever leaves your device. +Amiro is local-first: your identity, avatar, and passport (people you've +met) live on your device by default. There is no email-and-password Amiro +account, and no analytics or advertising SDK in the App. The App does use +the network for (1) optional profile backup/restore and (2) verifying +real-money Store purchases — see those sections below. Sharing with another +person (QR/NFC) still goes directly device-to-device and does not pass +through our servers. ## Who this covers @@ -25,11 +27,28 @@ When you set up your Amiro identity, you may enter: - Mobile number, email address - Social handles (X/Twitter, Instagram) and a personal website URL -All of this is stored **only on your device**, in a local database. It is -never sent to Developer's Coffee or to any third party by the App itself. +This is stored **on your device** in a local database. Your avatar (body, +hair, outfit, and other cosmetic choices) is stored the same way. -Your avatar (body, hair, outfit, and other cosmetic choices) is also -stored only on your device. +### Optional profile backup (Supabase) + +To help you recover your identity after reinstalling or moving to a new +phone, the App can back up your profile to a [Supabase](https://supabase.com/privacy) +database we operate: + +- On first launch (and when your identity changes), the App may create an + anonymous Supabase session and upload a copy of your identity fields and + avatar definition JSON. Row access is protected by Supabase row-level + security; the App uses Supabase's public ("anon") key, not a root + password. +- The App gives you a **recovery code** (stored on your device) you can use + on another install to pull that profile back. +- The backup may also store RevenueCat's anonymous purchase user id next to + your profile so purchases and identity can be reconciled after restore — + not your name, email, or phone from the profile form. +- Your passport / encounter history stays **local only** (not uploaded). + +If backup fails (no network), the App keeps working from local storage only. ### Sharing with another person @@ -40,9 +59,9 @@ Amiro's core feature is sharing your profile with someone else, by: Before you share, you choose which fields are public. Only the fields you mark public are included in what you show or send — anything you leave -private never leaves your device. The other person's phone reads that -information directly from the QR code or NFC tap; it does not pass -through any server Developer's Coffee operates, because none exists. +private is not included in the QR/NFC payload. The other person's phone +reads that information directly from the QR code or NFC tap; it does not +pass through Developer's Coffee servers. If you scan someone else's QR code or receive their NFC tap, whatever public profile information they chose to share is stored on **your** @@ -63,8 +82,7 @@ deleting it) on your device. The Store lets you buy optional cosmetic items with real money through Google Play Billing. Purchases are processed by [RevenueCat](https://www.revenuecat.com/privacy), which we use to verify a -purchase actually happened before unlocking the item — this is the one -part of Amiro that talks to a server. +purchase actually happened before unlocking the item. What that involves: @@ -77,54 +95,45 @@ What that involves: product bought, and the timestamp — in a Supabase database, so we can look up and restore your purchases if needed (for example, after reinstalling). This record never includes your name, email, phone - number, or any other profile field, because the App never sends them. + number, or any other profile field from the identity form. - None of this is used for advertising, and we don't sell or share it with anyone beyond RevenueCat and Google Play, who process it to complete the purchase. -If you don't use the Store's paid items, none of this applies to you — -free cosmetics unlock without any network request. +If you don't use the Store's paid items, none of the purchase network +traffic applies to you — free cosmetics unlock without a purchase request. ## What the App does not do -- No account creation, no sign-in, no password. -- No analytics, crash reporting, or advertising SDKs. -- No network requests from the App to Developer's Coffee or anyone else, - except the purchase verification described above, and only when you buy - a paid Store item. +- No traditional account (no email sign-up, no password login). +- No analytics or advertising SDKs. +- No selling of your data. - No location data is collected. -- No data is sold or shared with third parties, other than what's - described under "Purchases" above. +- Sharing and passport data are not uploaded to our servers (only the + optional profile backup and purchase audit paths described above use + the network). ## Your control over your data -Because everything lives on your device: - -- Editing or clearing a field in your identity removes it immediately. -- Uninstalling the App removes all Amiro data stored by it, subject to - your device's own OS behavior (for example, an OS-level backup you - created yourself, which is outside the App's control). -- There is no Amiro account to delete, because there is no Amiro account. -- If you've made a paid purchase, we also keep a purchase record (the - random per-install identifier, product, and timestamp — see - "Purchases") in RevenueCat and our Supabase database. Email - coffee.devloper@gmail.com to request deletion of that record; it isn't - removed automatically by uninstalling, since it exists to protect your - purchase across reinstalls. +- Editing or clearing a field in your identity removes it on your device + immediately; the next successful backup sync updates the cloud copy. +- Uninstalling the App removes local Amiro data, subject to your device's + own OS behavior (for example, an OS-level backup you created yourself). +- There is no password-based Amiro account to delete. To remove a cloud + profile backup or purchase audit row, email + coffee.devloper@gmail.com — these are not always removed automatically + on uninstall because they exist to support restore. ## Children's privacy Amiro is not directed at children and we do not knowingly collect -information from children. Since all data stays on-device and nothing is -transmitted to us, we have no way to collect such information in the -first place. +information from children. We do not ask for age. Profile backup and +purchases are initiated by whoever uses the App on the device. ## Changes to this policy -If Amiro's data practices change (for example, if a future version adds -optional cloud backup), this policy will be updated first, and that -version will describe the change clearly, including whether it is -optional. +If Amiro's data practices change materially, this policy will be updated +and the "Last updated" date above will change. ## Contact diff --git a/docs/legal/store-listing.md b/docs/legal/store-listing.md index 68303ec..b7a343c 100644 --- a/docs/legal/store-listing.md +++ b/docs/legal/store-listing.md @@ -20,8 +20,9 @@ share everywhere. **Build your avatar** Choose a hairstyle, outfit, and accessories. Rotate it a full 360° to see -every angle. Your avatar is yours: it lives on your device, not on a -server. +every angle. Your avatar is yours: it lives on your device first, with +optional cloud backup so you can recover your look after reinstalling (see +the in-app privacy policy). **Share it instantly** Show a QR code, or — on Android — just tap phones together. Whoever you diff --git a/docs/protocols/identity-link.md b/docs/protocols/identity-link.md index 510643b..8f7a333 100644 --- a/docs/protocols/identity-link.md +++ b/docs/protocols/identity-link.md @@ -1,14 +1,45 @@ # Identity Link Protocol -**Status: not started.** Planned for milestone M4 (Sharing), owned by -the NFC/Sharing agent. - -Planned shape (from the PRD): a compact identity URL, -`https://id.amiro.app/u/`, used identically as the NFC payload and -the QR code content. The receiver's app (if installed) resolves it via -Universal Link / App Link to the native profile; otherwise it opens -the web fallback. NFC/QR never carry the full profile — only this -identifier. - -See `packages/sharing/README.md`, `packages/nfc/README.md`, -`packages/qr/README.md` for the per-package planned interfaces. +**Status:** implemented on-device (custom scheme). The PRD's shorter +`https://id.amiro.app/u/` App Link / web fallback is **not** built yet. + +## Primary: encounter links (current Share tab) + +``` +amiro://encounter?d= +amiro://encounter?d=...&c= # optional +``` + +- Encoded/decoded in `packages/sharing/lib/src/encounter_qr.dart` + (`encodeEncounterUri`, `decodeEncounterUri`). Max URI length 2000 + characters for QR/NFC. +- Payload schema: `packages/sharing/lib/src/amiro_sharing_payload.dart` + (`schemaVersion`, collection, equipped cosmetics, etc.). +- App routing: `app/lib/sharing/scan_result.dart` → encounter comparison → + optional save to passport (`packages/discovery`). + +## Legacy: profile links (older builds) + +``` +amiro://share?d= +``` + +- Built/parsed by `packages/sharing/lib/src/share_payload_codec.dart` + (`buildShareUri`, `parseShareUri`) as a `SharedProfile` (username, contact + fields, avatar JSON, owned cosmetic ids). +- Still **received** (QR, NFC, deep link) and open + `SharedProfileScreen`; new shares emit `amiro://encounter` instead + (`app/lib/sharing/own_encounter.dart`). + +## Transport + +| Channel | Package | Notes | +|---|---|---| +| QR generate | `packages/qr` | `buildQrWidget(fullUri)` | +| QR scan | `app/lib/sharing/qr_scan_screen.dart` | camera → `resolveScannedText` | +| NFC emulate | `packages/nfc` | Android HCE, URI as NDEF text | +| NFC read | `packages/nfc` | NDEF → first `amiro://` link | +| Cold/warm deep link | `app/lib/sharing/sharing_providers.dart` (`incomingShareLinkProvider`) | `app_links`; UI in `incoming_share_listener.dart` | + +See each package's `README.md` for interfaces; historical design notes remain +in `docs/superpowers/specs/2026-09-23-nfc-sharing-design.md`. diff --git a/docs/qa/test-plan.md b/docs/qa/test-plan.md index d27e5fc..296219f 100644 --- a/docs/qa/test-plan.md +++ b/docs/qa/test-plan.md @@ -1,25 +1,36 @@ # QA Test Plan -**Status: this pass covers unit/widget tests only (see below). Full -device-matrix QA (Android phone, iPhone, Android+NFC, iPhone+NFC) is -out of scope until NFC/QR ship in M4.** +**Status:** automated unit/widget tests cover identity, avatar, store, +sharing, discovery, and theme components on CI (`melos run test`). Full +device-matrix QA (two-phone NFC tap, iOS parity, real Play purchase flows) +remains manual and is not fully exercised. -## Coverage shipped in this pass +## Coverage in CI today - `rust/core`: `cargo test` — Identity struct validation + serde round-trip. -- `packages/identity_core`: unit tests for `Identity` model, JSON - round-trip, `publicFields()` privacy filtering, `IsarIdentityRepository` - CRUD. -- `packages/avatar_core`: unit tests for `AvatarDefinition`, slot - update, JSON round-trip. -- `packages/avatar_renderer`: unit tests for asset path resolution and - `ThermionAvatarRenderer` slot-swap bookkeeping (engine calls faked). -- `app`: widget tests for identity create/edit (field entry, privacy - toggle, save) and avatar screen (initial load, cosmetic swap on/off). +- `packages/identity_core`: `Identity` model, JSON round-trip, + `publicFields()` privacy filtering, `IsarIdentityRepository` CRUD. +- `packages/avatar_core`: `AvatarDefinition`, slots, JSON round-trip. +- `packages/avatar_renderer`: asset path resolution and slot-swap bookkeeping + (engine faked). +- `packages/store`: catalog, entitlements (including RevenueCat channel mocks), + valuation, rarity/collection helpers. +- `packages/sharing`, `packages/nfc`, `packages/qr`: payload/URI/NDEF and + emulator channel contracts. +- `packages/discovery`: encounter records, file repository, processor. +- `app`: widget tests under `app/test/` for identity, avatar, store, sharing + (QR scan, NFC receive, scan routing, identity card), discovery/passport, + theme components, and related providers. -## Explicitly not covered yet +## Explicitly not covered (or only partially covered) in CI -NFC, QR, deep links, purchases, offline/online sync divergence, profile -privacy on the (not-yet-built) web fallback, app-not-installed -experience, cross-device pairing. Each becomes a QA Agent task when its -owning milestone ships. +- Two-device NFC tap-to-tap and real QR scan against a physical code (see + `docs/architecture/overview.md` §11). +- Live Play Billing / RevenueCat purchase sheets and receipt edge cases. +- iOS store, NFC emulate, and full renderer parity on Apple hardware. +- Profile cloud backup against a live Supabase project (`identity` PR #43). +- PRD web fallback when the app is not installed (`https://id.amiro.app/...`). + +Each gap becomes a focused QA pass when its hardware or credentials are +available; device spikes are recorded in the architecture doc rather than +here. diff --git a/packages/discovery/README.md b/packages/discovery/README.md new file mode 100644 index 0000000..9d417ef --- /dev/null +++ b/packages/discovery/README.md @@ -0,0 +1,12 @@ +# discovery + +Encounter ledger and state machine for the Passport tab. + +- **`EncounterRecord`**, **`DiscoveryRepository`** + (`FileDiscoveryRepository` persists to `discovery.json` under app support). +- **`EncounterProcessor`** — two-phase receive vs save; classifies new/known/self + encounters before anything is written. + +Wired from `app/lib/discovery/` (Passport screen, encounter comparison). +Sharing payloads reach this package only after `packages/sharing` validates +them (`AmiroSharingPayload`). diff --git a/packages/nfc/README.md b/packages/nfc/README.md index 0acbd61..6724e9d 100644 --- a/packages/nfc/README.md +++ b/packages/nfc/README.md @@ -1,11 +1,15 @@ # nfc -**Status: stub — not implemented.** - -Owns reading and writing the compact identity deep-link payload over -NFC (tap-to-share). Planned interface: - -- `Future writeIdentityPayload(String url)` -- `Stream readIncomingPayload()` - -Implemented in a future milestone (M4, Sharing), by the NFC/Sharing agent. +NFC transport for whatever `amiro://…` URI the Share flow builds (today +`amiro://encounter`, see `packages/sharing`). + +- **`NfcReader` / `ManagerNfcReader`** — starts an NDEF read session and + emits the first `amiro://` link found in tag records (strict UTF-8/UTF-16). + Used from `app/lib/sharing/nfc_receive_screen.dart`. +- **`NfcEmulator` / `AndroidNfcEmulator`** — Android-only HCE: writes the + URI to the native `AmiroHceService` over `MethodChannel('amiro/nfc_hce')`. + iOS cannot emulate a general-purpose tag; the Share screen gates emulate UI + on Android. + +End-to-end tap-between-two-phones behavior is implemented but not fully +device-verified; see `docs/architecture/overview.md` §11. diff --git a/packages/qr/README.md b/packages/qr/README.md index 07a0f89..3771322 100644 --- a/packages/qr/README.md +++ b/packages/qr/README.md @@ -1,11 +1,11 @@ # qr -**Status: stub — not implemented.** +QR **generation** for share links. Scanning (camera + `mobile_scanner`) lives +in `app/lib/sharing/qr_scan_screen.dart`. -Owns generating a QR code for the identity share link and scanning an -incoming QR code. Planned interface: +- **`buildQrWidget(String data, {double size})`** — brass-framed cream card + wrapping `qr_flutter`. Pass the full URI string (today + `encodeEncounterUri(...)` from `packages/sharing`, not raw JSON). -- `Widget buildIdentityQrCode(String url)` -- `Future scanQrCode()` - -Implemented in a future milestone (M4, Sharing), by the NFC/Sharing agent. +The package stays Flutter-dependent but does not import `app/` or +`packages/sharing/` so it can be tested in isolation. diff --git a/packages/qr/lib/src/qr_generator.dart b/packages/qr/lib/src/qr_generator.dart index 5c54652..0866853 100644 --- a/packages/qr/lib/src/qr_generator.dart +++ b/packages/qr/lib/src/qr_generator.dart @@ -9,7 +9,8 @@ const _brass = Color(0xFFC08A3E); /// Renders [data] as a scannable QR code on a brass-framed cream card. Used /// for the identity share URI — [data] is expected to already be the full -/// `amiro://share?d=...` URI, not raw profile data. +/// `amiro://encounter?...` (or legacy `amiro://share?d=...`) URI, not raw +/// profile JSON. /// /// [size] is the code itself; the card adds padding around it. Widget buildQrWidget(String data, {double size = 240}) { diff --git a/packages/sharing/README.md b/packages/sharing/README.md index b627ffa..92e0993 100644 --- a/packages/sharing/README.md +++ b/packages/sharing/README.md @@ -1,17 +1,21 @@ # sharing -**Status: stub — not implemented.** +Owns the on-wire identity and discovery payloads plus URI codecs the app, +NFC, and QR layers share. -Owns the orchestration between `nfc`, `qr`, and deep-link resolution: -picking which channel to use, building the compact identity payload -(`https://id.amiro.app/u/`), and routing an incoming tap/scan to -either the native profile screen (app installed) or a signal to open -the web fallback. +**Primary link (Share tab today):** `amiro://encounter?d=` +with optional `&c=`. Built by +`encodeEncounterUri` / parsed by `decodeEncounterUri` in +`lib/src/encounter_qr.dart` (`AmiroSharingPayload`, `ContactCard`). -Planned interface (from `docs/superpowers/specs/2026-09-22-amiro-foundation-design.md` -and the product PRD's NFC/QR sections): +**Legacy profile link:** `amiro://share?d=` via +`buildShareUri` / `parseShareUri` in `lib/src/share_payload_codec.dart` +(`SharedProfile`). Still accepted on receive (`app/lib/sharing/scan_result.dart`) +but no longer emitted for new shares. -- `ShareableIdentityLink buildShareLink(String identityId)` -- `Stream listenForIncomingShares()` +**NDEF helpers:** `lib/src/ndef_encounter.dart` — decode NFC Forum text +records to an `amiro://` URI without depending on platform plugins. -Implemented in a future milestone (M4, Sharing), by the Sharing agent. +Wired from `app/lib/sharing/` (identity card, QR scan, NFC receive, deep +links). The PRD's shorter `https://id.amiro.app/u/` web fallback is +not implemented yet. diff --git a/workers/revenuecat-webhook/README.md b/workers/revenuecat-webhook/README.md index 925a159..59b6c2e 100644 --- a/workers/revenuecat-webhook/README.md +++ b/workers/revenuecat-webhook/README.md @@ -5,10 +5,14 @@ writes them to Supabase. See `TODOS.md` #15 for the full picture of what's wired up and what's still pending (Play Console products, RC↔Play service account link, iOS). -The Flutter app never talks to this Worker or to Supabase directly — it -trusts the RevenueCat SDK's own on-device `CustomerInfo` cache. This Worker -exists purely as a server-side audit trail (support, fraud review, restoring -purchase history if the SDK cache is ever lost). +The Flutter app never calls this Worker. Entitlements on-device come from +the RevenueCat SDK's `CustomerInfo` cache +(`packages/store/lib/src/revenuecat_entitlement_store.dart`). Since #43 the +app **does** talk to the same Supabase project for optional **profile** +backup/restore (`SupabaseProfileCloud` in `app/lib/identity/`), using the +public anon key and row-level security — not this Worker's service-role +path. This Worker exists for purchase-event audit (support, fraud review, +restoring purchase history if the SDK cache is ever lost). ## Request flow