Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# AI assistant working files (specs, plans, session settings)
.claude/
CLAUDE.md
CLAUDE.local.md
.superpowers/
docs/superpowers/

# Logs
logs
*.log
Expand Down
22 changes: 20 additions & 2 deletions apps/app/app/(app)/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useAuth } from "@clerk/expo";
import { router } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import {
Expand Down Expand Up @@ -161,6 +162,7 @@ function SectionLabel({ children }: { children: string }) {
}

export default function ProfileScreen() {
const { signOut } = useAuth();
const xpProgress = useMemo(() => DEMO_USER.xpCurrent / DEMO_USER.xpForNext, []);
const profile = useUserProfile();
const unlockedCount = PERKS.filter((p) => p.unlocked).length;
Expand All @@ -179,6 +181,18 @@ export default function ProfileScreen() {
setNameModalOpen(false);
}, [draftName]);

const [signingOut, setSigningOut] = useState(false);
const handleSignOut = useCallback(async () => {
if (signingOut) return;
setSigningOut(true);
try {
await signOut();
router.replace("/");
} catch {
setSigningOut(false);
}
}, [signingOut, signOut]);

return (
<Screen>
{/* ── Identity hero ─────────────────────────── */}
Expand Down Expand Up @@ -320,9 +334,13 @@ export default function ProfileScreen() {
</Text>

{/* ── Sign out ──────────────────────────────── */}
<Pressable style={({ pressed }) => [styles.signOutBtn, pressed && styles.pressed]}>
<Pressable
disabled={signingOut}
onPress={handleSignOut}
style={({ pressed }) => [styles.signOutBtn, pressed && styles.pressed]}
>
<LogOut color={colors.danger} size={16} />
<Text style={styles.signOutLabel}>sign out</Text>
<Text style={styles.signOutLabel}>{signingOut ? "signing out…" : "sign out"}</Text>
</Pressable>

<Modal
Expand Down
17 changes: 0 additions & 17 deletions apps/app/app/(auth)/sign-in.native.tsx

This file was deleted.

84 changes: 83 additions & 1 deletion apps/app/app/(auth)/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,83 @@
export { default } from "./sign-in.web";
import { useAuth } from "@clerk/expo";
import { useSignIn } from "@clerk/expo/legacy";
import { Redirect, useRouter } from "expo-router";
import { useState } from "react";
import { StyleSheet, View } from "react-native";

import { AuthScreen } from "@/components/auth/AuthScreen";
import { clerkErrorMessage } from "@/components/auth/clerkError";
import { FormError } from "@/components/auth/FormError";
import { PixelInput } from "@/components/auth/PixelInput";
import { Button } from "@/components/Button";

export default function SignInScreen() {
const { isLoaded, setActive, signIn } = useSignIn();
const router = useRouter();
const [identifier, setIdentifier] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [pending, setPending] = useState(false);

const { isSignedIn } = useAuth();

if (isSignedIn) {
return <Redirect href="/(app)/map" />;
}

const submit = async () => {
if (!isLoaded || pending) {
return;
}
setError(null);
setPending(true);
try {
const attempt = await signIn.create({ identifier: identifier.trim(), password });
if (attempt.status === "complete") {
await setActive({ session: attempt.createdSessionId });
router.replace("/(app)/map");
} else {
setError("something went wrong — try again");
}
} catch (err) {
setError(clerkErrorMessage(err));
} finally {
setPending(false);
}
};

return (
<AuthScreen>
<PixelInput
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
onChangeText={setIdentifier}
placeholder="username or email..."
value={identifier}
/>
<PixelInput
onChangeText={setPassword}
onSubmitEditing={submit}
placeholder="password..."
secureTextEntry
value={password}
/>
<View style={styles.submit}>
<Button
disabled={pending}
label={pending ? "logging in..." : "login"}
onPress={submit}
variant="sage"
/>
</View>
<FormError message={error} />
</AuthScreen>
);
}

const styles = StyleSheet.create({
submit: {
alignSelf: "center",
marginTop: 8,
},
});
49 changes: 0 additions & 49 deletions apps/app/app/(auth)/sign-in.web.tsx

This file was deleted.

17 changes: 0 additions & 17 deletions apps/app/app/(auth)/sign-up.native.tsx

This file was deleted.

113 changes: 112 additions & 1 deletion apps/app/app/(auth)/sign-up.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,112 @@
export { default } from "./sign-in.web";
import { useAuth } from "@clerk/expo";
import { useSignUp } from "@clerk/expo/legacy";
import { Redirect, useRouter } from "expo-router";
import { useState } from "react";
import { StyleSheet, View } from "react-native";

import { AuthScreen } from "@/components/auth/AuthScreen";
import { clerkErrorMessage } from "@/components/auth/clerkError";
import { FormError } from "@/components/auth/FormError";
import { PixelInput } from "@/components/auth/PixelInput";
import { Button } from "@/components/Button";

export default function SignUpScreen() {
const { isLoaded, setActive, signUp } = useSignUp();
const router = useRouter();
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [passwordAgain, setPasswordAgain] = useState("");
const [error, setError] = useState<string | null>(null);
const [pending, setPending] = useState(false);

const passwordsMismatch = passwordAgain.length > 0 && password !== passwordAgain;

const { isSignedIn } = useAuth();

if (isSignedIn) {
return <Redirect href="/(app)/map" />;
}

const submit = async () => {
if (!isLoaded || pending) {
return;
}
setError(null);
if (password !== passwordAgain) {
setError("passwords don't match");
return;
}
setPending(true);
try {
const attempt = await signUp.create({
emailAddress: email.trim(),
password,
username: username.trim(),
});
if (attempt.status === "complete") {
// Clerk instance has verification disabled — straight in.
await setActive({ session: attempt.createdSessionId });
router.replace("/(app)/map");
return;
}
await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
router.push("/(auth)/verify");
} catch (err) {
setError(clerkErrorMessage(err));
} finally {
setPending(false);
}
};

return (
<AuthScreen>
<PixelInput
autoCapitalize="none"
autoCorrect={false}
inputMode="email"
keyboardType="email-address"
onChangeText={setEmail}
placeholder="email..."
value={email}
/>
<PixelInput
autoCapitalize="none"
autoCorrect={false}
onChangeText={setUsername}
placeholder="username..."
value={username}
/>
<PixelInput
onChangeText={setPassword}
placeholder="password..."
secureTextEntry
value={password}
/>
<PixelInput
hasError={passwordsMismatch}
onChangeText={setPasswordAgain}
onSubmitEditing={submit}
placeholder="password again..."
secureTextEntry
value={passwordAgain}
/>
<View style={styles.submit}>
<Button
disabled={pending}
label={pending ? "registering..." : "register"}
onPress={submit}
variant="sage"
/>
</View>
<FormError message={error} />
</AuthScreen>
);
}

const styles = StyleSheet.create({
submit: {
alignSelf: "center",
marginTop: 8,
},
});
49 changes: 0 additions & 49 deletions apps/app/app/(auth)/sign-up.web.tsx

This file was deleted.

Loading
Loading