# InkSend — build spec

Turn any message you type into a styled image — pick a font, color, and background — and send it to WhatsApp in one tap, right from the keyboard, with no forced ads and no 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:** Fontmaker - Font Keyboard App
- **Package id:** `com.takeofflabs.fontmaker`
- **Google Play:** https://play.google.com/store/apps/details?id=com.takeofflabs.fontmaker
- **appy.fyi report:** https://appy.fyi/report/com.takeofflabs.fontmaker
- **Category:** Productivity

## Overview

- **Working name:** InkSend (trademark cleared: no)
- **Package id:** `fyi.appy.inksend`
- **Min / target SDK:** 26 / 35
- **Backend:** none
- **Estimated build time:** 8.4 weeks
- **Pricing:** one-time purchase, $4.99 via `play_billing_direct`
- **Runtime AI:** none
- **Permissions:** none

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

- No AI-assisted glyph completion or AI image generation in v1 — the handwriting font creator requires drawing all 62 characters by hand, keeping v1 fully client-side and ad-free.
- No dedicated one-tap send button for Instagram, Snapchat, TikTok, or Facebook in v1 — the keyboard panel's one-tap action targets WhatsApp specifically; other apps still work through the ordinary system share sheet.
- No cloud backup or cross-device sync of saved styles or handwriting fonts in v1 — everything lives in local Room storage on the one device.
- No animated or video styles, stickers, or GIFs in v1 — output is always a single static PNG image of the typed text.
- No accounts, sign-in, or server-side anything in v1 — backend is none.

## Tech stack

- **Language / UI:** Kotlin, Jetpack Compose
- **Kotlin:** 2.1.20
- **Compose BOM:** 2025.06.01
- **Gradle:** 8.11.1

| Purpose | Gradle coordinate |
| --- | --- |
| Android Kotlin extensions and compatibility helpers | `androidx.core:core-ktx:1.13.1` |
| Compose host Activity | `androidx.activity:activity-compose:1.9.3` |
| Material 3 Compose components | `androidx.compose.material3:material3:1.3.1` |
| Compose Navigation for in-app screens | `androidx.navigation:navigation-compose:2.8.5` |
| ViewModel integration with Compose | `androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6` |
| Local Room database for style presets and handwriting fonts | `androidx.room:room-runtime:2.7.1` |
| Coroutine extensions for Room DAOs | `androidx.room:room-ktx:2.7.1` |
| Room annotation processing (KSP) | `androidx.room:room-compiler:2.7.1` |
| Local key-value storage for the purchase-flag cache and default-style id | `androidx.datastore:datastore-preferences:1.1.1` |
| One-time purchase billing via Play Billing | `com.android.billingclient:billing-ktx:7.1.1` |

## Design system

- **Primary color:** `#5B47E0`
- **Background color:** `#F7F5FF`
- **Error color:** `#D64545`
- **Typography:** Google Fonts 'Fraunces' (serif, expressive) for in-app headings/branding, Material 3 default type scale (Roboto Flex) for all other UI chrome and body text — never used for the rendered style output itself, which always uses the user's selected style font.
- **Launcher icon glyph:** Phosphor `pen` (regular weight)
- **Theme notes:** Dark mode follows the system default: light theme uses background #F7F5FF / surface #FFFFFF / on-background #1C1B1F; dark theme uses background #16151F / surface #201F2C / on-background #E7E1FF. The Keyboard Panel always renders in a fixed dark theme regardless of system setting, matching common keyboard conventions and staying visually distinct from whatever app is being typed into.

## Screens

### Home
- **Route:** `home`
- **Purpose:** Entry point showing all style presets, which one is default, and quick access to keyboard setup, the editor, and settings.
- **Reached via:** app launch; tapping back from Style Editor after saving
- **Key UI elements:** Style preset grid (LazyVerticalGrid); Default style badge on the active preset; '+ New Style' floating action button; 'Enable Keyboard' banner shown until setup is complete; Settings gear icon in the top bar; 'Create your own handwriting font' entry below the built-in styles
- **States:** loading, empty_no_custom_styles, populated, error_failed_to_load_presets

