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(wip): implement next-auth for expo #133

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 31 additions & 3 deletions apps/expo/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
import { ExpoConfig, ConfigContext } from "@expo/config";

const env = process.env.EXPO_ENVIRONMENT;

/**
* 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.
*/
const localhost =
// Constants.manifest?.debuggerHost?.split(":")[0] ??
process.env.LOCALHOST ?? "localhost";

const appUrl = env === "production" ? "" : `http://${localhost}:3000`;
return appUrl;
};

const APP_URL = getBaseUrl();
const API_URL = APP_URL + "/api/trpc";
const AUTH_URL = APP_URL + "/api/auth";

const defineConfig = (_ctx: ConfigContext): ExpoConfig => ({
name: "expo",
slug: "expo",
Expand All @@ -18,7 +42,7 @@ const defineConfig = (_ctx: ConfigContext): ExpoConfig => ({
assetBundlePatterns: ["**/*"],
ios: {
supportsTablet: true,
bundleIdentifier: "your.bundle.identifier",
bundleIdentifier: "com.juliusmarminge.expo",
},
android: {
adaptiveIcon: {
Expand All @@ -28,10 +52,14 @@ const defineConfig = (_ctx: ConfigContext): ExpoConfig => ({
},
extra: {
eas: {
projectId: "your-project-id",
projectId: "768478b6-46cd-43a3-904b-f4d5065648d2",
Copy link

@ItsWendell ItsWendell Mar 14, 2023

Choose a reason for hiding this comment

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

This should be removed

},
APP_URL,
API_URL,
AUTH_URL,
nextAuthUrl: AUTH_URL,
},
plugins: ["./expo-plugins/with-modify-gradle.js"]
plugins: ["./expo-plugins/with-modify-gradle.js"],
});

export default defineConfig;
2 changes: 1 addition & 1 deletion apps/expo/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ config.resolver.nodeModulesPaths = [
path.resolve(workspaceRoot, "node_modules"),
];
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;
// config.resolver.disableHierarchicalLookup = true;

module.exports = config;
18 changes: 14 additions & 4 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,30 @@
},
"dependencies": {
"@acme/api": "*",
"@acme/auth": "*",
"@acme/tailwind-config": "*",
"@shopify/flash-list": "1.3.1",
"@tanstack/react-query": "^4.20.4",
"@trpc/client": "^10.8.1",
"@trpc/react-query": "^10.8.1",
"@trpc/server": "^10.8.1",
"expo": "~47.0.12",
"expo-apple-authentication": "^5.0.1",
"expo-application": "^5.0.1",
"expo-auth-session": "^3.8.0",
"expo-constants": "^14.0.2",
"expo-random": "^13.0.0",
"expo-secure-store": "^12.0.0",
"expo-status-bar": "~1.4.2",
"nativewind": "^2.0.11",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-native": "0.70.5",
"next-auth": "4.12.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.71.0-rc.5",
"react-native-safe-area-context": "4.4.1",
"expo-splash-screen": "~0.17.5"
"react-native-url-polyfill": "^1.3.0",
"expo-splash-screen": "~0.17.5",
"uuid": "^9.0.0"
},
"devDependencies": {
"@babel/core": "^7.19.3",
Expand Down
17 changes: 11 additions & 6 deletions apps/expo/src/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import { StatusBar } from "expo-status-bar";
import React from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { TRPCProvider } from "./utils/api";
import { SessionProvider } from "next-auth/expo";

import { HomeScreen } from "./screens/home";
import { AuthScreen } from "./screens/auth";

export const App = () => {
return (
<TRPCProvider>
<SafeAreaProvider>
<HomeScreen />
<StatusBar />
</SafeAreaProvider>
</TRPCProvider>
<SessionProvider>
<TRPCProvider>
<SafeAreaProvider>
{/* <HomeScreen /> */}
<AuthScreen />
<StatusBar />
</SafeAreaProvider>
</TRPCProvider>
</SessionProvider>
);
};
70 changes: 70 additions & 0 deletions apps/expo/src/screens/auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import { Text, View, SafeAreaView, TouchableOpacity } from "react-native";
// import { SafeAreaView } from "react-native-safe-area-context";
import * as AppleAuth from "expo-apple-authentication";
import { signIn } from "@acme/auth/src/expo/client";
import { FontAwesome5 } from "@expo/vector-icons";
import { useSession } from "next-auth/expo";
import { nativeApple, socialLogin } from "../utils/auth";

export const AuthScreen = () => {
const session = useSession();

return (
<SafeAreaView className="bg-[#2e026d] bg-gradient-to-b from-[#2e026d] to-[#15162c]">
<View className="h-full w-full p-4">
<Text className="mx-auto pb-2 text-5xl font-bold text-white">
Create <Text className="text-[#cc66ff]">T3</Text> Turbo
</Text>

<View className="flex flex-col items-center gap-4 p-2">
<Text className="mx-auto text-3xl font-bold text-white">
Authenticate yourself!
</Text>
<AppleAuth.AppleAuthenticationButton
buttonType={AppleAuth.AppleAuthenticationButtonType.SIGN_IN}
buttonStyle={AppleAuth.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={5}
className="h-12 w-full"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onPress={async () => {
await signIn(nativeApple);
}}
/>
<TouchableOpacity
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onPress={async () => {
await signIn(() => socialLogin("github"));
}}
className="flex h-12 w-full flex-row items-center justify-center space-x-2 rounded-md bg-black text-white"
>
<FontAwesome5 name="github" size={16} color="white" />
<Text className="text-lg font-semibold text-white">
Sign In With GitHub
</Text>
</TouchableOpacity>
<TouchableOpacity
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onPress={async () => {
await signIn(() => socialLogin("discord"));
}}
className="flex h-12 w-full flex-row items-center justify-center space-x-2 rounded-md bg-black text-white"
>
<FontAwesome5 name="discord" size={16} color="white" />
<Text className="text-lg font-semibold text-white">
Sign In With Discord
</Text>
</TouchableOpacity>
</View>

<View className="flex flex-col items-center gap-4 p-2">
<Text className="mx-auto text-3xl font-bold text-white">
{session.status === "authenticated"
? `Hello ${session.data.user.name ?? "Unknown"}!`
: "Not Authenticated"}
</Text>
</View>
</View>
</SafeAreaView>
);
};
22 changes: 3 additions & 19 deletions apps/expo/src/utils/api.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@acme/api";
/**
Expand All @@ -17,32 +18,15 @@ export type RouterInputs = inferRouterInputs<AppRouter>;
**/
export type RouterOutputs = inferRouterOutputs<AppRouter>;

/**
* Extend this function when going to production by
* setting the baseUrl to your production API URL.
*/
import Constants from "expo-constants";
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.
*/
const localhost = Constants.manifest?.debuggerHost?.split(":")[0];
if (!localhost)
throw new Error("failed to get localhost, configure it manually");
return `http://${localhost}:3000`;
};

/**
* A wrapper for your app that provides the TRPC context.
* Use only in _app.tsx
*/
import React from "react";
import Constants from "expo-constants";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { transformer } from "@acme/api/transformer";
import { inferRouterInputs, inferRouterOutputs } from "@trpc/server";

export const TRPCProvider: React.FC<{ children: React.ReactNode }> = ({
children,
Expand All @@ -53,7 +37,7 @@ export const TRPCProvider: React.FC<{ children: React.ReactNode }> = ({
transformer,
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
url: Constants.expoConfig?.extra?.API_URL as string,
}),
],
}),
Expand Down
128 changes: 128 additions & 0 deletions apps/expo/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as AuthSession from "expo-auth-session";
import { SigninResult, getSignInInfo } from "next-auth/expo";

export const nativeProviders = {
discord: "discord-expo",
github: "github-expo",
} as const;

export const isValidProvider = (
k: string,
): k is keyof typeof nativeProviders => {
return k in nativeProviders;
};

export const nativeDiscoveryUrls = {
discord: {
authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
tokenEndpoint: "https://discord.com/api/oauth2/token",
revocationEndpoint: "https://discord.com/api/oauth2/token/revoke",
},
github: {
authorizationEndpoint: "https://github.com/login/oauth/authorize",
tokenEndpoint: "https://github.com/login/oauth/access_token",
revocationEndpoint:
"https://github.com/settings/connections/applications/XXXXXXXXXXX", // ignore this, it should be set to a clientId.
},
};

export const socialLogin = async (
baseProvider: keyof typeof nativeProviders,
): Promise<SigninResult | null> => {
const proxyRedirectUri = AuthSession.makeRedirectUri({ useProxy: true });

const provider = isValidProvider(baseProvider)
? nativeProviders[baseProvider]
: baseProvider;

const signinInfo = await getSignInInfo({
provider: provider,
proxyRedirectUri,
});

console.log("Signin info", signinInfo);
console.log("redirect", proxyRedirectUri);

if (!signinInfo) {
Alert.alert("Error", "Couldn't get sign in info from server");
return null;
}

const { state, codeChallenge, stateEncrypted, codeVerifier, clientId } =
signinInfo;

const request = new AuthSession.AuthRequest({
clientId: clientId,
scopes: ["identify", "email"],
redirectUri: proxyRedirectUri,
usePKCE: false,
});

const discovery = nativeDiscoveryUrls[baseProvider];

request.state = state;
request.codeChallenge = codeChallenge;
await request.makeAuthUrlAsync(discovery);

const result = await request.promptAsync(discovery, { useProxy: true });

return {
result,
state,
stateEncrypted,
codeVerifier,
provider,
};
};

import * as AppleAuthentication from "expo-apple-authentication";
import Constants from "expo-constants";
import { Alert } from "react-native";

export const nativeApple = async (): Promise<SigninResult | null> => {
const redirectUri = AuthSession.makeRedirectUri({
useProxy: false,
projectNameForProxy: Constants.manifest2?.extra?.scopeKey as string,
});
const signinInfo = await getSignInInfo({
provider: "expo-apple",
proxyRedirectUri: redirectUri,
});

if (!signinInfo) {
Alert.alert("Error", "Couldn't get sign in info from server");
return null;
}
const { state, stateEncrypted, codeVerifier } = signinInfo;
let credential: AppleAuthentication.AppleAuthenticationCredential | undefined;
try {
credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
state,
});
} catch (e) {
console.log("Error signing in with Apple", e);
return null;
}

return {
codeVerifier,
provider: "apple-expo",
result: {
authentication: null,
error: null,
errorCode: null,
params: {
code: credential?.authorizationCode as string,
state,
},
type: "success",
url: "",
},
state,
stateEncrypted,
};
};
2 changes: 1 addition & 1 deletion apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@trpc/react-query": "^10.8.1",
"@trpc/server": "^10.8.1",
"next": "^13.1.1",
"next-auth": "^4.18.6",
"next-auth": "4.12.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"zod": "^3.20.0"
Expand Down
8 changes: 7 additions & 1 deletion apps/nextjs/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import NextAuth from "next-auth";

import { authOptions } from "@acme/auth";
import { NextApiRequest, NextApiResponse } from "next";

export default NextAuth(authOptions);
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextApiRequest, res: NextApiResponse) => {
// console.log("Incoming request", req);
await NextAuth(req, res, authOptions);
// console.log("Responding with", res.statusCode, res);
};
Loading