# InkPreview Studio — build spec

A trust-led AI tattoo designer that shows the first generated tattoo before payment, gives tattoo-specific controls, and sells only a transparent one-time Pro pack instead of a subscription.

## 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:** Tattoo AI - Tattoo Design
- **Package id:** `com.hubx.tattoo`
- **Google Play:** https://play.google.com/store/apps/details?id=com.hubx.tattoo
- **appy.fyi report:** https://appy.fyi/report/com.hubx.tattoo
- **Category:** Art & Design

## Overview

- **Working name:** InkPreview Studio (trademark cleared: no)
- **Package id:** `fyi.appy.inkpreviewstudio`
- **Min / target SDK:** 26 / 35
- **Backend:** none
- **Estimated build time:** 9 weeks
- **Pricing:** one-time purchase, $14.99 via `play_billing_direct`
- **Runtime AI:** yes (ai_gateway_llm): Compile messy tattoo intent into structured subject, symbolism, placement, style, exclusions, stencil, and safety fields before image generation. ≈ $0.002/call; Generate a tattoo preview, high-resolution export, retry variation, or stencil PNG from the compiled prompt. ≈ $0.04/call
- **Permissions:** `INTERNET`

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

- No subscription plan, weekly trial, renewal funnel, or in-app cancellation workflow beyond linking users to Google Play purchase management.
- No rewarded ads or ad-unlock economy, because the report identifies broken ad rewards as a reliability complaint.
- No account system, cloud sync, social feed, or cross-device project library for v1.
- No custom model training or fine-tuning; v1 uses managed AI calls only.
- No live AR camera try-on; placement preview is a static body-area guide and generated artwork preview.
- No legal tattoo consultation, medical skin advice, or tattoo artist marketplace.

## Tech stack

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

| Purpose | Gradle coordinate |
| --- | --- |
| Compose Material 3 UI components | `androidx.compose.material3:material3:1.3.1` |
| Compose Navigation graph for screens and routes | `androidx.navigation:navigation-compose:2.8.5` |
| Activity integration for Jetpack Compose | `androidx.activity:activity-compose:1.9.3` |
| Lifecycle-aware ViewModel state collection in Compose | `androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7` |
| Lifecycle runtime Kotlin extensions | `androidx.lifecycle:lifecycle-runtime-ktx:2.8.7` |
| Local persistence for saved tattoo projects and generation attempts | `androidx.room:room-runtime:2.6.1` |
| Room coroutine and Flow support | `androidx.room:room-ktx:2.6.1` |
| Room annotation processor for entities and DAOs | `androidx.room:room-compiler:2.6.1` |
| Image loading from local file paths and remote temporary generation URLs | `io.coil-kt:coil-compose:2.7.0` |
| Google Play Billing one-time Pro pack purchase and entitlement query | `com.android.billingclient:billing-ktx:7.1.1` |
| HTTP client for AI gateway LLM and image-generation requests | `com.squareup.okhttp3:okhttp:4.12.0` |
| JSON encoding and decoding for prompt compiler, AI gateway, and stored compiled prompt payloads | `org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3` |
| Type-safe date and time handling for saved projects | `org.jetbrains.kotlinx:kotlinx-datetime:0.6.1` |
| Local key-value storage for onboarding-complete and cached Pro entitlement flags | `androidx.datastore:datastore-preferences:1.1.1` |

## Design system

- **Primary color:** `#2F6F5E`
- **Background color:** `#FAF7F0`
- **Error color:** `#B3261E`
- **Typography:** Material 3 default type scale, no custom font
- **Launcher icon glyph:** Phosphor `pen-nib` (regular weight)
- **Theme notes:** Use Material 3 dynamic color disabled for brand consistency. Light theme uses background #FAF7F0, surface #FFFFFF, primary #2F6F5E, onPrimary #FFFFFF, onSurface #201A17. Dark theme uses background #171412, surface #211D1A, primary #8FD8BE, onPrimary #00382D, onSurface #EDE0D8. Tattoo previews appear on neutral cards with 16dp rounded corners and no decorative skull, wing, or body imagery baked into the UI.

## Screens