### StyleEditor
- **Route:** `style/{styleId}/edit`
- **Purpose:** Create or edit one style preset's font, text color, and background.
- **Reached via:** tap '+ New Style' on Home; tap a preset's edit icon on Home
- **Key UI elements:** Live text preview canvas; Font family dropdown (bundled fonts plus any saved handwriting fonts); Text color picker; Background color/gradient picker; 'Set as default' checkbox; Save button
- **States:** new_style_blank, editing_existing_style, unsaved_changes_pending, saved_confirmation

### KeyboardSetup
- **Route:** `keyboard-setup`
- **Purpose:** Guide the user through enabling and selecting the app's custom keyboard system-wide, confirmed live.
- **Reached via:** first app launch if the keyboard isn't yet enabled; tapping the 'Enable Keyboard' banner on Home
- **Key UI elements:** Step 1: 'Enable in Settings' button; Step 2: 'Choose Keyboard' button; Status chip (Not enabled / Enabled / Active); Live test-typing text field
- **States:** not_enabled, enabled_not_selected, fully_configured

### KeyboardPanel
- **Route:** `keyboard/panel`
- **Purpose:** The persistent UI shown while this app's keyboard is active in any other app, exposing the one-tap style-and-send action inline while typing.
- **Reached via:** user switches to this app's keyboard via the system keyboard switcher from any text field
- **Key UI elements:** Standard QWERTY key rows; 'Style & Send to WhatsApp' action button; Horizontal quick-style swatch strip; Current default-style indicator chip
- **States:** no_text_entered, text_entered, sending

### HandwritingFontCreator
- **Route:** `handwriting/create`
- **Purpose:** Optional, secondary flow to draw each character by hand and compile them into a personal font usable as one more style.
- **Reached via:** tap 'Create your own handwriting font' entry on Home
- **Key UI elements:** Glyph drawing canvas with baseline guide; Character prompt (e.g. 'Draw: A'); Progress indicator (e.g. '12 / 62'); Undo/Redo/Clear buttons; Save Font button (enabled once all glyphs are drawn)
- **States:** drawing_current_glyph, in_progress_partial_set_saved, complete_all_glyphs_drawn, compiling_font

### Settings
- **Route:** `settings`
- **Purpose:** App-level preferences: default style, purchase status, privacy policy.
- **Reached via:** tap gear icon on Home top bar
- **Key UI elements:** Default style selector; Purchase status row + 'Restore purchase' button; Privacy policy link; App version/about
- **States:** loading, populated

## Data model

### StylePreset (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| name | `String` | user-facing label, e.g. 'Cursive Blue' |
| font_family | `String` | bundled font asset id, or a HandwritingFont.file_path for a personal font |
| text_color_hex | `String` | e.g. #1B6E5C |
| background_type | `String` | 'solid' or 'gradient' |
| background_color_hex | `String` | solid color, or gradient start color |
| background_color_hex_2 | `String` | nullable — gradient end color, empty string when background_type is solid |
| is_default | `Boolean` | exactly one row true at a time, enforced in the DAO transaction |
| is_built_in | `Boolean` | true for the 3 free bundled presets, false for user-created ones |

### HandwritingFont (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| name | `String` | user-given label for their handwriting font |
| file_path | `String` | internal storage path to the compiled .ttf |
| glyphs_completed | `Int` | 0-62, drives the resume/progress state on HandwritingFontCreator |
| created_at | `Instant` |  |

## Features

### Style Gallery & Presets

Browse, create, and pick a default text style — a saved combination of font, text color, and background.

- **Answers complaint:** baseline parity

- **Screens:** Home, StyleEditor

- **Estimated hours:** 50

**Implementation notes:** Persist StylePreset rows in a local Room database (androidx.room). Home screen is a LazyVerticalGrid of preset cards, each rendering a small preview via the shared TextImageRenderer draw function (see the Text-to-Image Renderer & One-Tap Send feature) at low resolution for speed. 'Set as default' updates is_default=true on the tapped row and false on all others inside a single Room @Transaction DAO method. Style Editor uses an inline HSV color wheel + hex input (no third-party color-picker library needed) for text/background color, and a FontFamily dropdown backed by ~8 bundled .ttf assets under app/src/main/assets/fonts plus any completed handwriting fonts.

