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:
1 parent
4642d875de
commit
c94df5fc6b
3 files changed
+72
-3
No files matched your search
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user