# Wallwise Studio — build spec

An AI room redesign app that gives a real watermarked preview before payment, discloses monthly render limits upfront, and uses locked-structure masks so walls, doors, and windows stay in place.

## Project context

This spec describes an independent, alternative Android app you are building from scratch to compete with an existing incumbent app on Google Play — not a modification, clone, or reskin of the incumbent's own code, assets, or branding. Use the incumbent only as a market reference (via the report data below), and design working_name/package_id/store_listing/design_system so the result is clearly its own product.

## Incumbent app

- **Name:** RoomGPT AI - Interior Design
- **Package id:** `the.magic.ai.room.gpt.bot`
- **Google Play:** https://play.google.com/store/apps/details?id=the.magic.ai.room.gpt.bot
- **appy.fyi report:** https://appy.fyi/report/the.magic.ai.room.gpt.bot
- **Category:** House & Home

## Overview

- **Working name:** Wallwise Studio (trademark cleared: no)
- **Package id:** `fyi.appy.wallwisestudio`
- **Min / target SDK:** 26 / 35
- **Backend:** firebase
- **Estimated build time:** 6 weeks
- **Pricing:** subscription, $7.5 via `revenuecat`
- **Runtime AI:** yes (image_gen_api): Structure-preserving room image edit using uploaded source photo, transparent editable mask, selected style prompt, and fixed-structure preservation instruction ≈ $0.08/call
- **Permissions:** `CAMERA`, `INTERNET`

## Non-goals (out of scope for v1)

- No custom model training or proprietary image-generation research for v1; v1 uses a commercial image-editing API through Firebase Cloud Functions.
- No forced in-app review prompt before the first render or before purchase.
- No undisclosed render caps, hidden credit packs, or purchase flow that omits cancellation instructions.
- No structural remodeling promises such as adding/removing walls, doors, windows, plumbing, electrical work, measurements, permits, or contractor-ready plans.
- No AR room scanning, 3D floor plans, or furniture shopping marketplace in v1.

## Tech stack

- **Language / UI:** Kotlin, Jetpack Compose
- **Kotlin:** 2.0.21
- **Compose BOM:** 2024.10.01
- **Gradle:** 8.9

| Purpose | Gradle coordinate |
| --- | --- |
| Compose activity host | `androidx.activity:activity-compose:1.9.3` |
| Compose navigation graph | `androidx.navigation:navigation-compose:2.8.3` |
| ViewModel integration for Compose screens | `androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6` |
| Lifecycle-aware state collection in Compose | `androidx.lifecycle:lifecycle-runtime-compose:2.8.6` |
| Load local and remote render images in Compose | `io.coil-kt:coil-compose:2.7.0` |
| Camera preview and image capture core | `androidx.camera:camera-core:1.3.4` |
| Camera2 backend for CameraX | `androidx.camera:camera-camera2:1.3.4` |
| Bind CameraX to lifecycle | `androidx.camera:camera-lifecycle:1.3.4` |
| CameraX PreviewView interop | `androidx.camera:camera-view:1.3.4` |
| Local project/render cache database | `androidx.room:room-runtime:2.6.1` |
| Room coroutine extensions | `androidx.room:room-ktx:2.6.1` |
| Room annotation processor | `androidx.room:room-compiler:2.6.1` |
| Anonymous user identity for server-side credits | `com.google.firebase:firebase-auth-ktx:23.1.0` |
| Firestore credit ledger and generation metadata | `com.google.firebase:firebase-firestore-ktx:25.1.1` |
| Upload source photos, masks, and generated images | `com.google.firebase:firebase-storage-ktx:21.0.1` |
| Call Firebase Cloud Functions generation endpoint | `com.google.firebase:firebase-functions-ktx:21.1.0` |
| Await Firebase Task APIs from coroutines | `org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0` |
| Subscription purchase and entitlement SDK | `com.revenuecat.purchases:purchases:8.9.0` |
| HTTP client for the Firebase Cloud Function image-generation proxy | `com.squareup.okhttp3:okhttp:4.12.0` |
| JSON adapter for generation request/response DTOs | `com.squareup.moshi:moshi-kotlin:1.15.1` |