### Honest Onboarding
- **Route:** `onboarding`
- **Purpose:** Explain exactly what the app can do, that the first generated preview is visible before payment, and that Pro is a one-time Google Play purchase.
- **Reached via:** app launch when onboardingComplete is false; tap About this app from Settings
- **Key UI elements:** App logo and title; Three promise cards: first preview before payment, tattoo-specific controls, one-time Pro pack; Plain-language Pro note: higher-resolution exports, more generations, stencil mode, saved projects; Primary button: Start designing; Secondary link: View privacy and AI use
- **States:** initial, privacy_expanded, completed

### Tattoo Builder
- **Route:** `builder`
- **Purpose:** Collect the user's tattoo idea with tattoo-specific controls before calling the AI prompt compiler.
- **Reached via:** app launch when onboardingComplete is true; tap Start designing from Honest Onboarding; tap Edit from Result Preview; tap Retry with edits from Project Detail
- **Key UI elements:** Multiline idea prompt field; Combine symbols chips and free-text field; Negative prompt field labeled Things to avoid; Style selector: fine line, traditional, neo traditional, geometric, minimalist, blackwork, watercolor, realism; Placement selector: forearm, upper arm, wrist, shoulder, chest, back, ankle, thigh, ribs, neck; Body-part fit selector: small, medium, large; Stencil toggle shown disabled for non-Pro with Pro badge; Generate preview button; Link to Gallery
- **States:** empty, editing, validation_error, compiling_prompt, generating, ai_error, moderation_blocked_with_reason, populated_from_project

### Result Preview
- **Route:** `result/{projectId}`
- **Purpose:** Show the generated first result before payment and offer retry, edit, save, share, export, and Pro upgrade actions.
- **Reached via:** successful generation from Tattoo Builder; tap a project from Gallery; tap latest design notification inside app state after generation completes
- **Key UI elements:** Generated tattoo image; Compiled prompt summary card with subject, symbolism, placement, style, exclusions; Buttons: Edit prompt, Retry variation, Save project, Share preview; Export high resolution button with Pro badge when locked; Stencil mode button with Pro badge when locked; Generation status banner and error retry button
- **States:** loading_project, image_loading, populated_free_preview, populated_pro, save_in_progress, save_success, share_sheet_opening, export_locked, export_in_progress, export_success, error_project_missing, error_export_failed

### Gallery
- **Route:** `gallery`
- **Purpose:** List locally saved tattoo projects so users can reopen, edit, retry, share, or export them.
- **Reached via:** tap Gallery from Tattoo Builder; tap Saved projects from Settings; tap Save success View gallery action from Result Preview
- **Key UI elements:** Top app bar with back button; Lazy grid of saved project thumbnails; Project title, style, placement, and updated date; Empty-state message with Create first tattoo button; Overflow actions per project: rename, delete
- **States:** loading, empty, populated, delete_confirming, delete_error, thumbnail_missing_error

### Project Detail
- **Route:** `project/{projectId}`
- **Purpose:** Show one saved tattoo project with its prompt details, generated image, and continuation actions.
- **Reached via:** tap project tile from Gallery; tap Save project then View from Result Preview
- **Key UI elements:** Large project image; Editable project title; Original prompt; Negative prompt; Compiled prompt JSON rendered as readable chips; Buttons: Edit, Retry variation, Share, Export high resolution, Delete
- **States:** loading, populated, rename_editing, delete_confirming, error_project_missing, error_image_missing

### Pro Pack
- **Route:** `pro`
- **Purpose:** Sell the $14.99 one-time Pro pack with transparent benefits and Google Play purchase handling.
- **Reached via:** tap Pro badge on Result Preview; tap stencil toggle while locked on Tattoo Builder; tap Pro Pack from Settings
- **Key UI elements:** Headline: One-time Pro pack; Price text: $14.99 one-time purchase; Benefit list: higher-resolution exports, more generations, stencil mode, saved projects; No subscription or renewal text; Buy with Google Play button; Restore purchases button; Manage purchases link to Google Play subscriptions/purchases page
- **States:** loading_product, not_purchased, purchase_pending, purchased, purchase_cancelled, purchase_error, restore_in_progress, restore_none_found, restore_success, billing_unavailable

