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:
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user