## Design system

- **Primary color:** `#2F6F5E`
- **Background color:** `#FAF7F2`
- **Error color:** `#B3261E`
- **Typography:** Material 3 default type scale, no custom font
- **Launcher icon glyph:** Phosphor `paint-roller` (regular weight)
- **Theme notes:** Use Material 3 light and dark themes. Light mode background is warm off-white #FAF7F2 with primary #2F6F5E. Dark mode background is #111614, surface #1B211F, primary #8FD8BE, and error #F2B8B5. Image cards use 16dp rounded corners, no skeuomorphic room-design styling, and every paywall/credit number is rendered as normal body text rather than fine print.

## Screens

### Onboarding
- **Route:** `onboarding`
- **Purpose:** Explain the honest flow: first watermarked preview before payment, visible monthly limits, and structure-lock editing before generation.
- **Reached via:** app launch when no local project exists; Settings screen tap Replay onboarding
- **Key UI elements:** Three promise cards: Free watermarked preview, 48 renders/month shown before purchase, Lock walls/doors/windows; Primary button: Start with a room photo; Secondary button: Import from gallery; No review request and no paywall on this screen
- **States:** initial, camera_permission_rationale, ready, error

### Project Home
- **Route:** `home`
- **Purpose:** Show current projects and give entry points for capture/import, history, and settings.
- **Reached via:** app launch after onboarding; back from Capture Import; back from Preview Result; bottom navigation Home item
- **Key UI elements:** Top app bar with Settings and Credits buttons; Project list cards with latest source and render thumbnails; Floating action button: New room; Empty-state card explaining first preview is free and watermarked
- **States:** loading, empty, populated, error

### Capture Import
- **Route:** `capture`
- **Purpose:** Capture a room photo with CameraX or import one or more photos through Android Photo Picker without asking for storage permission.
- **Reached via:** Onboarding primary button; Project Home New room button; Project detail Add photo action
- **Key UI elements:** Camera preview; Shutter button; Import photos button; Selected-photo strip supporting multiple source photos; Continue to lock structure button
- **States:** camera_permission_needed, camera_permission_denied, camera_ready, photo_picker_open, photos_selected, saving, error

### Mask Editor
- **Route:** `project/{projectId}/mask`
- **Purpose:** Let the user paint locked regions for walls, doors, and windows so the edit mask preserves fixed structure.
- **Reached via:** Capture Import Continue button; Preview Result Edit structure lock button
- **Key UI elements:** Source photo canvas; Brush size slider; Lock Walls button; Lock Doors/Windows button; Erase button; Show mask toggle; Reset mask button; Continue to styles button
- **States:** loading, no_photos, editing, saving, error

### Style Library
- **Route:** `project/{projectId}/styles`
- **Purpose:** Choose a redesign style template before creating the free preview or paid render.
- **Reached via:** Mask Editor Continue button; Preview Result Try another style button
- **Key UI elements:** Grid of style template cards; Style names: Modern, Scandinavian, Japandi, Industrial, Minimal, Warm Traditional, Coastal, Boho; Selected style preview chip; Generate watermarked preview button or Generate paid render button depending on entitlement/credit state
- **States:** loading, no_photo, populated, checking_credit_state, error

### Generation Progress
- **Route:** `project/{projectId}/generate/{styleId}`
- **Purpose:** Upload photos and masks, call the Firebase generation function, and show transparent progress while credits are checked and consumed.
- **Reached via:** Style Library Generate watermarked preview button; Style Library Generate paid render button
- **Key UI elements:** Progress stepper: Checking credits, Uploading photos, Preserving locked structure, Generating design, Saving result; Credit disclosure text shown before paid generation starts; Cancel button before generation API call begins; Error panel with refund status when applicable
- **States:** checking_credits, uploading, generating, saving_result, complete, credit_required, error

### Preview Result
- **Route:** `render/{renderId}`
- **Purpose:** Display the generated room redesign with source comparison, watermark status, structure-lock reminder, and next actions.
- **Reached via:** Generation Progress complete state; History render tile tap
- **Key UI elements:** Before/after image comparison slider; Watermark badge for free preview; Style name and generation timestamp; Button: Try another style; Button: Edit structure lock; Button: Unlock monthly renders when free preview is used
- **States:** loading, populated, missing_image_error, error

