feat(waitlist): capture OpenCode Connect signups via beta-signup API (#92)

* feat(waitlist): capture OpenCode Connect signups via beta-signup API (closes #87)

The 'OpenCode Connect — Coming Soon' card only opened a raw mailto: link,
so waitlist signups existed solely as loose emails in the support inbox
with no backend capture.

- POST the signup to https://opencode.agentlabs.cc/api/beta-signup
  (OpenCodeMobileSite route -> Brevo list) tagged with
  source: "opencode-connect-waitlist". The route ignores unknown fields
  today, so the tag is forward-compatible.
- Pure payload/validation/fallback logic lives in src/lib/waitlist.ts
  (no react-native imports, dependency-injected fetch, AbortController
  timeout like diagnostics.ts) with node --test coverage.
- Graceful degradation: transport failures and 5xx fall back to the old
  mailto: path so the signup still reaches the inbox; 4xx asks the user
  to fix their email. Success shows an inline confirmation state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(waitlist): handle mailto fallback failure, name 502 in fallback test

Review findings: Linking.openURL was fire-and-forget, so a device with
no mail app failed the recovery path silently — await it and alert with
a manual instruction instead. Test title now names 502 (Brevo failure)
as an explicit fallback case.

Refs #87

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Den
2026-07-16 23:32:41 -07:00
committed by GitHub
parent 6a9bb6d2d5
commit 0fdfb54d9d
3 changed files with 338 additions and 22 deletions

View File

@@ -19,6 +19,7 @@ import { probeConnection, shareReport } from "../../src/lib/diagnostics"
import { captureDiagnostic } from "../../src/lib/sentry"
import { parseUrl } from "../../src/lib/diagnostics-classify"
import { AnalyticsEvent, track } from "../../src/lib/analytics"
import { submitWaitlistSignup, buildWaitlistMailtoUrl } from "../../src/lib/waitlist"
export default function AddConnectionScreen() {
const colorScheme = useColorScheme()
@@ -37,6 +38,7 @@ export default function AddConnectionScreen() {
const [password, setPassword] = useState("")
const [isConnecting, setIsConnecting] = useState(false)
const [waitlistEmail, setWaitlistEmail] = useState("")
const [waitlistState, setWaitlistState] = useState<"idle" | "submitting" | "joined">("idle")
const buildUrl = () => {
if (mode === "advanced") return url.trim()
@@ -150,11 +152,27 @@ export default function AddConnectionScreen() {
router.back()
}
const handleJoinWaitlist = () => {
const email = waitlistEmail.trim()
const subject = encodeURIComponent("OpenCode Connect Waitlist")
const body = encodeURIComponent(email ? `Sign me up!\n\nEmail: ${email}` : "Sign me up!")
Linking.openURL(`mailto:support@agentlabs.cc?subject=${subject}&body=${body}`)
const handleJoinWaitlist = async () => {
if (waitlistState === "submitting") return
setWaitlistState("submitting")
const result = await submitWaitlistSignup(waitlistEmail)
if (result.ok) {
setWaitlistState("joined")
return
}
setWaitlistState("idle")
if (result.fallback) {
// API unreachable/broken: fall back to the pre-#87 mailto path so the
// signup still reaches the support inbox instead of being lost.
try {
await Linking.openURL(buildWaitlistMailtoUrl(result.email))
} catch {
// No mail app either — tell the user instead of failing silently.
Alert.alert("Join Waitlist", "Could not reach the signup service or open an email app. Please email support@agentlabs.cc with subject \"OpenCode Connect Waitlist\".")
}
} else {
Alert.alert("Join Waitlist", result.error)
}
}
// Quick connect mode - simplified
@@ -283,23 +301,44 @@ export default function AddConnectionScreen() {
Bridge your phone to your opencode server — no tunnel setup, no firewall config. One-tap connect from
anywhere.
</Text>
<TextInput
style={[styles.input, isDark && styles.inputDark, { marginTop: 12 }]}
placeholder="your@email.com"
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={waitlistEmail}
onChangeText={setWaitlistEmail}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
/>
<TouchableOpacity
style={styles.waitlistButton}
onPress={handleJoinWaitlist}
>
<Ionicons name="mail-outline" size={16} color="#ffffff" />
<Text style={styles.waitlistButtonText}>Join Waitlist</Text>
</TouchableOpacity>
{waitlistState === "joined" ? (
<View style={styles.waitlistSuccess} testID="waitlist-success">
<Ionicons name="checkmark-circle" size={20} color="#22c55e" />
<Text style={[styles.waitlistSuccessText, isDark && styles.textDark]}>
You're on the list — we'll email you when OpenCode Connect is ready.
</Text>
</View>
) : (
<>
<TextInput
style={[styles.input, isDark && styles.inputDark, { marginTop: 12 }]}
placeholder="your@email.com"
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={waitlistEmail}
onChangeText={setWaitlistEmail}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
editable={waitlistState !== "submitting"}
testID="waitlist-email-input"
/>
<TouchableOpacity
style={styles.waitlistButton}
onPress={handleJoinWaitlist}
disabled={waitlistState === "submitting"}
testID="waitlist-submit-button"
>
{waitlistState === "submitting" ? (
<ActivityIndicator size="small" color="#ffffff" />
) : (
<>
<Ionicons name="mail-outline" size={16} color="#ffffff" />
<Text style={styles.waitlistButtonText}>Join Waitlist</Text>
</>
)}
</TouchableOpacity>
</>
)}
</View>
{/* Advanced mode link */}
@@ -711,4 +750,16 @@ const styles = StyleSheet.create({
fontWeight: "600",
color: "#ffffff",
},
waitlistSuccess: {
flexDirection: "row",
alignItems: "center",
gap: 8,
marginTop: 12,
},
waitlistSuccessText: {
flex: 1,
fontSize: 13,
color: "#0a0a0a",
lineHeight: 20,
},
})