From 330cfdeac2cd3c7b609c5d7990d241e3f0cf7d7f Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 6 Sep 2026 14:49:13 +0200 Subject: [PATCH] feat(mobile): add sign-up screen sign-in.tsx only ever covered login (per its own comment "sign-up screens are follow-up work"). Adds the other half: email/password sign-up using Clerk's Futures API (signUp.password() -> verifications.sendEmailCode()/ verifyEmailCode() -> finalize()), mirroring sign-in.tsx's two-step credentials/verify shape. No invite-code step, since INVITE_REQUIRED is globally off on the backend since launch. Verified live on the Android emulator: real Clerk validation surfaces correctly (breached-password rejection), and a valid submission correctly sends a verification email and transitions to the code-entry step. Co-Authored-By: Claude Sonnet 5 --- mobile/README.md | 15 +++- mobile/app/(auth)/sign-in.tsx | 11 +-- mobile/app/(auth)/sign-up.tsx | 129 ++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 mobile/app/(auth)/sign-up.tsx diff --git a/mobile/README.md b/mobile/README.md index c227225..51e4321 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -31,7 +31,13 @@ account and come later (same codebase, no separate source tree — see - **Auth:** `@clerk/expo` (the current SDK — `@clerk/clerk-expo` is deprecated, don't reinstall it). Session JWT persists in the OS keystore via `expo-secure-store` (`src/lib/tokenCache.ts`) instead of Clerk's - in-memory default. + in-memory default. Sign-in and sign-up (`app/(auth)/sign-in.tsx`, + `sign-up.tsx`) are both custom screens using Clerk's "Futures" API + (`signIn.password()`/`signUp.password()` + `verifications`/`finalize()`) + rather than the pre-built ``/`` web components, which + don't render in React Native. Sign-up has no invite-code step — unlike + web's `/join` gate, `INVITE_REQUIRED` is globally off on the backend + since launch. - **API:** `src/api/trpc.ts` creates a `@trpc/client` pointed at `${EXPO_PUBLIC_API_URL}/api/trpc`, with the Clerk session token attached as `Authorization: Bearer ` on every request (`useAuth().getToken`, @@ -155,6 +161,7 @@ mobile/ _layout.tsx # ClerkProvider + persisted QueryClient/tRPC + ActivePetProvider + auth redirect (auth)/ sign-in.tsx # email/password + Clerk Device Trust email-code step + sign-up.tsx # email/password + Clerk email-verification-code step, no invite code (app)/ _layout.tsx # Stack hosting the (tabs) group + the messages/search/story/comment screens (tabs)/ @@ -229,6 +236,12 @@ and verified end-to-end against the live backend on a real Android emulator. Still missing, roughly in the order they'd unblock the most: - No i18n — the whole app is hardcoded German text, matching the sign-in screen +- **No onboarding/first-pet-creation flow.** Web redirects an owner with + zero pets to `/onboarding/pet`; mobile has no equivalent screen yet. A + freshly signed-up account (see sign-up.tsx above) lands in `(app)` with + `activePetId: null` and no way to create a pet from the app — every + pet-scoped screen expects a pet to already exist. Needed before sign-up + is actually useful end-to-end, not just reachable. ### Push notifications diff --git a/mobile/app/(auth)/sign-in.tsx b/mobile/app/(auth)/sign-in.tsx index de1ea25..cd69aaa 100644 --- a/mobile/app/(auth)/sign-in.tsx +++ b/mobile/app/(auth)/sign-in.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { View, Text, StyleSheet } from "react-native"; +import { View, Text, Pressable, StyleSheet } from "react-native"; import { useSignIn } from "@clerk/expo"; import { useRouter } from "expo-router"; import { Button } from "../../src/components/Button"; @@ -7,9 +7,8 @@ import { TextField } from "../../src/components/TextField"; import { colors, typography } from "../../src/theme/tokens"; /** - * Minimal email/password sign-in — enough to prove the Clerk <-> backend - * auth flow end to end. Social sign-in / sign-up screens are follow-up work, - * not part of the initial scaffold. + * Minimal email/password sign-in. Social sign-in is still follow-up work; + * password sign-up now exists at sign-up.tsx. * * Uses Clerk's newer "Futures" API (signIn.password()/signIn.finalize()) — * the classic signIn.create()/setActive() pattern from older Clerk docs @@ -111,6 +110,9 @@ export default function SignInScreen() { disabled={fetchStatus === "fetching"} onPress={onSignInPress} /> + router.replace("/(auth)/sign-up")} hitSlop={8}> + Noch kein Account? Registrieren + ); } @@ -126,4 +128,5 @@ const styles = StyleSheet.create({ title: { ...typography.headline, marginBottom: 16 }, hint: { ...typography.body, color: colors.quietGray, marginBottom: 8 }, error: { ...typography.body, color: colors.alertRed }, + link: { ...typography.body, color: colors.marigoldDeep, textAlign: "center", marginTop: 8 }, }); diff --git a/mobile/app/(auth)/sign-up.tsx b/mobile/app/(auth)/sign-up.tsx new file mode 100644 index 0000000..b5535d4 --- /dev/null +++ b/mobile/app/(auth)/sign-up.tsx @@ -0,0 +1,129 @@ +import { useState } from "react"; +import { View, Text, Pressable, StyleSheet } from "react-native"; +import { useSignUp } from "@clerk/expo"; +import { useRouter } from "expo-router"; +import { Button } from "../../src/components/Button"; +import { TextField } from "../../src/components/TextField"; +import { colors, typography } from "../../src/theme/tokens"; + +/** + * Email/password sign-up — the mobile app's other half of sign-in.tsx (that + * screen only ever covered login, per its own comment "sign-up screens are + * follow-up work"). No invite-code step: INVITE_REQUIRED is globally off on + * the backend since launch (post-beta), unlike web's /join gate. + * + * 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. + */ +export default function SignUpScreen() { + const { signUp, fetchStatus } = useSignUp(); + const router = useRouter(); + const [emailAddress, setEmailAddress] = useState(""); + const [password, setPassword] = useState(""); + const [code, setCode] = useState(""); + const [step, setStep] = useState<"credentials" | "verify-email">("credentials"); + const [error, setError] = useState(null); + + const finalizeIfComplete = async () => { + if (signUp.status !== "complete") { + setError(`Registrierung unvollständig — Status: ${signUp.status}`); + return; + } + await signUp.finalize(); + router.replace("/(app)"); + }; + + const onSignUpPress = async () => { + setError(null); + const { error: signUpError } = await signUp.password({ emailAddress, password }); + if (signUpError) { + setError(signUpError.longMessage ?? signUpError.message); + return; + } + + if (signUp.verifications.emailAddress.status !== "verified") { + const { error: sendError } = await signUp.verifications.sendEmailCode(); + if (sendError) { + setError(sendError.longMessage ?? sendError.message); + return; + } + setStep("verify-email"); + return; + } + + await finalizeIfComplete(); + }; + + const onVerifyPress = async () => { + setError(null); + const { error: verifyError } = await signUp.verifications.verifyEmailCode({ code }); + if (verifyError) { + setError(verifyError.longMessage ?? verifyError.message); + return; + } + await finalizeIfComplete(); + }; + + if (step === "verify-email") { + return ( + + E-Mail bestätigen + + Wir haben einen Code an {emailAddress} geschickt, um deine E-Mail-Adresse zu bestätigen. + + + {error ? {error} : null} +