**Acceptance criteria:**
- Home screen shows at least 3 built-in style presets (font+color+background combos) before any purchase or style creation.
- Tapping a preset's 'Set as default' badge persists that preset as default across app restarts.
- Style Editor lets the user change font family, text color, and background color/gradient and preview the change live before saving.
- Saving a new style in the Style Editor adds it to the Home gallery grid immediately.

### Text-to-Image Renderer & One-Tap Send

Renders the currently typed text as a styled image and sends it to WhatsApp in a single tap from the keyboard panel.

- **Answers complaint:** Ads gate using the font, too

- **Screens:** KeyboardPanel

- **Estimated hours:** 70

**Implementation notes:** Implement TextImageRenderer.render(text: String, style: StylePreset): Bitmap using android.graphics.Canvas + StaticLayout for text layout/wrapping, drawn onto a Bitmap sized to wrap content with 32dp padding, background as a solid color or 2-stop LinearGradient per style.background. Save the Bitmap to a cache file via FileProvider (content://<applicationId>.fileprovider/images/...), then start Intent(ACTION_SEND).setPackage("com.whatsapp").putExtra(EXTRA_STREAM, uri).setType("image/png") with FLAG_GRANT_READ_URI_PERMISSION; if resolveActivity() returns null (WhatsApp not installed), fall back to a plain ACTION_SEND without setPackage so the system share sheet appears instead.

**Acceptance criteria:**
- From the keyboard panel, tapping 'Style & Send' with non-empty typed text renders that exact text as a PNG image using the active style's font/color/background within 500ms on a mid-range device.
- Tapping 'Style & Send' opens WhatsApp's share target (or the system share sheet if WhatsApp isn't installed) with the rendered image already attached, in one tap — no interstitial screen or ad.
- The 'Style & Send' button is disabled, not just a no-op, when the text field is empty.

### Custom Keyboard (IME) Integration

A system-wide custom keyboard that embeds the Style & Send action inline while typing in any app.

- **Answers complaint:** baseline parity

- **Screens:** KeyboardSetup, KeyboardPanel

- **Estimated hours:** 90

**Implementation notes:** Implement InkSendIme : InputMethodService, registered in AndroidManifest.xml with android.permission.BIND_INPUT_METHOD and a method.xml under res/xml declaring supportsSwitchingToNextInputMethod. Host the visible keyboard UI as a ComposeView set via onCreateInputView(), embedding a standard QWERTY key layout (delegate key events through InputConnection.commitText/deleteSurroundingText) plus the persistent bottom action bar. Track composing/committed text via a local StringBuilder synced on each InputConnection call, since InputConnection.getExtractedText() alone is unreliable across host apps — this buffer is what the renderer receives as `text`.

**Acceptance criteria:**
- After enabling the app's keyboard in system Settings > Languages & Input, switching to it in any third-party app (e.g. WhatsApp, Messenger) shows the standard QWERTY layout plus the Style & Send action bar.
- Typing works identically to the system default keyboard (letters, numbers, backspace, enter) with no dropped or duplicated characters.
- Switching away from this keyboard and back preserves the last-typed, not-yet-sent text in the input field.

### Guided Keyboard Setup with Live Test

Step-by-step onboarding that deep-links into system Settings to enable and select the keyboard, confirmed with a live test-typing box.

- **Answers complaint:** baseline parity

- **Screens:** KeyboardSetup

- **Estimated hours:** 30