### Settings and Support
- **Route:** `settings`
- **Purpose:** Provide purchase transparency, restore/manage links, privacy information, support contact text, and app reliability diagnostics.
- **Reached via:** tap Settings icon from Tattoo Builder; tap Settings icon from Gallery; tap privacy link from Honest Onboarding
- **Key UI elements:** Pro entitlement status row; Restore purchases button; Manage Google Play purchases button; Privacy and AI use section; Support email link; Clear local cache button; App version row
- **States:** loading_entitlement, populated_free, populated_pro, restore_in_progress, restore_success, restore_error, cache_clear_confirming, cache_clear_success

## Data model

### TattooProject (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| title | `String` | user-editable; default is first 40 characters of ideaPrompt |
| ideaPrompt | `String` | raw user tattoo idea |
| negativePrompt | `String` | raw user exclusions; empty string when none |
| symbolsToCombine | `String` | comma-separated symbols entered by user; empty string when none |
| style | `String` | one of fine_line, traditional, neo_traditional, geometric, minimalist, blackwork, watercolor, realism |
| placement | `String` | one of forearm, upper_arm, wrist, shoulder, chest, back, ankle, thigh, ribs, neck |
| bodyPartFit | `String` | one of small, medium, large |
| stencilRequested | `Boolean` | true when user asked for stencil mode |
| compiledPromptJson | `String` | serialized PromptCompilerResult JSON used for generation |
| previewImagePath | `String` | absolute path in app internal files directory for free preview PNG |
| highResImagePath | `String?` | nullable absolute path for Pro high-resolution PNG export |
| createdAt | `Instant` | stored with kotlinx-datetime ISO string converter |
| updatedAt | `Instant` | stored with kotlinx-datetime ISO string converter |

### GenerationAttempt (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| projectId | `Long` | foreign key to TattooProject.id with cascade delete |
| attemptNumber | `Int` | starts at 1 for each project |
| compiledPromptJson | `String` | exact prompt compiler output sent for this attempt |
| imagePrompt | `String` | final positive prompt sent to image model |
| imageNegativePrompt | `String` | final negative prompt sent to image model |
| imagePath | `String` | absolute internal file path to PNG |
| status | `String` | one of compiling, generating, succeeded, failed, blocked |
| errorMessage | `String?` | nullable user-safe failure reason |
| createdAt | `Instant` | stored with kotlinx-datetime ISO string converter |

## Features

### Honest first-session preview before payment

Let the user enter a tattoo idea and see the first generated preview before any Pro purchase prompt is required.

- **Answers complaint:** Make the first result visible before payment.

- **Screens:** Honest Onboarding, Tattoo Builder, Result Preview, Pro Pack

- **Estimated hours:** 40

**Implementation notes:** On first launch, route to onboarding until DataStore boolean onboardingComplete is true. The builder must not show Pro Pack automatically. When Generate preview is tapped, validate ideaPrompt length is at least 8 non-whitespace characters, then run prompt compilation and preview image generation for one 1024px preview PNG regardless of entitlement. Persist the resulting TattooProject and navigate to result/{projectId}. Only lock high-resolution export, stencil mode, saved-project limit beyond local basic saving, and extra generation quota behind Pro UI. The Pro Pack screen is opened only when the user taps a Pro-badged action.

**Acceptance criteria:**
- A fresh install can reach Tattoo Builder without starting a purchase flow.
- A non-Pro user can generate one visible tattoo preview and land on Result Preview.
- The first generated image is displayed before Pro Pack is shown.
- Tapping locked high-resolution export opens Pro Pack, but the preview remains visible behind navigation back.

### Transparent one-time Pro purchase

Sell a $14.99 one-time Pro pack through Google Play Billing with restore and purchase-management links.

- **Answers complaint:** Billing and cancellation trust The most common complaint in the sample is not “AI tattoos are useless”; it is that users feel charged unexpectedly, unable to cancel cleanly, or ignored when asking for refunds and support.

- **Screens:** Pro Pack, Settings and Support, Result Preview, Tattoo Builder

- **Estimated hours:** 40

**Implementation notes:** Use Google Play Billing Library billing-ktx 7.1.1 with one in-app product id `pro_pack_one_time_1499`. Query ProductDetails with ProductType.INAPP when Pro Pack screen opens. Launch BillingFlowParams for that ProductDetails when Buy is tapped. In PurchasesUpdatedListener, acknowledge purchased INAPP purchases using acknowledgePurchase if not already acknowledged, then cache entitlement true in DataStore and refresh UI. Restore uses queryPurchasesAsync(ProductType.INAPP) and sets entitlement true only if a purchase for `pro_pack_one_time_1499` is PURCHASED. The screen text must include exactly: `$14.99 one-time purchase` and `No subscription or renewal`. Manage purchases opens Intent ACTION_VIEW with Uri `https://play.google.com/store/account/subscriptions` so users know where Google Play purchase management lives even though this app does not sell subscriptions.

