Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use expo-linking to construct urls for expo auth #832

Merged
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ To add a new package, simply run `pnpm turbo gen init` in the monorepo root. Thi

The generator sets up the `package.json`, `tsconfig.json` and a `index.ts`, as well as configures all the necessary configurations for tooling around your package such as formatting, linting and typechecking. When the package is created, you're ready to go build out the package.

### 4. Configuring Next-Auth to work with Expo

In order for the CSRF protection to work when developing locally, you will need to set the AUTH_URL to the same IP address your expo dev server is listening on. This address is displayed in your Expo CLI when starting the dev server.

## FAQ

### Does the starter include Solito?
Expand Down
25 changes: 23 additions & 2 deletions apps/expo/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { FlashList } from "@shopify/flash-list";

import type { RouterOutputs } from "~/utils/api";
import { api } from "~/utils/api";
import { useSignIn, useSignOut, useUser } from "~/utils/auth-hooks";
import { useSignIn, useSignOut, useUser } from "~/utils/auth";

function PostCard(props: {
post: RouterOutputs["post"]["all"][number];
Expand Down Expand Up @@ -97,6 +97,25 @@ function CreatePost() {
);
}

function MobileAuth() {
const user = useUser();
const signIn = useSignIn();
const signOut = useSignOut();

return (
<>
<Text className="pb-2 text-center text-xl font-semibold text-white">
{user?.name ?? "Not logged in"}
</Text>
<Button
onPress={() => (user ? signOut() : signIn())}
title={user ? "Sign Out" : "Sign In With Discord"}
color={"#5B65E9"}
/>
</>
);
}

export default function Index() {
const utils = api.useUtils();

Expand All @@ -111,14 +130,16 @@ export default function Index() {
});

return (
<SafeAreaView className="bg-[#1F104A]">
<SafeAreaView style={{ backgroundColor: "#1F104A" }}>
{/* Changes page title visible on the header */}
<Stack.Screen options={{ title: "Home Page" }} />
<View className="h-full w-full p-4">
<Text className="pb-2 text-center text-5xl font-bold text-white">
Create <Text className="text-pink-400">T3</Text> Turbo
</Text>

<MobileAuth />

<Button
onPress={() => void utils.post.all.invalidate()}
title="Refresh posts"
Expand Down
27 changes: 1 addition & 26 deletions apps/expo/src/utils/api.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useState } from "react";
import Constants from "expo-constants";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import superjson from "superjson";

import type { AppRouter } from "@acme/api";

import { getBaseUrl } from "./base-url";
import { getToken } from "./session-store";

/**
Expand All @@ -15,31 +15,6 @@ import { getToken } from "./session-store";
export const api = createTRPCReact<AppRouter>();
export { type RouterInputs, type RouterOutputs } from "@acme/api";

/**
* Extend this function when going to production by
* setting the baseUrl to your production API URL.
*/
const getBaseUrl = () => {
/**
* Gets the IP address of your host-machine. If it cannot automatically find it,
* you'll have to manually set it. NOTE: Port 3000 should work for most but confirm
* you don't have anything else running on it, or you'd have to change it.
*
* **NOTE**: This is only for development. In production, you'll want to set the
* baseUrl to your production API URL.
*/
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];

if (!localhost) {
// return "https://turbo.t3.gg";
throw new Error(
"Failed to get localhost. Please point to your production server.",
);
}
return `http://${localhost}:3000`;
};

/**
* A wrapper for your app that provides the TRPC context.
* Use only in _app.tsx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@ import * as Linking from "expo-linking";
import * as Browser from "expo-web-browser";

import { api } from "./api";
import { getBaseUrl } from "./base-url";
import { deleteToken, setToken } from "./session-store";

export const signIn = async () => {
const signInUrl = "http://localhost:3000/api/auth/signin";
const redirectTo = "exp://192.168.10.181:8081/login";
const signInUrl = `${getBaseUrl()}/api/auth/signin`;
const redirectTo = Linking.createURL("/login");
const result = await Browser.openAuthSessionAsync(
`${signInUrl}?expo-redirect=${encodeURIComponent(redirectTo)}`,
redirectTo,
);
if (result.type !== "success") return;

if (result.type !== "success") return;
const url = Linking.parse(result.url);
const sessionToken = String(url.queryParams?.session_token);
if (!sessionToken) return;

await setToken(sessionToken);
// ...
};

export const useUser = () => {
Expand Down
26 changes: 26 additions & 0 deletions apps/expo/src/utils/base-url.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Constants from "expo-constants";

/**
* Extend this function when going to production by
* setting the baseUrl to your production API URL.
*/
export const getBaseUrl = () => {
/**
* Gets the IP address of your host-machine. If it cannot automatically find it,
* you'll have to manually set it. NOTE: Port 3000 should work for most but confirm
* you don't have anything else running on it, or you'd have to change it.
*
* **NOTE**: This is only for development. In production, you'll want to set the
* baseUrl to your production API URL.
*/
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];

if (!localhost) {
// return "https://turbo.t3.gg";
throw new Error(
"Failed to get localhost. Please point to your production server.",
);
}
return `http://${localhost}:3000`;
};
20 changes: 10 additions & 10 deletions apps/nextjs/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,47 @@ import type { NextRequest } from "next/server";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

import { GET as _GET, POST } from "@acme/auth";
import { GET as DEFAULT_GET, POST } from "@acme/auth";

export const runtime = "edge";

const expoRedirectCookieName = "__acme-expo-redirect-state";
const setCookieMatchPattern = /next-auth\.session-token=([^;]+)/;
const EXPO_COOKIE_NAME = "__acme-expo-redirect-state";
const AUTH_COOKIE_PATTERN = /authjs\.session-token=([^;]+)/;

export const GET = async (
req: NextRequest,
props: { params: { nextauth: string[] } },
) => {
const nextauthAction = props.params.nextauth[0];
const isExpoSignIn = req.nextUrl.searchParams.get("expo-redirect");
const isExpoCallback = cookies().get(expoRedirectCookieName);
const isExpoCallback = cookies().get(EXPO_COOKIE_NAME);

if (nextauthAction === "signin" && !!isExpoSignIn) {
// set a cookie we can read in the callback
// to know to send the user back to expo
cookies().set({
name: expoRedirectCookieName,
name: EXPO_COOKIE_NAME,
value: isExpoSignIn,
maxAge: 60 * 10, // 10 min
path: "/",
});
}

if (nextauthAction === "callback" && !!isExpoCallback) {
cookies().delete(expoRedirectCookieName);
cookies().delete(EXPO_COOKIE_NAME);

const authResponse = await _GET(req);
const authResponse = await DEFAULT_GET(req);
const setCookie = authResponse.headers.getSetCookie()[0];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to use this solution with Google and it didn't work, after some debugging it turned out this cookie is actually at index 1, so I think this part needs to be a find or something

const match = setCookie?.match(setCookieMatchPattern)?.[1];
if (!match) throw new Error("No session cookie found");
const match = setCookie?.match(AUTH_COOKIE_PATTERN)?.[1];
if (!match) throw new Error("Unable to find session cookie");

const url = new URL(isExpoCallback.value);
url.searchParams.set("session_token", match);
return NextResponse.redirect(url);
}

// Every other request just calls the default handler
return _GET(req);
return DEFAULT_GET(req);
};

export { POST };