### Paywall Credits
- **Route:** `paywall`
- **Purpose:** Sell the $7.50/month subscription while showing the exact 48-render monthly limit and self-serve cancellation steps beside the price.
- **Reached via:** Style Library when free preview already used and no active subscription; Preview Result Unlock monthly renders button; Project Home Credits button
- **Key UI elements:** Product card: $7.50/month; Limit text: Includes 48 room renders per billing period; free preview does not renew weekly; Remaining-credit meter for subscribers; Cancellation instructions: Google Play > Payments & subscriptions > Subscriptions > Wallwise Studio; Subscribe button; Restore purchases button; Open Google Play subscription management button
- **States:** loading_products, product_available_not_subscribed, purchase_in_progress, purchase_error, subscribed, product_unavailable, restore_complete

### History
- **Route:** `history`
- **Purpose:** Show prior source photos and generated renders so users can compare outputs without spending extra credits.
- **Reached via:** Project Home bottom navigation History item; Preview Result back navigation
- **Key UI elements:** Chronological render list; Filter chips: All, Free preview, Paid renders; Thumbnail pair for source and result; Empty state with New room button
- **States:** loading, empty, populated, error

### Settings
- **Route:** `settings`
- **Purpose:** Show account, subscription status, disclosed limits, cancellation link, privacy note, and onboarding replay.
- **Reached via:** Project Home settings button; Paywall Credits cancellation link return
- **Key UI elements:** Anonymous account ID row; Subscription status row; Monthly credit limit and remaining credits row; Open Google Play cancellation link; Privacy summary: photos are uploaded for generation; Replay onboarding button
- **States:** loading, populated, not_signed_in, error

## Data model

### ProjectEntity (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key, UUID string |
| createdAt | `Instant` |  |
| updatedAt | `Instant` |  |
| title | `String` | default Room plus local date |
| selectedStyleId | `String?` | nullable until user chooses a style |
| freePreviewUsed | `Boolean` | local mirror of Firestore user flag |

### PhotoEntity (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key, UUID string |
| projectId | `String` | foreign key to ProjectEntity.id |
| localUri | `String` | content or file URI for locally cached source image |
| remoteStoragePath | `String?` | nullable until uploaded to Firebase Storage |
| widthPx | `Int` |  |
| heightPx | `Int` |  |
| createdAt | `Instant` |  |

### MaskEntity (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key, UUID string |
| projectId | `String` | foreign key to ProjectEntity.id |
| localMaskPngUri | `String` | PNG where opaque pixels are locked structure and transparent pixels are editable |
| remoteStoragePath | `String?` | nullable until uploaded to Firebase Storage |
| brushVersion | `Int` | increment on each saved edit |
| updatedAt | `Instant` |  |

### RenderEntity (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key, UUID string matching Firestore generation id |
| projectId | `String` | foreign key to ProjectEntity.id |
| styleId | `String` | one of hardcoded v1 style IDs |
| sourcePhotoIds | `List<String>` | photo IDs used for generation |
| resultLocalUri | `String?` | nullable until downloaded |
| resultRemoteStoragePath | `String?` | Firebase Storage path returned by function |
| isWatermarked | `Boolean` | true for free preview result |
| status | `String` | queued, uploading, generating, complete, failed |
| errorMessage | `String?` | nullable |
| createdAt | `Instant` |  |

### UserAccount (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| uid | `String` | document id from Firebase anonymous auth |
| hasUsedFreePreview | `Boolean` | server-authoritative free preview flag |
| revenueCatAppUserId | `String` | same as Firebase uid |
| subscriptionActive | `Boolean` | set by RevenueCat webhook or entitlement sync |
| currentPeriodStart | `Instant?` | nullable until subscribed |
| currentPeriodEnd | `Instant?` | nullable until subscribed |
| monthlyRenderLimit | `Int` | 48 for v1 subscription |
| rendersUsedThisPeriod | `Int` | incremented transactionally before paid generation |