**Acceptance criteria:**
- The Pro Pack screen never displays trial, weekly, monthly, yearly, renewal, or subscription copy.
- A successful test purchase sets entitlement to Pro and unlocks high-resolution export and stencil mode.
- Restore purchases changes the UI to purchased when Google Play returns the one-time product as PURCHASED.
- Purchase cancellation leaves entitlement false and shows a non-blocking cancelled message.
- The manage purchases button launches a browser or Play Store intent instead of a fake in-app cancellation page.

### Tattoo-specific prompt controls

Provide structured controls for symbols, placement, style, body-part fit, stencil intent, and things to avoid.

- **Answers complaint:** Prompt control and originality Many reviewers ask for the generator to follow the actual prompt: combine multiple ideas, respect placement, obey negative prompts, avoid repeating the same skulls, wings, or bodies, and stop over-blocking ordinary tattoo requests as NSFW.

- **Screens:** Tattoo Builder, Result Preview, Project Detail

- **Estimated hours:** 50

**Implementation notes:** Build Tattoo Builder as a single form backed by immutable BuilderUiState. Style and placement are required selections with defaults `fine_line` and `forearm`. Negative prompt is stored separately from ideaPrompt and is never concatenated into the positive prompt. `symbolsToCombine` chips are appended as a structured array in PromptCompilerRequest. Stencil toggle is disabled for non-Pro and opens Pro Pack when tapped; for Pro it sets stencilRequested true. The Generate button sends all fields to the prompt compiler as JSON keys: idea, symbols_to_combine, negative_prompt, style, placement, body_part_fit, stencil_requested.

**Acceptance criteria:**
- Generated PromptCompilerRequest contains the exact negative prompt text in a negative_prompt field.
- Changing placement from forearm to ribs changes the request placement field to ribs.
- Adding three symbols produces a symbols_to_combine array with three entries in the order entered.
- A non-Pro tap on stencil toggle opens Pro Pack and does not silently enable stencilRequested.

### AI tattoo prompt compiler

Use an LLM call to convert messy tattoo intent into a structured prompt that preserves symbolism, placement, style, exclusions, and stencil requirements.

- **Answers complaint:** Prompt compiler Turn “strength, love, Egypt, calm breakthrough” into structured subject, symbolism, placement, style, exclusions, and stencil requirements before sending it to the generator.

- **Screens:** Tattoo Builder, Result Preview, Project Detail

- **Estimated hours:** 55

**Implementation notes:** Use OkHttp to POST to an OpenAI-compatible AI gateway chat-completions endpoint `${AI_GATEWAY_BASE_URL}/v1/chat/completions` with Authorization bearer `${AI_GATEWAY_API_KEY}` supplied by BuildConfig. Request JSON must set temperature 0.3 and ask for JSON only. The system prompt must instruct the model to output this schema: subject:String, symbolism:Array<String>, composition:String, placement:String, tattoo_style:String, body_fit:String, must_include:Array<String>, must_avoid:Array<String>, safety_classification:String, stencil:Boolean, originality_guidance:String. The user message is the serialized PromptCompilerRequest. Parse with kotlinx.serialization. If parsing fails, retry once with the same payload plus `Return valid JSON only.` If the second parse fails, fall back to a deterministic compiler that maps the raw idea to subject, symbols_to_combine to must_include, negative_prompt split by commas to must_avoid, and selected controls to placement/style/body_fit/stencil. Store the final JSON in TattooProject.compiledPromptJson.

**Acceptance criteria:**
- The compiler output for idea `strength, love, Egypt, calm breakthrough` includes strength, love, Egypt, and calm breakthrough in either subject, symbolism, or must_include.
- The selected placement appears unchanged in compiler output.
- The selected negative prompt terms appear in must_avoid and are not included in must_include.
- Invalid LLM JSON triggers exactly one retry and then deterministic fallback instead of an endless loading state.

### Generation, moderation, and retry reliability

