Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ android {
applicationId 'cc.agentlabs.opencode'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 39
versionName "0.4.12"
versionCode 40
versionName "0.4.13"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand Down
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "OpenCode",
"slug": "opencode-mobile",
"version": "0.4.12",
"version": "0.4.13",
"orientation": "portrait",
"scheme": "opencode",
"userInterfaceStyle": "automatic",
Expand Down Expand Up @@ -35,7 +35,7 @@
},
"android": {
"package": "cc.agentlabs.opencode",
"versionCode": 39,
"versionCode": 40,
"usesCleartextTraffic": true,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
Expand Down
7 changes: 6 additions & 1 deletion app/connection/add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import { buildAuth } from "../../src/lib/auth"
import { AnalyticsEvent, track } from "../../src/lib/analytics"
import { submitWaitlistSignup, buildWaitlistMailtoUrl, needsManualEscapeHatch, type QueuedSignup } from "../../src/lib/waitlist"
import { flushPendingSignups, queuePendingSignup, readPendingSignups, dropPendingSignup } from "../../src/lib/waitlist-queue-storage"
import appJson from "../../app.json"

// Same read as sentry.ts: app.json is the single source of the user-visible
// version (package.json/gradle are kept in parity by `npm run check:versions`).
const APP_VERSION = (appJson as { expo?: { version?: string } }).expo?.version ?? "unknown"

export default function AddConnectionScreen() {
const colorScheme = useColorScheme()
Expand Down Expand Up @@ -281,7 +286,7 @@ export default function AddConnectionScreen() {
// Last-resort, user-initiated only. Never opened automatically.
const openWaitlistMailto = async (email: string) => {
try {
await Linking.openURL(buildWaitlistMailtoUrl(email))
await Linking.openURL(buildWaitlistMailtoUrl(email, APP_VERSION))
} catch {
Alert.alert(t("connection.add.waitlist.alertTitle"), t("connection.add.waitlist.noMailAppMessage"))
}
Expand Down
5 changes: 5 additions & 0 deletions distribution/changelogs/40.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
v0.4.13 — Waitlist signups no longer get lost

• Joining the OpenCode Connect waitlist now retries in the background if the network drops.
• A pending signup is saved on-device and re-sent the next time you open the app.
• The email-app fallback only opens if you tap it after repeated failures.
17 changes: 17 additions & 0 deletions distribution/waitlist-signup-path-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ returns 0 results) and the app is not on IzzyOnDroid, so those channels contribu
retry failures. Expect the reconciler's `synced_count` to trend toward the sideload cohort only.
Any plan that assumes "ship an update and the leak closes" is still wrong for those ~436 devices.

## After-number: how it gets attributed (AGE-100)

_Pending — v0.4.13 (the first build users can actually run the retry queue on) ships 2026-08-14._

Baseline at release time, from the hourly reconciler's last run before the cut
(`Waitlist Mailto Reconcile`, 2026-08-14 07:59 UTC): `scanned=34 synced=0 skipped=34 failed=0`
— i.e. 34 known mailto conversations, all already healed into the store, nothing new that hour.

The reconciler reads a Chatwoot mail body, so "split by app version" only works if the body
carries one. It did not. As of v0.4.13 the escape hatch stamps `App: OpenCode Mobile v<version>`
into the mail (`buildWaitlistMailtoUrl`, `src/lib/waitlist.ts`). That gives a clean read a week out:

| Mail body | Means |
| --- | --- |
| no `App:` line | a build older than v0.4.13 — expected, this is the ~436-device sideload cohort no release can reach |
| `App: OpenCode Mobile v0.4.13` or newer | a current build still reached mailto — the retry queue leaked, **file it as a new defect with the mail as evidence** |

## Method / reproducing

```bash
Expand Down
4 changes: 2 additions & 2 deletions distribution/whatsnew/whatsnew-en-US
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
v0.4.12Global recent sessions
v0.4.13Waitlist signups no longer get lost

Recent Sessions now shows every session across all your projects automatically — no need to pick a folder or directory first. The list is populated globally as soon as you connect, and falls back to per-directory listing on older servers.
Joining the OpenCode Connect waitlist now survives a flaky connection: a signup that fails is saved on your device and re-sent automatically the next time you open the app. The email-app fallback only opens if you choose it after repeated retries.
5 changes: 5 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/40.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
v0.4.13 — waitlist signups no longer get lost

- Joining the OpenCode Connect waitlist now retries in the background if the network drops.
- A pending signup is saved on-device and re-sent the next time you open the app.
- The email-app fallback only opens if you tap it after repeated failures.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/mobile",
"version": "0.4.12",
"version": "0.4.13",
"private": true,
"main": "expo-router/entry",
"scripts": {
Expand Down
15 changes: 15 additions & 0 deletions src/lib/waitlist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ test("mailto escape hatch works without an email", () => {
assert.ok(!url.includes("Email"))
})

// AGE-100: the reconciler has to be able to say which build produced a mailto
// signup. Pre-v0.4.8 sideloads (no signup API, unreachable by any release)
// send no App: line; a current build sending one means the retry queue leaked
// and that is a new defect, not the known stale cohort.
test("mailto escape hatch stamps the app version when given one", () => {
const url = buildWaitlistMailtoUrl("dev@example.com", "0.4.13")
assert.ok(url.includes(encodeURIComponent("App: OpenCode Mobile v0.4.13")))
assert.ok(url.includes(encodeURIComponent("Email: dev@example.com")))
})

test("mailto escape hatch omits the version line when the version is unknown", () => {
const url = buildWaitlistMailtoUrl("dev@example.com")
assert.ok(!url.includes("App%3A"))
})

// --- retry decision ---

// 502 named explicitly: the server returns it when Brevo itself fails, and the
Expand Down
14 changes: 12 additions & 2 deletions src/lib/waitlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,20 @@ export function buildWaitlistPayload(email: string): { email: string; source: st
* automatically: a failed signup is queued locally and retried
* (waitlist-queue.ts). This URL is only offered after retries keep failing,
* behind an explicit "still not working? email us" tap.
*
* `appVersion` stamps the build into the mail body (AGE-100). Without it the
* hourly reconciler that heals these mails
* (VibeBrowserProductPage/scripts/reconcile-waitlist-mailto.js) cannot tell a
* ~436-device pre-v0.4.8 sideload — which has no signup API at all and can
* never be fixed by shipping code — from a current build that still fell back,
* which would be a live defect. Absence of the line means "pre-v0.4.13 build".
*/
export function buildWaitlistMailtoUrl(email: string): string {
export function buildWaitlistMailtoUrl(email: string, appVersion?: string): string {
const subject = encodeURIComponent("OpenCode Connect Waitlist")
const body = encodeURIComponent(email ? `Sign me up!\n\nEmail: ${email}` : "Sign me up!")
const lines = ["Sign me up!"]
if (email) lines.push("", `Email: ${email}`)
if (appVersion) lines.push("", `App: OpenCode Mobile v${appVersion}`)
const body = encodeURIComponent(lines.join("\n"))
return `mailto:${WAITLIST_FALLBACK_EMAIL}?subject=${subject}&body=${body}`
}

Expand Down
Loading