### GenerationRequest (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | document id, UUID string |
| uid | `String` | foreign key to UserAccount.uid |
| projectId | `String` |  |
| styleId | `String` |  |
| photoStoragePaths | `List<String>` | Firebase Storage paths for source photos |
| maskStoragePath | `String` | Firebase Storage path for structure-lock mask PNG |
| mode | `String` | free_watermarked_preview or paid_render |
| status | `String` | queued, running, complete, failed, refunded |
| creditDebited | `Boolean` | false for first free preview |
| resultStoragePath | `String?` | nullable until complete |
| errorMessage | `String?` | nullable |
| createdAt | `Instant` |  |

## Features

### No-paywall first preview onboarding

Users can capture or import a room photo and request one watermarked AI preview before any subscription screen or review prompt appears.

- **Answers complaint:** Pay before you can see anything

- **Screens:** Onboarding, Capture Import, Mask Editor, Style Library, Generation Progress, Preview Result

- **Estimated hours:** 34

**Implementation notes:** On first launch, route to Onboarding, then Capture Import, Mask Editor, Style Library, and Generation Progress without showing Paywall Credits. Use Firebase anonymous auth on app start. The Generation Progress ViewModel calls Cloud Function `generateRoomRender` with `mode='free_watermarked_preview'`; the function permits this only when `UserAccount.hasUsedFreePreview == false`, writes `hasUsedFreePreview=true` in the same Firestore transaction that creates the GenerationRequest, and returns a watermarked JPEG Storage path. Do not include Google Play In-App Review API in v1 and do not navigate to Paywall Credits until the user taps an unlock button or tries a second generation without subscription.

**Acceptance criteria:**
- A fresh install can reach Generation Progress and receive one watermarked result without seeing Paywall Credits.
- No screen before the first completed preview contains a review prompt, rating widget, subscription price, or purchase button.
- After the first preview completes, `UserAccount.hasUsedFreePreview` is true in Firestore and a second free preview request returns a credit_required state instead of generating.

### Photo capture, gallery import, and multi-photo project input

Users can capture a room photo with CameraX or import multiple existing room photos for one redesign project.

- **Answers complaint:** baseline parity

- **Screens:** Capture Import, Project Home, Style Library

- **Estimated hours:** 28

**Implementation notes:** Use CameraX `ImageCapture.takePicture()` to write JPEGs into app-specific files under `context.filesDir/projects/{projectId}/`. Use Android Photo Picker via `ActivityResultContracts.PickMultipleVisualMedia` for gallery import so READ_MEDIA_IMAGES is not requested. Downscale each source image before upload so the longest edge is 1536 px and JPEG quality is 90. Store each selected image as PhotoEntity with width/height and preserve order in `sourcePhotoIds`; uploads go to Firebase Storage path `users/{uid}/projects/{projectId}/photos/{photoId}.jpg`. The Style Library Generate button is enabled only when at least one PhotoEntity exists.

**Acceptance criteria:**
- Denying CAMERA permission still allows gallery import through Photo Picker.
- Selecting three gallery images creates three PhotoEntity rows linked to the same ProjectEntity.
- A captured 4000 px wide photo is uploaded with longest edge no greater than 1536 px while the local original remains available for display.

### Structure-lock mask editor and generation prompt

Users paint fixed walls, doors, and windows; the app sends an edit mask and prompt that preserve those structures while restyling surfaces and furnishings.

- **Answers complaint:** Structure isn't preserved

- **Screens:** Mask Editor, Style Library, Generation Progress, Preview Result

- **Estimated hours:** 56

**Implementation notes:** Mask Editor renders the first source photo on a Compose Canvas and records brush strokes in image-coordinate space. Save a PNG mask matching the uploaded source image dimensions: pixels painted as locked structure are opaque white, all editable areas are fully transparent. For image-editing APIs that edit transparent mask regions, pass this PNG directly. The Cloud Function builds the prompt as: `Redesign this room in {styleName} style. Preserve the exact room structure: do not move, add, remove, resize, or cover walls, doors, windows, ceiling lines, floor boundaries, columns, fireplaces, built-ins, or openings. Only change colors, materials, lighting, decor, and movable furniture.` If multiple photos are uploaded, send the first photo as the edit base and include additional photos as references in the function request payload where the selected image API supports reference images; otherwise include their signed URLs in the prompt context sent to the provider wrapper.

