chore(launch): reconcile README/store live-status + add demo-funnel analytics (#111)

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 <noreply@anthropic.com>
This commit is contained in:
Den
2026-07-17 19:49:47 -07:00
committed by GitHub
parent c9ec92d8b7
commit 1ea84f8236
10 changed files with 220 additions and 37 deletions

View File

@@ -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<DemoPermissionReply | null>(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() {
<Text style={[s.ctaSubtitle, isDark && s.metaDark]}>{t("demo.ctaSubtitle")}</Text>
<TouchableOpacity
style={s.connectButton}
onPress={() => router.push("/connection/add")}
onPress={handleConnectPress}
testID="demo-connect-button"
>
<Text style={s.connectButtonText}>{t("demo.connectButton")}</Text>