**Implementation notes:** KeyboardSetup checks enablement via InputMethodManager.getEnabledInputMethodList() (contains this app's ComponentName) and current-selection via Settings.Secure.getString(contentResolver, DEFAULT_INPUT_METHOD) — both re-checked in onResume() so returning from Settings updates the UI live. 'Enable in Settings' launches startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)); 'Choose Keyboard' calls inputMethodManager.showInputMethodPicker() directly. The test-typing TextField below both steps is a real Compose TextField the user can tap into to confirm the switch worked.

**Acceptance criteria:**
- First app launch shows KeyboardSetup if the app's IME isn't yet enabled in system settings.
- Tapping 'Enable in Settings' deep-links directly to Settings > Languages & Input > On-screen keyboard (via ACTION_INPUT_METHOD_SETTINGS), not just the Settings app root.
- The live test-typing box only accepts input once the app's keyboard has actually been switched to, confirming end-to-end that setup succeeded.

### One-Time Purchase, No Ads

A single $4.99 in-app purchase unlocks unlimited custom styles, custom colors, and the handwriting font creator — no ads anywhere, no subscription.

- **Answers complaint:** Trial/subscription confusion

- **Screens:** Settings

- **Estimated hours:** 25

**Implementation notes:** Integrate com.android.billingclient:billing-ktx with one non-consumable product id 'unlock_all_styles' at $4.99, queried via BillingClient.queryProductDetailsAsync and purchased via launchBillingFlow. Gate Style Editor's custom color pickers and the HandwritingFontCreator entry point behind a purchased-state flag cached in DataStore (androidx.datastore:datastore-preferences) and re-verified against BillingClient.queryPurchasesAsync(BillingClient.ProductType.INAPP) on app start. 'Restore purchase' in Settings re-runs queryPurchasesAsync and re-caches the flag — no server round-trip since backend is none.

**Acceptance criteria:**
- The free tier allows using and sending the 3 built-in styles as images at zero cost, with zero ads shown anywhere in the app.
- Attempting to set a custom color or open the Handwriting Font Creator while unpurchased shows a single one-time $4.99 purchase prompt via Play Billing, never a subscription.
- After a successful one-time purchase, 'Restore purchase' on a fresh install (same Google account) unlocks all paid features without repurchasing.

### Personal Handwriting Font (secondary)

An optional flow to draw each character by hand and use the result as one more style — a nice extra, not the app's main focus.

- **Answers complaint:** Unskippable ads while creating the font

- **Screens:** HandwritingFontCreator

- **Estimated hours:** 70

**Implementation notes:** Present each of the 62 required glyphs (A-Z, a-z, 0-9) one at a time on a Compose Canvas with a baseline guideline drawn at 70% height; capture the drawn strokes as a List<Offset> path, convert to a cubic-bezier outline via fixed-width stroke expansion (no ML), then compile all 62 glyph outlines into a valid TrueType font file entirely on-device by writing the glyf/loca/cmap/hmtx tables directly with a small embedded OpenType table writer — no third-party font-compiler library needed since the glyph set and encoding are fixed and small. Save the resulting .ttf to internal storage and register it as one more FontFamily option in Style Editor's dropdown.

**Acceptance criteria:**
- The glyph canvas prompts for all 62 required characters (A-Z, a-z, 0-9) one at a time with a visible progress count.
- A completed set (all 62 glyphs drawn and saved) compiles into a valid, installable font file usable as a style option in the Style Gallery within 10 seconds on a mid-range device.
- No ad, timer, or forced wait is shown between any two glyph-drawing steps.

## Store listing

- **Title:** InkSend: Style & Send Text
- **Short description:** Style your text and send it to WhatsApp in one tap. No ads, no subscription.
- **Category:** Productivity
- **Keywords:** font keyboard, style text, whatsapp fonts, text styler, stylish text, handwriting font, custom keyboard, chat fonts
- **Icon prompt:** Create a 1024x1024 Android app icon for InkSend, a text-styling and WhatsApp-sharing app. Rounded-square adaptive icon style, indigo background (#5B47E0), a simple white fountain-pen nib icon with a short flowing ink stroke curling into a small rounded chat-bubble corner. Flat vector design, high contrast, no text, no brand logos, no photorealistic elements.

**Long description:**

InkSend turns anything you type into a beautifully styled image — your choice of font, text color, and background — and sends it straight to WhatsApp in a single tap, right from your keyboard.

Pick a default style once, then every message you want to stand out is one tap away: type, tap Style & Send, and WhatsApp opens with your image ready to send. Switch styles anytime from a quick strip built into the keyboard.

Want something truly personal? Draw your own handwriting, letter by letter, and use it as one more style — it's a nice extra, not the main event.

Everything happens on your device. No account, no ads, no weekly subscription — unlock every style and your own handwriting font once for $4.99.

## Legal

- **Regulated category:** none
- **Privacy policy URL:** https://www.appyfyi.com/privacy/inksend (privacy claims verified: no)
- **Data collected:** none

## Test plan

### 1. Unskippable ads while creating the font (instrumented)

1. Launch InkSend on an API 35 emulator.
2. Open Handwriting Font Creator and draw and save all 62 glyphs back-to-back, logging a timestamp before the first glyph and after the last glyph is saved.
3. Assert no Activity from an ad SDK package, no WebView-hosted ad content, and no forced delay screen appears at any point during the 62-glyph sequence.
4. Compute elapsed wall time between the first and last timestamp.

**Expected:** No ad or forced-wait screen is ever shown between glyphs, and a scripted 62-glyph sequence completes in well under 60 seconds of actual drawing time — unlike the incumbent's reported 90+ second ad every 3-5 letters.

### 2. Ads gate using the font, too (instrumented)

1. With a stub package declaring the com.whatsapp application id present so resolveActivity can find it, switch the system input method to InkSend's keyboard inside a plain EditText test host app.
2. Type a short message into the test host's text field.
3. Tap the keyboard panel's 'Style & Send' button and capture the resulting launched Intent via an ActivityMonitor.
4. Assert the captured Intent's action, package, and MIME type, and that no intermediate Activity was started between the button tap and this Intent.

**Expected:** Exactly one Intent(ACTION_SEND) targeting com.whatsapp with type image/png is launched within 1 second of the tap, with zero intermediate ad or interstitial screens.

### 3. Trial/subscription confusion (instrumented)

1. Launch InkSend fresh (no prior purchase) and open Style Editor to attempt a custom color.
2. Assert Play Billing's launchBillingFlow is invoked with a single non-consumable (INAPP) product id, never a subscription product id.
3. Complete a test purchase via Play Billing's test-purchase flow.
4. Assert the purchased-state flag becomes true, no free trial or recurring billing is ever offered, and Style Editor's custom color picker becomes accessible immediately.

**Expected:** The only purchase path offered anywhere in the app is the single $4.99 one-time unlock — no weekly/monthly price, no trial period, and no auto-renewing subscription language appears anywhere in the UI.

### 4. baseline parity: the keyboard must type reliably across host apps (instrumented)

1. Enable InkSend's keyboard via InputMethodManager in test setup and select it as the active IME.
2. Open a plain EditText test host Activity and type a 50-character mixed-case string with punctuation and digits using UiAutomator key events.
3. Assert the EditText's final text exactly matches the 50-character input string.

**Expected:** The EditText's committed text is character-for-character identical to what was typed, with zero drop/duplicate/reorder errors.

### 5. baseline parity: a first-time user must reach a working keyboard using only in-app guidance (manual)

1. On a fresh device/emulator with InkSend just installed, follow only the in-app Keyboard Setup screen with no other instructions.
2. Tap 'Enable in Settings', enable the keyboard, return to the app, tap 'Choose Keyboard', select InkSend.
3. Type into the live test-typing box on the Keyboard Setup screen.

**Expected:** A first-time user reaches a fully working, selected InkSend keyboard using only the in-app guided steps, confirmed by successfully typing in the live test box — unlike the incumbent's own reviews flagging missing Samsung setup directions.

## Build instructions

```sh
chmod +x ./gradlew
./gradlew clean
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest
keytool -genkeypair -v -keystore inksend-upload.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload -dname "CN=InkSend Upload,O=AppyFYI,C=US" -storepass changeit -keypass changeit
RELEASE_STORE_FILE="$PWD/inksend-upload.jks" RELEASE_STORE_PASSWORD="changeit" RELEASE_KEY_ALIAS="upload" RELEASE_KEY_PASSWORD="changeit" ./gradlew bundleRelease
```

## Human gates still required

- `trademark_and_privacy_review`
- `closed_testing_recruitment`
