diff --git a/touchgrass.app/bun.lock b/touchgrass.app/bun.lock
index c0936a3..1f56b80 100644
--- a/touchgrass.app/bun.lock
+++ b/touchgrass.app/bun.lock
@@ -17,7 +17,7 @@
"@react-native/typescript-config": "^0.87.1",
"@types/node": "^26.6.4",
"core-js": "^3.50.0",
- "expo": "~57.0.27",
+ "expo": "^57.0.27",
"expo-constants": "~57.0.21",
"expo-dev-client": "~57.0.19",
"expo-device": "~57.0.2",
@@ -40,12 +40,12 @@
"react-native-gesture-handler": "~2.32.0",
"react-native-mmkv": "^4.3.2",
"react-native-nitro-modules": "^0.37.1",
- "react-native-reanimated": "^4.7.1",
- "react-native-safe-area-context": "^5.10.1",
+ "react-native-reanimated": "4.5.1",
+ "react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.26.0",
"react-native-url-polyfill": "^4.0.0",
"react-native-web": "~0.21.0",
- "react-native-worklets": "^0.13.0",
+ "react-native-worklets": "0.10.1",
},
"devDependencies": {
"@atcute/lexicon-doc": "^3.1.0",
@@ -1455,9 +1455,9 @@
"react-native-nitro-modules": ["react-native-nitro-modules@0.37.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KpW6EQVQ/bfegpCGxN9Be+ndvsfj56t4IESEvCASu9gWpJa/NDfHpIeIwCOvZQ3eXmLEj6YD4wCLqcC1FJPr2w=="],
- "react-native-reanimated": ["react-native-reanimated@4.7.1", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", "react-native": "0.86 - 0.88", "react-native-worklets": "0.13.x" } }, "sha512-HviDFSZ6V4KTRsrLS6hRbBJ4j63l/L8a5az0EoWdJwaMLYnBNyYISTDxwXpEYkMbAbLzCMCviIdiOxXHYQWcNA=="],
+ "react-native-reanimated": ["react-native-reanimated@4.5.1", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", "react-native": "0.83 - 0.86", "react-native-worklets": "0.10.x" } }, "sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA=="],
- "react-native-safe-area-context": ["react-native-safe-area-context@5.10.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ax0T3cAHQIhuAjDQpJ9C1Le96aRiM9PF+Kmgg7Y8+mD7QJMEIx04cG0eG3EGUTdvymgOkCrkakgZFwuOIuNQGw=="],
+ "react-native-safe-area-context": ["react-native-safe-area-context@5.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ=="],
"react-native-screens": ["react-native-screens@4.26.2", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-2XnWsZToKj76trGtEZzx5ELD/qOICFEprEeUntImmitQFVUkea27fiWdUSITArI356Y1qynpXZINW+Unbhky/A=="],
@@ -1465,7 +1465,7 @@
"react-native-web": ["react-native-web@0.21.4", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-77B6KlalB4osNIck91TNFixPkYPsTbuNEvw8P7IV+4v4/5wZSBRKZnw0Zhx6YjuauDTz71gc8IW+fVhcBMjr5A=="],
- "react-native-worklets": ["react-native-worklets@0.13.0", "", { "dependencies": { "@babel/generator": "^7.27.1", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "semver": "^7.7.4", "whatwg-fetch": "^3.6.20" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.86 - 0.88" } }, "sha512-1OrhAeAKaZF4XrBa4YbPQbKzNOpjh52HDlId8e6ete1OCHNdAlrQHzG4gznJ225EAj3dgQXHViJvK51M9B52jQ=="],
+ "react-native-worklets": ["react-native-worklets@0.10.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.4" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.83 - 0.86" } }, "sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
diff --git a/touchgrass.app/nativewind-env.d.ts b/touchgrass.app/nativewind-env.d.ts
index 60b1c7b..8197891 100644
--- a/touchgrass.app/nativewind-env.d.ts
+++ b/touchgrass.app/nativewind-env.d.ts
@@ -1,3 +1,5 @@
///
+///
+
// NOTE: This file should not be edited and should be committed with your source code. It is generated by react-native-css. If you need to move or disable this file, please see the documentation.
\ No newline at end of file
diff --git a/touchgrass.app/package.json b/touchgrass.app/package.json
index 3c78980..406edec 100644
--- a/touchgrass.app/package.json
+++ b/touchgrass.app/package.json
@@ -16,7 +16,7 @@
"@react-native/typescript-config": "^0.87.1",
"@types/node": "^26.6.4",
"core-js": "^3.50.0",
- "expo": "~57.0.27",
+ "expo": "^57.0.27",
"expo-constants": "~57.0.21",
"expo-dev-client": "~57.0.19",
"expo-device": "~57.0.2",
@@ -39,12 +39,12 @@
"react-native-gesture-handler": "~2.32.0",
"react-native-mmkv": "^4.3.2",
"react-native-nitro-modules": "^0.37.1",
- "react-native-reanimated": "^4.7.1",
- "react-native-safe-area-context": "^5.10.1",
+ "react-native-reanimated": "4.5.1",
+ "react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.26.0",
"react-native-url-polyfill": "^4.0.0",
"react-native-web": "~0.21.0",
- "react-native-worklets": "^0.13.0"
+ "react-native-worklets": "0.10.1"
},
"devDependencies": {
"@atcute/lexicon-doc": "^3.1.0",
@@ -77,5 +77,14 @@
},
"patchedDependencies": {
"@atproto/oauth-client-expo@0.2.6": "patches/@atproto%2Foauth-client-expo@0.2.6.patch"
+ },
+ "expo": {
+ "doctor": {
+ "reactNativeDirectoryCheck": {
+ "exclude": [
+ "@atproto/oauth-client-expo"
+ ]
+ }
+ }
}
}
\ No newline at end of file
diff --git a/touchgrass.app/src/app/(authenticated)/_layout.tsx b/touchgrass.app/src/app/(authenticated)/_layout.tsx
new file mode 100644
index 0000000..7a0cd61
--- /dev/null
+++ b/touchgrass.app/src/app/(authenticated)/_layout.tsx
@@ -0,0 +1,12 @@
+import { Stack } from "expo-router";
+
+export default function AuthenticatedLayout() {
+ return (
+
+ );
+}
diff --git a/touchgrass.app/src/app/(authenticated)/home.tsx b/touchgrass.app/src/app/(authenticated)/home.tsx
new file mode 100644
index 0000000..67fdf94
--- /dev/null
+++ b/touchgrass.app/src/app/(authenticated)/home.tsx
@@ -0,0 +1,134 @@
+import { useEffect, useState } from "react";
+import {
+ ActivityIndicator,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+
+import { useAuth } from "../../components/SessionProvider";
+
+type SessionInfo = {
+ did: string;
+ handle: string;
+ active?: boolean;
+};
+
+export default function Home() {
+ const { agent, did, signOut } = useAuth();
+
+ const [info, setInfo] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!agent) return;
+
+ let cancelled = false;
+
+ agent.com.atproto.server
+ .getSession()
+ .then((response) => {
+ if (!cancelled) setInfo(response.data);
+ })
+ .catch((err: unknown) => {
+ if (!cancelled)
+ setError(err instanceof Error ? err.message : String(err));
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [agent]);
+
+ return (
+
+ signed in as
+
+ {info ? `@${info.handle}` : (did ?? "…")}
+
+ {did}
+
+ {error ? (
+ {error}
+ ) : info ? (
+ com.atproto.server.getSession ok
+ ) : (
+
+ )}
+
+
+
+ {
+ void signOut();
+ }}
+ style={({ pressed }) => [
+ styles.button,
+ pressed && styles.buttonPressed,
+ ]}
+ >
+ Sign out
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: "center",
+ paddingHorizontal: 24,
+ },
+ eyebrow: {
+ fontSize: 12,
+ letterSpacing: 1,
+ textTransform: "uppercase",
+ color: "#737373",
+ },
+ handle: {
+ marginTop: 4,
+ fontSize: 28,
+ fontWeight: "700",
+ color: "#ffffff",
+ },
+ meta: {
+ marginTop: 6,
+ fontSize: 12,
+ color: "#a3a3a3",
+ },
+ ok: {
+ marginTop: 20,
+ fontSize: 13,
+ color: "#4ade80",
+ },
+ error: {
+ marginTop: 20,
+ fontSize: 13,
+ color: "#f87171",
+ },
+ spinner: {
+ marginTop: 20,
+ alignSelf: "flex-start",
+ },
+ spacer: {
+ height: 32,
+ },
+ button: {
+ borderWidth: 1,
+ borderColor: "#262626",
+ backgroundColor: "#171717",
+ borderRadius: 16,
+ paddingVertical: 16,
+ alignItems: "center",
+ },
+ buttonPressed: {
+ opacity: 0.7,
+ },
+ buttonLabel: {
+ fontSize: 16,
+ fontWeight: "600",
+ color: "#ffffff",
+ },
+});
diff --git a/touchgrass.app/src/app/_layout.tsx b/touchgrass.app/src/app/_layout.tsx
index 616c6d1..8535d76 100644
--- a/touchgrass.app/src/app/_layout.tsx
+++ b/touchgrass.app/src/app/_layout.tsx
@@ -1,8 +1,7 @@
import { SplashScreen, Stack } from "expo-router";
import { useEffect } from "react";
-
+import { View } from "react-native";
import { AuthProvider, useAuth } from "../components/SessionProvider";
-import "./global.css";
void SplashScreen.preventAutoHideAsync();
@@ -25,14 +24,21 @@ function RootNavigator() {
console.log(`authenticated ${isAuthed}`);
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/touchgrass.app/src/app/auth/callback.tsx b/touchgrass.app/src/app/auth/callback.tsx
new file mode 100644
index 0000000..b51e409
--- /dev/null
+++ b/touchgrass.app/src/app/auth/callback.tsx
@@ -0,0 +1,30 @@
+import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
+
+/**
+ * The client metadata declares `gay.lunarequest.touchgrass:/auth/callback` as
+ * its redirect URI. The in-app browser session normally swallows that redirect
+ * and `signIn()` resolves by itself, so this route usually flashes past — it
+ * exists so that if the redirect is ever handed to the router instead, it lands
+ * on a real route rather than "Unmatched Route".
+ */
+export default function AuthCallback() {
+ return (
+
+
+ finishing sign-in…
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ gap: 12,
+ },
+ label: {
+ fontSize: 14,
+ color: "#a3a3a3",
+ },
+});
diff --git a/touchgrass.app/src/app/global.css b/touchgrass.app/src/app/global.css
index 9db85bf..9431a57 100644
--- a/touchgrass.app/src/app/global.css
+++ b/touchgrass.app/src/app/global.css
@@ -1,2 +1,5 @@
-@import "tailwindcss";
-@import "nativewind/theme";
\ No newline at end of file
+@import "tailwindcss/theme.css" layer(theme);
+@import "tailwindcss/preflight.css" layer(base);
+@import "tailwindcss/utilities.css";
+
+@import "nativewind/theme";
diff --git a/touchgrass.app/src/app/index.tsx b/touchgrass.app/src/app/index.tsx
new file mode 100644
index 0000000..76d440c
--- /dev/null
+++ b/touchgrass.app/src/app/index.tsx
@@ -0,0 +1,11 @@
+import { Redirect } from "expo-router";
+import { useAuth } from "../components/SessionProvider";
+import "./global.css";
+
+export default function Index() {
+ const { isLoading, isAuthed } = useAuth();
+
+ if (isLoading) return null;
+
+ return ;
+}
diff --git a/touchgrass.app/src/app/sign-in.tsx b/touchgrass.app/src/app/sign-in.tsx
index 5dfaa16..dbd2a4d 100644
--- a/touchgrass.app/src/app/sign-in.tsx
+++ b/touchgrass.app/src/app/sign-in.tsx
@@ -1,76 +1,83 @@
-import { StyleSheet, Text, View } from "react-native";
+import OauthForm from "#/components/OAuthForm";
+import { useAuth } from "#/components/SessionProvider";
+import { useState } from "react";
+import { Pressable, Text, View } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+type LoginMode = "oauth" | "legacy";
+
+type SignInProps = {
+ setError?: (v: string) => void;
+};
+
+export default function SignIn({ setError = () => {} }: SignInProps) {
+ const { isLoading, signIn } = useAuth();
+ const [mode, setMode] = useState("oauth");
+
+ const switchMode = (next: LoginMode) => {
+ if (next === mode) return;
+ setError("");
+ setMode(next);
+ };
+
+ if (isLoading) return null;
-export default function SignIn() {
- console.log("sign in page loaded");
return (
-
-
- 🌱
-
+
+
+
+
+ Sign In
+
+
- Touch Grass
+
+ switchMode("oauth")}
+ className={`flex-1 items-center justify-center border-b-2 py-4 ${
+ mode === "oauth" ? "border-b-emerald-500" : "border-b-transparent"
+ }`}
+ >
+
+ OAuth
+
+
- Your app is running!
+ switchMode("legacy")}
+ className={`flex-1 items-center justify-center border-b-2 py-4 ${
+ mode === "legacy"
+ ? "border-b-emerald-500"
+ : "border-b-transparent"
+ }`}
+ >
+
+ Legacy
+
+
+
-
- Everything looks good.
-
- Expo Router · React Native · TypeScript
-
+ {mode === "oauth" ? (
+
+ ) : (
+ Place holder for legacy login
+ )}
-
+
);
}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: "#0a0a0a",
- paddingHorizontal: 24,
- },
- icon: {
- width: 80,
- height: 80,
- alignItems: "center",
- justifyContent: "center",
- borderRadius: 24,
- backgroundColor: "#4ade80",
- marginBottom: 24,
- },
- emoji: {
- fontSize: 36,
- },
- title: {
- fontSize: 30,
- fontWeight: "700",
- color: "#ffffff",
- },
- subtitle: {
- marginTop: 12,
- fontSize: 16,
- color: "#a3a3a3",
- },
- card: {
- marginTop: 32,
- paddingVertical: 16,
- paddingHorizontal: 20,
- borderRadius: 16,
- borderWidth: 1,
- borderColor: "#262626",
- backgroundColor: "#171717",
- alignItems: "center",
- },
- status: {
- fontSize: 15,
- fontWeight: "600",
- color: "#4ade80",
- },
- details: {
- marginTop: 8,
- fontSize: 12,
- color: "#a3a3a3",
- textAlign: "center",
- },
-});
diff --git a/touchgrass.app/src/components/OAuthForm.tsx b/touchgrass.app/src/components/OAuthForm.tsx
new file mode 100644
index 0000000..873d8f0
--- /dev/null
+++ b/touchgrass.app/src/components/OAuthForm.tsx
@@ -0,0 +1,27 @@
+import { useState } from "react";
+import { Pressable, Text, TextInput, View } from "react-native";
+
+type OAuthFormProps = {
+ signIn?: (v: string) => Promise;
+};
+
+export default function OAuthForm({ signIn }: OAuthFormProps) {
+ const [handle, setHandle] = useState("");
+ return (
+
+
+ @
+
+
+
+ Login
+
+
+ );
+}
diff --git a/touchgrass.app/src/components/SessionProvider.tsx b/touchgrass.app/src/components/SessionProvider.tsx
index 2409ec2..799f390 100644
--- a/touchgrass.app/src/components/SessionProvider.tsx
+++ b/touchgrass.app/src/components/SessionProvider.tsx
@@ -72,14 +72,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const signIn = useCallback(
async (handle: string) => {
- const result = await oauthClient.signIn(handle);
+ const session = await oauthClient.signIn(handle);
- //@ts-expect-error: TS2339 the types are fucky
- if (result.status !== "success") {
- return;
- }
-
- await setSession(result);
+ await setSession(session);
},
[setSession],
);
diff --git a/touchgrass.app/tsconfig.json b/touchgrass.app/tsconfig.json
index 20e6bfc..121e6e5 100644
--- a/touchgrass.app/tsconfig.json
+++ b/touchgrass.app/tsconfig.json
@@ -40,7 +40,7 @@
}
],
"customConditions": [
- "react-native-strict-api"
+ "react-native"
]
},
"exclude": [