From c94df5fc6ba2f3b0090d85eaa2b8d834395bff7e Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Mon, 7 Sep 2026 21:03:54 +0200 Subject: [PATCH] =?UTF-8?q?fix(mobile):=20sign-up=20stuck=20on=20"missing?= =?UTF-8?q?=5Frequirements"=20=E2=80=94=20no=20way=20to=20create=20an=20ac?= =?UTF-8?q?count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New sign-ups got stuck after entering the email verification code: Clerk's status stayed on "missing_requirements" (surfaced via the generic auth.signUpIncompleteError as "Sign-up incomplete") instead of "complete", with no way to proceed. Checked this Clerk instance's public /v1/environment config directly: first_name and last_name are both required, and sign_up.legal_consent_enabled is true — web's prebuilt component discovers and renders these automatically, but this hand-built native form only ever collected email + password. Added first/last name fields and a legal-consent checkbox (linking to the actual Terms/Privacy pages), passed as firstName/lastName/ legalAccepted in the same signUp.password() call (Clerk's Futures API accepts all three there — no separate .update() step needed). Submit is disabled until all fields are filled and consent is checked. This blocked 100% of new mobile sign-ups until fixed. --- mobile/app/(auth)/sign-up.tsx | 69 +++++++++++++++++++++++++++++++-- mobile/src/i18n/locales/de.json | 3 ++ mobile/src/i18n/locales/en.json | 3 ++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/mobile/app/(auth)/sign-up.tsx b/mobile/app/(auth)/sign-up.tsx index de02437..9b019f4 100644 --- a/mobile/app/(auth)/sign-up.tsx +++ b/mobile/app/(auth)/sign-up.tsx @@ -1,11 +1,13 @@ import { useMemo, useState } from "react"; -import { View, Text, Pressable, StyleSheet } from "react-native"; +import { View, Text, Pressable, Linking, StyleSheet } from "react-native"; import { useSignUp } from "@clerk/expo"; import { useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; +import { Check } from "lucide-react-native"; import { Button } from "../../src/components/Button"; import { TextField } from "../../src/components/TextField"; import { useTheme } from "../../src/context/ThemeContext"; +import { env } from "../../src/lib/env"; import { getTypography, type ColorTokens, type Typography } from "../../src/theme/tokens"; /** @@ -17,6 +19,17 @@ import { getTypography, type ColorTokens, type Typography } from "../../src/them * Uses Clerk's "Futures" API (signUp.password(), signUp.verifications, * signUp.finalize()), matching sign-in.tsx's pattern — see AGENTS.md before * assuming an older signUp.create()/setActive() shape from training data. + * + * First/last name + legal consent are collected here because this Clerk + * instance requires them (checked live via its public /v1/environment + * config: `first_name`/`last_name` both `required: true`, + * `sign_up.legal_consent_enabled: true`) — web's prebuilt + * component discovers this automatically and renders the fields itself, + * but this custom native form has to ask for them explicitly. Omitting + * them left signUp.status stuck on "missing_requirements" forever after + * email verification, which the generic auth.signUpIncompleteError + * surfaced as "Sign-up incomplete" — a dead end with no way to actually + * finish creating an account. */ export default function SignUpScreen() { const { t } = useTranslation(); @@ -24,12 +37,18 @@ export default function SignUpScreen() { const router = useRouter(); const { colors } = useTheme(); const styles = useMemo(() => makeStyles(colors), [colors]); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); const [emailAddress, setEmailAddress] = useState(""); const [password, setPassword] = useState(""); + const [legalAccepted, setLegalAccepted] = useState(false); const [code, setCode] = useState(""); const [step, setStep] = useState<"credentials" | "verify-email">("credentials"); const [error, setError] = useState(null); + const canSubmit = + firstName.trim().length > 0 && lastName.trim().length > 0 && emailAddress.trim().length > 0 && password.length > 0 && legalAccepted; + const finalizeIfComplete = async () => { if (signUp.status !== "complete") { setError(t("auth.signUpIncompleteError", { status: signUp.status })); @@ -41,7 +60,13 @@ export default function SignUpScreen() { const onSignUpPress = async () => { setError(null); - const { error: signUpError } = await signUp.password({ emailAddress, password }); + const { error: signUpError } = await signUp.password({ + emailAddress, + password, + firstName: firstName.trim(), + lastName: lastName.trim(), + legalAccepted, + }); if (signUpError) { setError(signUpError.longMessage ?? signUpError.message); return; @@ -91,6 +116,16 @@ export default function SignUpScreen() { return ( {t("auth.signUpTitle")} + + + setLegalAccepted((v) => !v)}> + + {legalAccepted && } + + {t("auth.legalConsentLabel")} + + + Linking.openURL(`${env.apiUrl}/nutzungsbedingungen`)}> + {t("onboarding.termsLink")} + + Linking.openURL(`${env.apiUrl}/datenschutz`)}> + {t("onboarding.privacyLink")} + + {error ? {error} : null}