**Acceptance criteria:**
- Painting a locked rectangle over a door produces a saved mask PNG where that rectangle's alpha is 255 and an unpainted sofa area alpha is 0.
- Every generation request contains the exact preserve-structure instruction text and the selected style name.
- The client exposes Edit structure lock from Preview Result so a user can correct a mask and regenerate instead of accepting moved walls/doors/windows.

### Transparent credits and paywall disclosure

The paywall shows $7.50/month, 48 renders per billing period, remaining credits, and cancellation steps before purchase.

- **Answers complaint:** Limits disclosed too late

- **Screens:** Paywall Credits, Settings, Style Library, Generation Progress

- **Estimated hours:** 32

**Implementation notes:** Use RevenueCat product id `wallwise_monthly_750` configured as a monthly subscription at $7.50/month. Paywall Credits loads offerings from RevenueCat and also reads Firestore `monthlyRenderLimit=48` and `rendersUsedThisPeriod`. Render the price, `48 room renders per billing period`, `remaining = 48 - rendersUsedThisPeriod`, and cancellation instructions in the same product card above the Subscribe button. The self-serve cancellation button opens `https://play.google.com/store/account/subscriptions?sku=wallwise_monthly_750&package=${BuildConfig.APPLICATION_ID}` using ACTION_VIEW. Do not hide credit limits behind support email or a secondary FAQ.

**Acceptance criteria:**
- Paywall Credits displays the price, 48-render limit, remaining-credit count, and cancellation instructions before the Subscribe button is tapped.
- When `rendersUsedThisPeriod` is 47, the paywall and Style Library show 1 remaining render.
- The cancellation link opens a browser or Play Store intent whose URL contains `sku=wallwise_monthly_750`.

### Subscription entitlement and server-side credit metering

Subscribers can generate paid renders until the disclosed 48-render monthly limit is reached, with credits debited atomically and refunded on generation failure.

- **Answers complaint:** great design but only gives you a limited amount of room redesigns before you have to pay more. this should be specifed before payment. I used them up testing all the different designs. if I knew there was a limit I wouldnt have bought.

- **Screens:** Generation Progress, Paywall Credits, Settings

- **Estimated hours:** 34

**Implementation notes:** Use RevenueCat SDK for purchase and restore. On login, set RevenueCat app user ID to Firebase anonymous uid. A Firebase webhook or scheduled entitlement sync updates `UserAccount.subscriptionActive`, `currentPeriodStart`, and `currentPeriodEnd`. The `generateRoomRender` Cloud Function starts a Firestore transaction: if mode is paid, require subscriptionActive true, current time before currentPeriodEnd, and `rendersUsedThisPeriod < monthlyRenderLimit`; increment `rendersUsedThisPeriod` and set `creditDebited=true` on GenerationRequest. If image API upload/generation fails, run a second transaction that decrements `rendersUsedThisPeriod` once and marks status `refunded`. When a new billing period starts, reset rendersUsedThisPeriod to 0 for that uid.

**Acceptance criteria:**
- A paid render request at 48/48 credits returns credit_required and does not call the image-generation API.
- A paid render request at 47/48 increments Firestore to 48 before the Cloud Function calls the image-generation API.
- If the fake image-generation provider returns an error after debit, Firestore status becomes refunded and the user's rendersUsedThisPeriod returns to its prior value.

### Style template library

Users choose from a small fixed set of room redesign styles before generation.

- **Answers complaint:** baseline parity

- **Screens:** Style Library, Preview Result

- **Estimated hours:** 22

**Implementation notes:** Hardcode eight StyleTemplate objects in Kotlin with ids `modern`, `scandinavian`, `japandi`, `industrial`, `minimal`, `warm_traditional`, `coastal`, and `boho`. Each object has displayName, one-sentence description, and prompt fragment. Style Library uses a two-column Compose grid; selecting a card writes `ProjectEntity.selectedStyleId`. Generation is blocked until a valid style id exists. The selected template's prompt fragment is appended before the structure-preservation sentence in the Cloud Function request.

