From 1ea84f8236ab80b1363e48c7def249a3ee336df6 Mon Sep 17 00:00:00 2001
From: Den <2119348+dzianisv@users.noreply.github.com>
Date: Fri, 17 Jul 2026 19:49:47 -0700
Subject: [PATCH] chore(launch): reconcile README/store live-status + add
demo-funnel analytics (#111)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two scoped changes for the no-spend growth launch (Growth Launch Kit,
Notion page 3a1ac25eb49f81099cc9f3a4286c8ec4):
1. README.md and distribution/play-listing.md said Google Play was
"coming soon" / internal-testing-only, while distribution/retention-analysis.md
and the live play.google.com listing show it's actually public with 1K+
installs. Fixed the contradiction, added Google Play as a third install
channel, and added an accurate mention of the new offline demo mode
("Try a Demo" — reasoning, grep, diff, permission prompt, ~30s, no server)
matching what app/demo.tsx + src/lib/demo-script.ts actually render.
play-listing.md's stale pre-launch checklists are marked historical
instead of rewritten, so #83's ASO copy/keyword work is untouched.
2. Added the demo funnel's key metric (demo-completion, per the launch
kit) as four consent-gated PostHog events: demo_started,
demo_step_advanced, demo_completed, demo_exited_to_connect. Pure
property-derivation logic lives in src/lib/demo-analytics.ts (no
RN/PostHog imports, unit-tested with node --test, same pattern as
analytics-classify.ts) and is wired into app/demo.tsx's lifecycle.
Updated docs/analytics.md's event table and the privacy policy's event
list (distribution/privacy-policy.md + its two HTML mirrors) per the
repo's "new event requires a policy update" convention.
Co-authored-by: Claude Fable 5
---
README.md | 29 +++++++++++++++-------
app/demo.tsx | 31 ++++++++++++++++++++----
distribution/play-listing.md | 37 +++++++++++++++++-----------
distribution/privacy-policy.html | 28 +++++++++++++++++++---
distribution/privacy-policy.md | 8 +++++--
docs/analytics.md | 6 ++++-
docs/privacy/index.html | 28 +++++++++++++++++++---
src/lib/analytics.ts | 11 +++++++++
src/lib/demo-analytics.test.ts | 41 ++++++++++++++++++++++++++++++++
src/lib/demo-analytics.ts | 38 +++++++++++++++++++++++++++++
10 files changed, 220 insertions(+), 37 deletions(-)
create mode 100644 src/lib/demo-analytics.test.ts
create mode 100644 src/lib/demo-analytics.ts
diff --git a/README.md b/README.md
index 57eb398..ed2c08e 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
# OpenCode Mobile
**The open-source Android client for the [opencode](https://github.com/sst/opencode) AI coding agent.**
-AI-assisted coding from your phone — Android, via F-Droid or a direct APK.
+AI-assisted coding from your phone — Android, via Google Play, F-Droid, or a direct APK.
[](LICENSE)
[](https://dzianisv.github.io/opencode-mobile/fdroid/repo)
[](https://github.com/dzianisv/opencode-mobile/releases/latest)
-[](#)
+[](https://play.google.com/store/apps/details?id=cc.agentlabs.opencode)
> **Not affiliated with opencode.** OpenCode Mobile is an independent, community-built client and is
> not made by, endorsed by, or affiliated with the opencode / Anomaly team. It talks to an opencode
@@ -14,20 +14,26 @@ AI-assisted coding from your phone — Android, via F-Droid or a direct APK.
---
+**New: tap "Try a Demo" in the app to see the agent fix a real bug — reasoning, a grep, a diff, a permission prompt — in about 30 seconds, no server needed.**
+
+---
+
## Install (Android)
-There are **two working ways** to install OpenCode Mobile today, both for Android:
+There are **three working ways** to install OpenCode Mobile today, all for Android:
-1. **F-Droid (recommended)** — add our self-hosted repo to any F-Droid client, then install/update from there:
+1. **Google Play** — **https://play.google.com/store/apps/details?id=cc.agentlabs.opencode**
+
+2. **F-Droid (self-hosted repo)** — add our self-hosted repo to any F-Droid client, then install/update from there:
```
https://dzianisv.github.io/opencode-mobile/fdroid/repo
```
In the F-Droid app: **Settings → Repositories → + (add)** and paste the URL above. Current version: **v0.4.3**.
-2. **Direct signed APK** — download the latest release and install it manually:
+3. **Direct signed APK** — download the latest release and install it manually:
**https://github.com/dzianisv/opencode-mobile/releases/latest**
-> iOS is not available (see [Roadmap](#roadmap)). Google Play is in internal testing only (no public listing yet). IzzyOnDroid submission is pending.
+> iOS is not available (see [Roadmap](#roadmap)). IzzyOnDroid submission is pending.
---
@@ -45,6 +51,7 @@ OpenCode Mobile is a React Native / Expo app that brings the power of the [openc
## Features
+- **Offline demo mode** — tap "Try a Demo" to see a full bug-fix walkthrough (reasoning → grep → diff → permission prompt) with zero setup, right from the empty state
- **Multi-connection** — manage multiple opencode servers (local network, Cloudflare Tunnel, ngrok, or Tailscale)
- **Biometric unlock** — Face ID, Touch ID, or Android fingerprint protects the app and individual message sends
- **Streaming chat** — token-by-token streaming responses directly from your opencode server
@@ -61,18 +68,20 @@ Package: `cc.agentlabs.opencode` · Android only · current version v0.4.3
| Channel | Status | How |
|---|---|---|
+| **Google Play** | **Live** | [play.google.com/store/apps/details?id=cc.agentlabs.opencode](https://play.google.com/store/apps/details?id=cc.agentlabs.opencode) |
| **F-Droid (self-hosted repo)** | **Live** | Add [`https://dzianisv.github.io/opencode-mobile/fdroid/repo`](https://dzianisv.github.io/opencode-mobile/fdroid/repo) in your F-Droid client |
| **Direct APK** | **Live** | [github.com/dzianisv/opencode-mobile/releases/latest](https://github.com/dzianisv/opencode-mobile/releases/latest) |
-| Google Play | Internal testing / coming soon | No public listing yet |
| IzzyOnDroid | Submission pending | Not live yet |
| Apple App Store / iOS | Not available | See [Roadmap](#roadmap) |
-> The two live, supported install channels are the **F-Droid self-hosted repo** and the **direct signed APK**, both Android. Google Play is internal-testing only, IzzyOnDroid is pending, and there is no iOS build.
+> The three live, supported install channels are **Google Play**, the **F-Droid self-hosted repo**, and the **direct signed APK**, all Android. IzzyOnDroid is pending, and there is no iOS build.
---
## Quick Start
+**Don't have a server yet?** Install the app and tap **Try a Demo** on the Sessions screen first — no setup required. It plays back a scripted bug-fix session through the app's real chat, diff, and permission-approval UI, offline, in about 30 seconds.
+
**Step 1 — Start opencode on your machine**
```bash
@@ -83,7 +92,7 @@ npm install -g opencode
OPENCODE_SERVER_PASSWORD=yourpassword opencode serve --hostname 0.0.0.0 --port 4096
```
-**Step 2 — Install OpenCode Mobile** via the [F-Droid self-hosted repo or direct APK](#install-android) (or build from source — see [CONTRIBUTING.md](CONTRIBUTING.md)).
+**Step 2 — Install OpenCode Mobile** via [Google Play, F-Droid, or a direct APK](#install-android) (or build from source — see [CONTRIBUTING.md](CONTRIBUTING.md)).
**Step 3 — Add a connection in the app**
@@ -132,6 +141,8 @@ OpenCode Mobile is a thin client. It speaks the opencode HTTP + SSE API: listing
| Feature | Status |
|---|---|
+| Offline demo mode | Stable |
+| First-run onboarding clarity | Stable |
| Multi-connection management | Stable |
| Session list + creation | Stable |
| Streaming chat | Stable |
diff --git a/app/demo.tsx b/app/demo.tsx
index df14092..8ab03c0 100644
--- a/app/demo.tsx
+++ b/app/demo.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useMemo, useState } from "react"
+import { useCallback, useEffect, useMemo, useState } from "react"
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, useColorScheme, Linking } from "react-native"
import { Stack, useRouter } from "expo-router"
import { Ionicons } from "@expo/vector-icons"
@@ -6,6 +6,13 @@ import { useTranslation } from "react-i18next"
import { MessageBubble, PermissionPrompt } from "../src/components/chat"
import { buildDemoScript, buildDemoCompletionMessage, buildDemoDenialMessage } from "../src/lib/demo-script"
import { SETUP_GUIDE_URL } from "../src/lib/links"
+import { track, AnalyticsEvent } from "../src/lib/analytics"
+import {
+ demoStepAdvancedProps,
+ demoCompletedOutcome,
+ demoExitedToConnectProps,
+ type DemoPermissionReply,
+} from "../src/lib/demo-analytics"
// Fully offline, scripted walkthrough for installers with no self-hosted
// opencode server. ISOLATION: every message/part below is hardcoded local
@@ -22,12 +29,26 @@ export default function DemoScreen() {
// Built once per mount from pure, hardcoded data (src/lib/demo-script.ts).
const script = useMemo(() => buildDemoScript(), [])
- const [reply, setReply] = useState<"once" | "always" | "reject" | null>(null)
+ const [reply, setReply] = useState(null)
- const handleReply = useCallback((r: "once" | "always" | "reject") => {
- setReply(r)
+ // Fires once per screen mount — this is a fully offline, consent-gated
+ // event (track() is a no-op without telemetry consent, same as every
+ // other analytics call site).
+ useEffect(() => {
+ track(AnalyticsEvent.DemoStarted)
}, [])
+ const handleReply = useCallback((r: DemoPermissionReply) => {
+ setReply(r)
+ track(AnalyticsEvent.DemoStepAdvanced, demoStepAdvancedProps(r))
+ track(AnalyticsEvent.DemoCompleted, { outcome: demoCompletedOutcome(r) })
+ }, [])
+
+ const handleConnectPress = useCallback(() => {
+ track(AnalyticsEvent.DemoExitedToConnect, demoExitedToConnectProps(reply !== null))
+ router.push("/connection/add")
+ }, [reply, router])
+
const completion = useMemo(() => {
if (!reply) return null
return reply === "reject" ? buildDemoDenialMessage() : buildDemoCompletionMessage()
@@ -59,7 +80,7 @@ export default function DemoScreen() {
{t("demo.ctaSubtitle")} router.push("/connection/add")}
+ onPress={handleConnectPress}
testID="demo-connect-button"
>
{t("demo.connectButton")}
diff --git a/distribution/play-listing.md b/distribution/play-listing.md
index 07c9f8b..abaa720 100644
--- a/distribution/play-listing.md
+++ b/distribution/play-listing.md
@@ -1,8 +1,14 @@
# Google Play Store Listing — OpenCode Mobile
-Copy-paste reference for completing the Play Console store listing for `cc.agentlabs.opencode`.
+Copy-paste reference for the Play Console store listing for `cc.agentlabs.opencode`.
Paste these values directly into Play Console. Fields are validated against Play limits.
+> **Status: live.** The app is publicly available on the production track —
+> https://play.google.com/store/apps/details?id=cc.agentlabs.opencode (1K+ installs;
+> see `distribution/retention-analysis.md`). The "First release strategy" and "Pending
+> before first publish" sections below are kept as a historical record of how it got
+> there — do not read them as the current state.
+
---
## Main store listing
@@ -322,25 +328,28 @@ Bump this file before tagging a release. Keep it under 500 chars. Per-language v
---
-## First release strategy
+## First release strategy (historical — completed)
-1. **Internal testing** track first (up to 100 testers, no review) — what CI is wired for.
+1. **Internal testing** track first (up to 100 testers, no review) — what CI was wired for.
2. **Closed testing** — 14+ days, 12+ testers required for new org accounts before promoting to production (Google's 2023 policy).
3. **Open testing** — optional intermediate step.
4. **Production** — only after Closed testing requirements met.
-CI currently publishes to `internal` track. ✅
+Result: the app cleared this path and is now live on the production track (see the
+status note at the top of this file). CI's default dispatch track (`internal`) is
+still used for routine test builds; production releases are dispatched explicitly
+(`track=production`) — see `.github/workflows/publish-play-store.yml`.
---
-## Pending before first publish
+## Pending before first publish (historical — resolved)
-- [ ] Identity verification (government ID upload, Google review)
-- [ ] App icon (real PNG, not placeholder)
-- [ ] Adaptive icon (real PNG)
-- [ ] Feature graphic 1024×500
-- [ ] At least 2 phone screenshots
-- [ ] Privacy policy live at https://dzianisv.github.io/opencode-mobile/privacy/
-- [ ] Decide pricing model
-- [ ] Run IARC content rating questionnaire (after app created in Play Console)
-- [ ] Complete Data safety form (after app created in Play Console)
+- [x] Identity verification (government ID upload, Google review)
+- [x] App icon (real PNG, not placeholder)
+- [x] Adaptive icon (real PNG)
+- [x] Feature graphic 1024×500
+- [x] At least 2 phone screenshots
+- [x] Privacy policy live at https://dzianisv.github.io/opencode-mobile/privacy/
+- [x] Decide pricing model (free, no ads, no IAP)
+- [x] Run IARC content rating questionnaire (after app created in Play Console)
+- [x] Complete Data safety form (after app created in Play Console)
diff --git a/distribution/privacy-policy.html b/distribution/privacy-policy.html
index bb90a42..6e3a028 100644
--- a/distribution/privacy-policy.html
+++ b/distribution/privacy-policy.html
@@ -88,7 +88,7 @@
You open the offline "Try a Demo" screen (no server, no network)
+
—
+
+
+
demo_step_advanced
+
You reply to the demo's scripted permission prompt
+
step_index, step_name, reply ("once", "always", or
+ "reject")
+
+
+
demo_completed
+
The scripted demo reaches its end
+
outcome ("completed" or "denied")
+
+
+
demo_exited_to_connect
+
You tap "Connect your own server" on the demo's CTA
+
reached_completion (true/false)
+
@@ -239,7 +260,8 @@
What analytics events never contain: your server URL, hostname, IP address,
or port; prompts, messages, or AI responses; code or file contents; tokens or credentials;
raw error messages. Connection failures are reduced to a fixed list of coarse categories
- before being sent.
+ before being sent. The demo screen is fully offline and hardcoded — these events describe
+ interaction with the scripted walkthrough, never real session content.
Analytics data is sent to PostHog's EU region (eu.i.posthog.com)
@@ -462,7 +484,7 @@