Generate tattoo preview images through a managed image model, separate request safety from generated image safety, and always end loading states with a result or user-safe error.

- **Answers complaint:** Core workflow reliability A smaller but concrete cluster reports ads that do not unlock rewards, maintenance errors, loading loops, broken save/download buttons, and edit or try-on controls that do not function after payment.

- **Screens:** Tattoo Builder, Result Preview, Project Detail

- **Estimated hours:** 65

**Implementation notes:** After prompt compilation, build imagePrompt as `Tattoo design, {tattoo_style}, intended for {placement}, {body_fit} size, composition: {composition}, include: {must_include joined}, symbolism: {symbolism joined}, originality: {originality_guidance}, clean isolated tattoo artwork on plain background`. Build imageNegativePrompt as `photorealistic skin, explicit nudity, gore, hateful symbols, copyrighted character, blurry, low quality` plus compiler must_avoid joined by commas. Before image generation, block only if safety_classification is one of `sexual_content`, `hate`, `self_harm`, or `graphic_violence`; do not block ordinary placement words such as chest, ribs, thigh, body, portrait, angel, vines. POST to `${AI_GATEWAY_BASE_URL}/v1/images/generations` with size 1024x1024 for preview and save returned base64 PNG bytes to `filesDir/projects/{projectId}/preview_{attemptNumber}.png`. Wrap compile and generation in a coroutine with a 90-second timeout. On IOException, HTTP 5xx, timeout, or invalid image bytes, update GenerationAttempt.status to failed and show a Retry button; never leave status compiling or generating after failure. Retry variation increments attemptNumber and adds `Create a distinct composition from previous attempt #{n}; avoid repeating skulls, wings, generic bodies unless explicitly requested.` to imagePrompt.

**Acceptance criteria:**
- A simulated 500 response from image generation produces an error state with Retry and no infinite spinner.
- A request containing `ribs placement with vines and angel memorial portrait` is not blocked solely because of ribs, vines, angel, portrait, or placement wording.
- Retry variation creates a new GenerationAttempt with attemptNumber incremented by 1.
- The retry image prompt includes an instruction to create a distinct composition.

### Stencil mode, placement preview, and exports

Provide Pro-only stencil rendering, placement summary, and reliable share/export paths for generated tattoo images.

- **Answers complaint:** Tattoo styles, placement preview, stencil mode, and exports

- **Screens:** Result Preview, Project Detail, Pro Pack

- **Estimated hours:** 45

**Implementation notes:** For placement preview, show the generated tattoo image on a neutral card with placement text and body-part fit chips; do not implement live camera AR. For Pro stencil mode, request a new image generation using the same compiled prompt but prepend `black line art tattoo stencil, no shading, high contrast, clean transferable outline` and append `color, gradient, filled background, skin photo` to the negative prompt. Save stencil PNG to `filesDir/projects/{projectId}/stencil_{attemptNumber}.png`. For share, use FileProvider with cache copy and ACTION_SEND image/png. For export, use MediaStore.Images.Media on API 29+ with RELATIVE_PATH `Pictures/InkPreview Studio`; on API 26-28 use ACTION_CREATE_DOCUMENT to let the user choose a PNG destination. Export high-resolution uses image generation size 2048x2048 only when entitlement is Pro; non-Pro export tap routes to Pro Pack.

**Acceptance criteria:**
- Non-Pro high-resolution export opens Pro Pack and does not call the image generation endpoint.
- Pro high-resolution export writes a PNG visible through MediaStore on API 29+.
- Share launches an ACTION_SEND chooser with MIME type image/png.
- Stencil generation prompt contains `black line art tattoo stencil` and its negative prompt contains `color`.

### Local gallery, save, edit, and retry flows

Persist generated tattoo projects locally with image paths so users can reopen, edit, share, delete, and retry designs.

- **Answers complaint:** Gallery, save, share, edit, and retry flows

- **Screens:** Gallery, Project Detail, Result Preview, Tattoo Builder

- **Estimated hours:** 35

**Implementation notes:** Use Room for TattooProject and GenerationAttempt, and store image files in app internal storage under `filesDir/projects/{projectId}/`. Save project creates or updates TattooProject with updatedAt set to Clock.System.now(). Gallery observes `SELECT * FROM TattooProject ORDER BY updatedAt DESC` as Flow and displays thumbnails with Coil from previewImagePath. Edit from Project Detail navigates to builder with all project fields prefilled. Delete removes the Room project and recursively deletes its internal project directory. If a thumbnail file is missing, Gallery shows a broken-image placeholder and keeps the project row so the user can delete it rather than crashing.