**Acceptance criteria:**
- The Style Library shows exactly eight style cards with stable ids.
- Tapping Japandi selects only the Japandi card and stores `selectedStyleId='japandi'`.
- A generation request without a selected style is rejected locally with a visible error and no credit debit.

### Render history and before/after review

Completed previews and paid renders are saved so users can compare results without spending credits retesting outputs.

- **Answers complaint:** baseline parity

- **Screens:** History, Preview Result, Project Home

- **Estimated hours:** 20

**Implementation notes:** After Generation Progress receives a result Storage path, insert or update RenderEntity with status complete, resultRemoteStoragePath, styleId, sourcePhotoIds, and isWatermarked. Download a local cached copy into `filesDir/renders/{renderId}.jpg` for fast History display. Preview Result uses a before/after slider implemented with two Coil-loaded images clipped by a draggable x-position state. History reads Room RenderEntity rows ordered by createdAt descending and never calls generation when opening an existing render.

**Acceptance criteria:**
- Opening History after one completed free preview shows one tile marked Free preview.
- Tapping a History tile opens Preview Result without changing Firestore `rendersUsedThisPeriod`.
- The before/after slider shows source image on the left side of the handle and generated result on the right side.

### Store QA and launch configuration

Prepare the Play-ready subscription utility with privacy disclosure, signed release build, and no hidden review/paywall blockers.

- **Answers complaint:** baseline parity

- **Screens:** Settings, Paywall Credits, Onboarding

- **Estimated hours:** 14

**Implementation notes:** Configure release signing from environment variables used in build_instructions. Add Play Billing/RevenueCat product id `wallwise_monthly_750` to a constants file, but fail gracefully with product_unavailable state if RevenueCat returns no package. Add a privacy summary screen in Settings that states photos and masks are uploaded to Firebase Storage and an image-generation provider for rendering. The Play listing copy must state first preview is watermarked and the paid plan is $7.50/month with 48 renders per billing period.

**Acceptance criteria:**
- `./gradlew bundleRelease` produces an AAB when signing environment variables are set.
- If RevenueCat offerings are empty, Paywall Credits shows product_unavailable and no broken purchase button.
- Settings privacy summary includes the words photos, masks, Firebase Storage, and image-generation provider.

## Store listing

- **Title:** Wallwise Room Preview
- **Short description:** Try one AI room redesign first. Limits and cancellation shown upfront.
- **Category:** House & Home
- **Keywords:** AI room design, interior design, room redesign, home decor, before after, style preview, renovation ideas
- **Icon prompt:** Create a clean Android app icon for an AI room redesign app named Wallwise Studio: rounded square background in deep green #2F6F5E, centered simple off-white paint roller forming the roofline of a small room, subtle warm beige floor plane, flat vector style, high contrast, no text, no photorealism, suitable at 48dp and 512px.

**Long description:**

Wallwise Studio is an AI room redesign app built around three promises: see a real result before paying, know the monthly render limit before subscribing, and keep fixed room structure in place.

Start with a room photo, paint over walls, doors, windows, and other fixed structure, choose a style, and generate one free watermarked preview. If you decide to subscribe, the app shows the price, 48-render monthly limit, remaining credits, and Google Play cancellation steps before purchase.

V1 focuses on restyling surfaces, colors, lighting, decor, and movable furniture. It does not promise construction plans, structural remodels, or adding/removing doors and windows.

## Legal

- **Regulated category:** none
- **Privacy policy URL:** https://wallwise-studio.example.com/privacy (privacy claims verified: no)
- **Data collected:** Room photos uploaded for generation; User-painted structure masks uploaded for generation; Generated room redesign images; Anonymous Firebase user ID; Subscription entitlement and purchase status; Credit usage count per billing period; App diagnostics related to generation failures

## Test plan

### 1. Pay before you can see anything (instrumented)

1. Install a fresh debug build and clear app data.
2. Launch the app.
3. Tap Start with a room photo on Onboarding.
4. Use a bundled test image through the gallery import test hook.
5. On Mask Editor, tap Continue without painting extra mask strokes.
6. Select the Modern style.
7. Tap Generate watermarked preview.
8. Use the fake Cloud Function response to complete generation.

