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}
+
+
+ );
+ }
+
+ return (
+
+ PawFeed Registrierung
+
+
+ {error ? {error} : null}
+
+ router.replace("/(auth)/sign-in")} hitSlop={8}>
+ Schon registriert? Anmelden
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: "center",
+ padding: 24,
+ gap: 12,
+ backgroundColor: colors.porchWhite,
+ },
+ 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 },
+});