**Acceptance criteria:**
- Saving a generated preview creates one TattooProject row and one PNG file under filesDir/projects.
- Gallery empty state appears when the Room table has zero projects.
- Gallery populated state orders newer updatedAt projects before older projects.
- Deleting a project removes its Room row and project image directory.
- Opening Edit from Project Detail prepopulates ideaPrompt, negativePrompt, style, placement, bodyPartFit, and symbols.

### Support, privacy, and trust copy

Expose privacy and AI-use details, support contact, purchase restore, and cache clearing from Settings.

- **Answers complaint:** Billing and cancellation trust The most common complaint in the sample is not “AI tattoos are useless”; it is that users feel charged unexpectedly, unable to cancel cleanly, or ignored when asking for refunds and support.

- **Screens:** Settings and Support, Honest Onboarding, Pro Pack

- **Estimated hours:** 30

**Implementation notes:** Settings must show entitlement status from Billing repository, a Restore purchases button, and a Manage Google Play purchases button using ACTION_VIEW. Privacy copy must state that tattoo prompts, selected controls, and generated images are sent to managed AI services for generation, while projects are stored locally on the device unless shared/exported by the user. Support email link uses ACTION_SENDTO with `mailto:support@inkpreview.example`. Clear local cache deletes temporary share files under cacheDir/shared but must not delete Room projects or filesDir/projects.

**Acceptance criteria:**
- Settings displays whether Pro entitlement is active or inactive.
- Support link opens an ACTION_SENDTO mail intent addressed to support@inkpreview.example.
- Privacy section explicitly mentions prompts, controls, generated images, managed AI services, and local project storage.
- Clear local cache leaves existing Gallery projects visible.

## Store listing

- **Title:** InkPreview AI
- **Short description:** See your first AI tattoo before buying. One-time Pro, no subscription.
- **Category:** Art & Design
- **Keywords:** AI tattoo, tattoo design, tattoo stencil, tattoo generator, tattoo ideas, fine line tattoo, blackwork tattoo, tattoo placement
- **Icon prompt:** Create a square Android app icon for an AI tattoo design app named InkPreview AI. Use a warm off-white background, a deep green circular field, and a simple cream pen-nib drawing a single clean tattoo line. Minimal vector style, high contrast, no skulls, no wings, no human body, no text, centered composition, rounded adaptive-icon safe area.

**Long description:**

Design tattoo ideas with controls built for ink, not a generic image box. Describe your concept, combine symbols, choose placement and style, add things to avoid, and see your first AI tattoo preview before any purchase. Upgrade only if you want the one-time Pro pack for higher-resolution exports, more generations, stencil mode, and saved projects. No subscription funnel, no hidden renewal, and purchase management stays with Google Play.

## Legal

- **Regulated category:** none
- **Privacy policy URL:** https://inkpreview.example/privacy (privacy claims verified: no)
- **Data collected:** Tattoo idea prompts entered by the user; Negative prompts and tattoo controls such as style, placement, symbols, and stencil preference; Generated tattoo images sent to or returned from managed AI services; Google Play purchase entitlement status for the one-time Pro pack; Support email address only if the user sends a support email

## Test plan

### 1. Make the first result visible before payment. (instrumented)

1. Install the app with cleared data and a fake BillingClient returning no purchases.
2. Launch the app.
3. Tap Start designing on Honest Onboarding.
4. Enter `small fine line lotus for wrist` in the idea field.
5. Tap Generate preview with fake AI responses returning valid compiler JSON and a valid PNG.
6. Wait for navigation to result/{projectId}.

**Expected:** Result Preview displays the generated image and compiled prompt summary, and Pro Pack has not been opened automatically.

### 2. Completely ignores negative prompts and seems like the model temperature is off. (unit)

1. Create a PromptCompilerRequest with idea `wolf and moon memorial tattoo`, negative_prompt `no skulls, no wings`, style `blackwork`, placement `forearm`, body_part_fit `medium`.
2. Run the prompt request builder.
3. Run the image prompt builder using a compiler result that includes must_avoid `skulls` and `wings`.

