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": [