**Expected:** A watermarked Preview Result is displayed before any Paywall Credits route appears and before any review prompt UI is shown.

### 2. Limits disclosed too late (instrumented)

1. Seed Firestore test user with subscriptionActive=false, monthlyRenderLimit=48, rendersUsedThisPeriod=12, hasUsedFreePreview=true.
2. Navigate directly to Paywall Credits.
3. Wait for fake RevenueCat offering for product `wallwise_monthly_750` priced at $7.50/month.
4. Read all visible text nodes on the product card.

**Expected:** The card visibly contains `$7.50/month`, `48 room renders per billing period`, `36 remaining`, and Google Play cancellation instructions before the Subscribe button.

### 3. Structure isn't preserved (unit)

1. Create a 100x100 Bitmap source fixture.
2. Create a MaskEditor stroke covering rectangle x=10..30 and y=20..60 to represent a locked doorway.
3. Export the mask PNG using the production mask encoder.
4. Read alpha values at pixel (20,40) and pixel (80,80).
5. Build a generation prompt for style `scandinavian`.

**Expected:** Pixel (20,40) has alpha 255, pixel (80,80) has alpha 0, and the prompt contains `do not move, add, remove, resize, or cover walls, doors, windows`.

### 4. Limited amount of room redesigns before you have to pay more (unit)

1. Seed a fake Firestore user with subscriptionActive=true, monthlyRenderLimit=48, rendersUsedThisPeriod=48, and currentPeriodEnd in the future.
2. Call the credit-check repository method for a paid render.
3. Record whether the fake image-generation API client was invoked.

**Expected:** The method returns credit_required, rendersUsedThisPeriod remains 48, and the fake image-generation API client invocation count is 0.

### 5. Credit refund when generation fails after debit (unit)

1. Seed a fake Firestore user with subscriptionActive=true, monthlyRenderLimit=48, rendersUsedThisPeriod=47.
2. Configure the fake image-generation provider to throw an error after the debit transaction.
3. Call `generateRoomRender` in paid mode.
4. Read the GenerationRequest and UserAccount records.

**Expected:** GenerationRequest status is refunded, creditDebited is true, errorMessage is non-empty, and rendersUsedThisPeriod is restored to 47.

### 6. baseline parity (instrumented)

1. Launch app with CAMERA permission denied.
2. Tap Import photos.
3. Select three test room images through the Photo Picker test contract.
4. Return to Capture Import.
5. Tap Continue to lock structure.

**Expected:** The app creates one project with three PhotoEntity records and navigates to Mask Editor; no READ_MEDIA_IMAGES permission dialog appears.

### 7. baseline parity (instrumented)

1. Seed Room with one completed RenderEntity whose isWatermarked=true and one source PhotoEntity.
2. Navigate to History.
3. Tap the render tile.
4. Drag the before/after slider handle from 25% to 75% width.

**Expected:** Preview Result opens without creating a new GenerationRequest, and the visible clipping boundary of the generated image follows the dragged handle position.

### 8. Pay before you can see anything (manual)

1. Install the release candidate from internal testing on a physical Android device.
2. Create a new account state by clearing app storage.
3. Run through onboarding, import a real room photo, select any style, and generate the first preview using the staging image-generation provider.
4. Observe every screen before the first result.

**Expected:** No subscription purchase screen, forced review request, or rating dialog appears before the first watermarked result is visible.

## Build instructions

```sh
./gradlew clean
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest
rm -f release.keystore
keytool -genkeypair -v -storetype PKCS12 -keystore release.keystore -alias release -keyalg RSA -keysize 2048 -validity 10000 -storepass changeit123 -keypass changeit123 -dname "CN=Wallwise Studio Preview,O=Solo Builder,C=US"
ANDROID_KEYSTORE_PATH="$PWD/release.keystore" ANDROID_KEYSTORE_PASSWORD="changeit123" ANDROID_KEY_ALIAS="release" ANDROID_KEY_PASSWORD="changeit123" ./gradlew bundleRelease
```

## Human gates still required

- `trademark_and_privacy_review`
- `closed_testing_recruitment`