**Expected:** The positive image prompt does not contain `no skulls` or `no wings`, and the imageNegativePrompt contains both `skulls` and `wings`.

### 3. Kept generating the same thing over and over despite changing the prompt. (unit)

1. Create an existing GenerationAttempt with attemptNumber 1.
2. Call the retry variation prompt builder for attemptNumber 2 using the same compiled prompt.
3. Inspect the generated imagePrompt string.

**Expected:** The retry imagePrompt contains `Create a distinct composition from previous attempt #2` and `avoid repeating skulls, wings, generic bodies unless explicitly requested`.

### 4. stop over-blocking ordinary tattoo requests as NSFW. (unit)

1. Create compiler outputs for requests containing the words `ribs`, `thigh`, `chest`, `portrait`, `angel`, and `vines` with safety_classification `ordinary_tattoo`.
2. Pass each output to the pre-generation moderation decision function.

**Expected:** Each moderation decision is allowed; none is blocked because of ordinary placement or tattoo vocabulary.

### 5. loading loops and maintenance errors. (instrumented)

1. Configure fake AI image endpoint to return HTTP 500.
2. Open Tattoo Builder and enter `geometric mountain ankle tattoo`.
3. Tap Generate preview.
4. Advance the test dispatcher until all coroutines complete.

**Expected:** Tattoo Builder or Result Preview shows an ai_error state with a visible Retry button, and no progress indicator remains visible.

### 6. broken save/download buttons. (instrumented)

1. Generate a preview with fake AI returning a valid PNG.
2. On Result Preview, tap Save project.
3. Open Gallery.
4. Tap the saved project.
5. Tap Share preview with an Espresso Intents interceptor installed for ACTION_SEND.

**Expected:** Gallery shows one populated project tile, Project Detail opens for that project, and the share intent has action ACTION_SEND and type image/png.

### 7. users feel charged unexpectedly and cannot locate cancellation or purchase management. (instrumented)

1. Open Pro Pack with fake BillingClient product details for one INAPP product.
2. Read all visible text on the screen.
3. Tap Manage purchases.

**Expected:** The screen contains `$14.99 one-time purchase` and `No subscription or renewal`, contains no `monthly` or `weekly` text, and launches an ACTION_VIEW intent for `https://play.google.com/store/account/subscriptions`.

### 8. Ad-to-product mismatch around birthdate, zodiac, astrology, mythical, soul tattoo, or personalized results. (manual)

1. Launch a clean install.
2. On Honest Onboarding, verify the app does not claim quiz-only hidden features or promise results that are not available in the builder.
3. Open Tattoo Builder.
4. Enter `zodiac Leo birthdate memorial soul tattoo with mythical phoenix`.
5. Add symbols `Leo`, `birthdate`, and `phoenix`.
6. Generate a preview.

**Expected:** The requested zodiac, birthdate, soul/memorial, and mythical phoenix concepts are accepted through visible prompt and symbol controls, and the generated result flow does not hide those controls behind an unrelated quiz or forced paywall.

### 9. baseline parity: saved projects survive app restart. (unit)

1. Insert two TattooProject rows with valid previewImagePath values into the Room database.
2. Create a new Gallery ViewModel instance using the same database.
3. Collect the first populated GalleryUiState.

**Expected:** GalleryUiState is populated with exactly two projects ordered by updatedAt descending.

### 10. baseline parity: Pro high-resolution export. (manual)

1. Install an internal test build with a licensed tester account.
2. Buy the one-time Pro pack through Google Play test purchase.
3. Generate a tattoo preview.
4. Tap Export high resolution.
5. Open the device Photos or Files app and navigate to Pictures/InkPreview Studio.

**Expected:** A PNG export for the generated tattoo is present in Pictures/InkPreview Studio and can be opened.

## Build instructions

```sh
set -e
./gradlew clean
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest
keytool -genkeypair -v -keystore release-upload.jks -storepass changeit1234 -keypass changeit1234 -alias upload -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=InkPreview Studio Upload,O=Solo Builder,C=US"
cat > keystore.properties <<'EOF'
storeFile=release-upload.jks
storePassword=changeit1234
keyAlias=upload
keyPassword=changeit1234
EOF
./gradlew bundleRelease
```

## Human gates still required

- `trademark_and_privacy_review`
- `closed_testing_recruitment`
