fix(mobile): sign-up stuck on "missing_requirements" — no way to create an account

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 <SignUp> 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.
This commit is contained in:
admin committed 2026-09-07 21:03:54 +02:00
1 parent 4642d875de
commit c94df5fc6b
3 files changed
+72 -3

No files matched your search

+66 -3
View File
@@ -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 <SignUp>
* 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<string | null>(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 (
<View style={styles.container}>
<Text style={styles.title}>{t("auth.signUpTitle")}</Text>
<TextField
placeholder={t("auth.firstNamePlaceholder")}
value={firstName}
onChangeText={setFirstName}
/>
<TextField
placeholder={t("auth.lastNamePlaceholder")}
value={lastName}
onChangeText={setLastName}
/>
<TextField
autoCapitalize="none"
keyboardType="email-address"
@@ -104,10 +139,24 @@ export default function SignUpScreen() {
value={password}
onChangeText={setPassword}
/>
<Pressable style={styles.checkboxRow} onPress={() => setLegalAccepted((v) => !v)}>
<View style={[styles.checkbox, legalAccepted && styles.checkboxChecked]}>
{legalAccepted && <Check color={colors.pureWhite} size={14} />}
</View>
<Text style={styles.checkboxLabel}>{t("auth.legalConsentLabel")}</Text>
</Pressable>
<View style={styles.legalLinksRow}>
<Pressable onPress={() => Linking.openURL(`${env.apiUrl}/nutzungsbedingungen`)}>
<Text style={styles.link}>{t("onboarding.termsLink")}</Text>
</Pressable>
<Pressable onPress={() => Linking.openURL(`${env.apiUrl}/datenschutz`)}>
<Text style={styles.link}>{t("onboarding.privacyLink")}</Text>
</Pressable>
</View>
{error ? <Text style={styles.error}>{error}</Text> : null}
<Button
title={t("auth.signUpButton")}
disabled={fetchStatus === "fetching"}
disabled={fetchStatus === "fetching" || !canSubmit}
onPress={onSignUpPress}
/>
<Pressable onPress={() => router.replace("/(auth)/sign-in")} hitSlop={8}>
@@ -131,5 +180,19 @@ function makeStyles(colors: ColorTokens) {
hint: { ...typography.body, color: colors.quietGray, marginBottom: 8 },
error: { ...typography.body, color: colors.alertRed },
link: { ...typography.body, color: colors.marigoldDeep, textAlign: "center", marginTop: 8 },
checkboxRow: { flexDirection: "row", alignItems: "flex-start", gap: 10, marginTop: 4 },
checkbox: {
width: 20,
height: 20,
borderRadius: 5,
borderWidth: 1.5,
borderColor: colors.hairlineGray,
alignItems: "center",
justifyContent: "center",
marginTop: 2,
},
checkboxChecked: { backgroundColor: colors.marigoldOrange, borderColor: colors.marigoldOrange },
checkboxLabel: { ...typography.body, flex: 1, fontSize: 13, lineHeight: 19 },
legalLinksRow: { flexDirection: "row", justifyContent: "center", gap: 16 },
});
}
+3
View File
@@ -206,6 +206,9 @@
"auth": {
"signInTitle": "PawFeed Login",
"signUpTitle": "PawFeed Registrierung",
"firstNamePlaceholder": "Vorname",
"lastNamePlaceholder": "Nachname",
"legalConsentLabel": "Ich akzeptiere die Nutzungsbedingungen und die Datenschutzerklärung.",
"emailPlaceholder": "E-Mail",
"passwordPlaceholder": "Passwort",
"codePlaceholder": "Code",
+3
View File
@@ -206,6 +206,9 @@
"auth": {
"signInTitle": "PawFeed Login",
"signUpTitle": "PawFeed Sign Up",
"firstNamePlaceholder": "First name",
"lastNamePlaceholder": "Last name",
"legalConsentLabel": "I accept the Terms of Service and the Privacy Policy.",
"emailPlaceholder": "Email",
"passwordPlaceholder": "Password",
"codePlaceholder": "Code",