├── .env-example ├── .eslintrc.js ├── .gitignore ├── README.md ├── apps ├── docs │ ├── .eslintrc.js │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ └── example.ts │ │ └── index.tsx │ └── tsconfig.json ├── mobile │ ├── .env.example │ ├── .eslintrc.json │ ├── .expo-shared │ │ └── assets.json │ ├── .gitignore │ ├── app.json │ ├── assets │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ └── splash.png │ ├── babel.config.js │ ├── index.js │ ├── metro.config.js │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── LoginOptions.tsx │ │ │ ├── Provider.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useAppReady.tsx │ │ │ └── useAuth.tsx │ │ ├── screens │ │ │ ├── LoginScreen.tsx │ │ │ └── index.ts │ │ ├── store │ │ │ └── index.ts │ │ ├── types │ │ │ └── global.ts │ │ └── utils │ │ │ ├── ignore-warnings.ts │ │ │ ├── secure-store.ts │ │ │ └── trpc.ts │ └── tsconfig.json └── web │ ├── .env-example │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── favicon.ico │ ├── src │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ ├── auth │ │ │ │ └── [...nextauth].ts │ │ │ ├── examples.ts │ │ │ ├── jwt.ts │ │ │ ├── restricted.ts │ │ │ └── trpc │ │ │ │ └── [trpc].ts │ │ └── index.tsx │ ├── styles │ │ └── globals.css │ ├── types │ │ └── next-auth.ts │ └── utils │ │ └── trpc.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── package.json ├── packages ├── api │ ├── .gitignore │ ├── db │ │ └── index.ts │ ├── expo-auth │ │ ├── apple-auth.ts │ │ ├── constants.ts │ │ ├── github-auth.ts │ │ ├── google-auth.ts │ │ ├── index.ts │ │ ├── prisma-auth.ts │ │ └── zod.ts │ ├── index.ts │ ├── next-auth │ │ └── index.ts │ ├── package.json │ ├── router │ │ ├── appRouter.ts │ │ ├── context.ts │ │ ├── example.ts │ │ ├── expoAuth.ts │ │ └── index.ts │ ├── tsconfig.json │ └── types │ │ └── global.ts ├── eslint-config-custom │ ├── index.js │ └── package.json ├── hooks │ ├── index.ts │ ├── package.json │ ├── trpc.ts │ └── tsconfig.json ├── tsconfig │ ├── README.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui-web │ ├── index.tsx │ ├── package.json │ ├── src │ └── Button.tsx │ └── tsconfig.json ├── prisma └── schema.prisma ├── scripts └── clean.sh ├── turbo.json └── yarn.lock /.env-example: -------------------------------------------------------------------------------- 1 | # Prisma 2 | DATABASE_URL= 3 | 4 | # NEXT AUTH 5 | NEXTAUTH_URL=http://localhost:3000 6 | NEXTAUTH_SECRET= 7 | 8 | ## Github 9 | GITHUB_ID= 10 | GITHUB_SECRET= 11 | 12 | ## Google 13 | GOOGLE_CLIENT_ID= 14 | GOOGLE_CLIENT_SECRET= 15 | 16 | BASE_URL=http://localhost:3000 17 | 18 | ## Email 19 | EMAIL_SERVER_USER= 20 | EMAIL_SERVER_REFRESH_TOKEN= 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ["custom"], 5 | settings: { 6 | next: { 7 | rootDir: ["apps/*/"], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | # SQLite 36 | db.sqlite 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turbo-t3-expo 2 | 3 | This is an example monorepo for create-t3-app and expo. If you want to create your own from scratch, make sure all of your react, react-dom, @types/react, and @types/react-dom are at the same version as the expo app in other apps. 4 | 5 | ## What's inside? 6 | 7 | This turborepo uses [Yarn](https://classic.yarnpkg.com/lang/en/) as a package manager. It includes the following packages/apps: 8 | 9 | ### Apps and Packages 10 | 11 | - `docs`: a [Next.js](https://nextjs.org) app 12 | - `web`: a [create-t3-app](https://github.com/t3-oss/create-t3-app) app 13 | - `api`: a shared library for all apps. Includes a prisma client, a trpc router, next-auth options, and expo-auth functionalities 14 | - `ui-web`: a stub React component library shared by both `web` and `docs` applications 15 | - `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) 16 | - `tsconfig`: `tsconfig.json`s used throughout the monorepo 17 | - `prisma`: a `schema.prisma` file with optional `db.sqlite` 18 | 19 | ## Next-auth with Expo 20 | 21 | This example app uses a lot of logic from Next-auth to implement the same/similar features for mobile. To make this possible, no cookies are used for authentication. Here's how it works: 22 | 23 | 1. From the Expo app, a request is made and the response is verified. 24 | 2. The Expo verfied response, is sent to a tRCP endpoint and verfied once again by the `.expo-auth` router. 25 | 3. Once successful, a JWT token is generated, the user and account is updated accordingly, and the new JWT and user is sent to the Expo app. 26 | 27 | ## Setup 28 | 29 | This repository can be cloned from https://github.com/mrzachnugent/turbo-t3-expo. 30 | 31 | ``` 32 | git clone git@github.com:mrzachnugent/turbo-t3-expo.git 33 | cd turbo-t3-expo 34 | yarn 35 | ``` 36 | 37 | ### Build 38 | 39 | To build all apps and packages, run the following command in the root folder: 40 | 41 | ``` 42 | yarn run build 43 | ``` 44 | 45 | ### Develop 46 | 47 | To develop all apps and packages, run the following command: 48 | 49 | ``` 50 | cd turbo-t3-expo 51 | yarn run dev 52 | ``` 53 | 54 | ### Remote Caching 55 | 56 | Turborepo can use a technique known as [Remote Caching](https://turborepo.org/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. 57 | 58 | By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup), then enter the following commands: 59 | 60 | ``` 61 | cd my-turborepo 62 | npx turbo login 63 | ``` 64 | 65 | This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). 66 | 67 | Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your turborepo: 68 | 69 | ``` 70 | npx turbo link 71 | ``` 72 | 73 | ## Useful Links 74 | 75 | Learn more about the power of Turborepo: 76 | 77 | - [Pipelines](https://turborepo.org/docs/core-concepts/pipelines) 78 | - [Caching](https://turborepo.org/docs/core-concepts/caching) 79 | - [Remote Caching](https://turborepo.org/docs/core-concepts/remote-caching) 80 | - [Scoped Tasks](https://turborepo.org/docs/core-concepts/scopes) 81 | - [Configuration Options](https://turborepo.org/docs/reference/configuration) 82 | - [CLI Usage](https://turborepo.org/docs/reference/command-line-reference) 83 | -------------------------------------------------------------------------------- /apps/docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["custom"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 12 | 13 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 14 | 15 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /apps/docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/docs/next.config.js: -------------------------------------------------------------------------------- 1 | const withTM = require('next-transpile-modules')(['api', 'ui-web']); 2 | 3 | module.exports = withTM({ 4 | reactStrictMode: true, 5 | }); 6 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --port 3001", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "12.0.8", 13 | "react": "17.0.2", 14 | "react-dom": "17.0.2", 15 | "ui-web": "*", 16 | "api": "*" 17 | }, 18 | "devDependencies": { 19 | "eslint": "7.32.0", 20 | "eslint-config-custom": "*", 21 | "next-transpile-modules": "9.0.0", 22 | "tsconfig": "*", 23 | "@types/node": "^17.0.12", 24 | "@types/react": "17.0.37", 25 | "typescript": "^4.5.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/docs/pages/api/example.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { prisma } from 'api'; 3 | 4 | const examples = async (req: NextApiRequest, res: NextApiResponse) => { 5 | const examples = await prisma.example.findMany(); 6 | res.status(200).json(examples); 7 | }; 8 | 9 | export default examples; 10 | -------------------------------------------------------------------------------- /apps/docs/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'ui-web'; 2 | 3 | export default function Docs() { 4 | return ( 5 |
6 |

Docs

7 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/mobile/.env.example: -------------------------------------------------------------------------------- 1 | # NEXT AUTH 2 | NEXT_API_URL=http://0.0.0.0:3000 3 | 4 | ## Github 5 | GITHUB_ID= 6 | GITHUB_SECRET= 7 | 8 | ## Google 9 | GOOGLE_CLIENT_ID= 10 | GOOGLE_CLIENT_SECRET= 11 | 12 | # EXPO 13 | SECURE_STORE_JWT_KEY= -------------------------------------------------------------------------------- /apps/mobile/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["custom"], 4 | "rules": { 5 | "jsx-a11y/alt-text": [0] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/mobile/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /apps/mobile/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | .env -------------------------------------------------------------------------------- /apps/mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "mobile", 4 | "scheme": "mobile.monolith", 5 | "slug": "mobile", 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "icon": "./assets/icon.png", 9 | "userInterfaceStyle": "light", 10 | "splash": { 11 | "image": "./assets/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "updates": { 16 | "fallbackToCacheTimeout": 0 17 | }, 18 | "assetBundlePatterns": ["**/*"], 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/mobile/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/mobile/assets/adaptive-icon.png -------------------------------------------------------------------------------- /apps/mobile/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/mobile/assets/favicon.png -------------------------------------------------------------------------------- /apps/mobile/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/mobile/assets/icon.png -------------------------------------------------------------------------------- /apps/mobile/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/mobile/assets/splash.png -------------------------------------------------------------------------------- /apps/mobile/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | [ 7 | 'inline-dotenv', 8 | { 9 | path: './.env', 10 | }, 11 | ], 12 | ], 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /apps/mobile/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /apps/mobile/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config'); 2 | const path = require('path'); 3 | 4 | // Find the workspace root, this can be replaced with `find-yarn-workspace-root` 5 | const workspaceRoot = path.resolve(__dirname, '../..'); 6 | const projectRoot = __dirname; 7 | 8 | const config = getDefaultConfig(projectRoot); 9 | 10 | // 1. Watch all files within the monorepo 11 | config.watchFolders = [workspaceRoot]; 12 | // 2. Let Metro know where to resolve packages, and in what order 13 | config.resolver.nodeModulesPaths = [ 14 | path.resolve(projectRoot, 'node_modules'), 15 | path.resolve(workspaceRoot, 'node_modules'), 16 | ]; 17 | 18 | config.resolver.sourceExts = ['jsx', 'js', 'ts', 'tsx', 'cjs']; 19 | 20 | module.exports = config; 21 | -------------------------------------------------------------------------------- /apps/mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "expo start --ios", 7 | "dev:android": "expo start --android", 8 | "start:clean": "expo start -c", 9 | "eject": "expo eject" 10 | }, 11 | "dependencies": { 12 | "@expo-google-fonts/poppins": "^0.2.2", 13 | "@react-native-community/netinfo": "8.2.0", 14 | "@trpc/client": "^9.26.0", 15 | "@trpc/react": "^9.26.0", 16 | "@trpc/server": "^9.26.0", 17 | "api": "*", 18 | "babel-plugin-inline-dotenv": "^1.7.0", 19 | "expo": "~45.0.0", 20 | "expo-apple-authentication": "^4.2.1", 21 | "expo-application": "^4.1.0", 22 | "expo-auth-session": "^3.6.1", 23 | "expo-font": "^10.1.0", 24 | "expo-haptics": "^11.2.0", 25 | "expo-network": "^4.2.0", 26 | "expo-random": "^12.2.0", 27 | "expo-secure-store": "^11.2.0", 28 | "expo-splash-screen": "^0.15.1", 29 | "expo-status-bar": "~1.3.0", 30 | "expo-web-browser": "^10.2.1", 31 | "react": "17.0.2", 32 | "react-dom": "17.0.2", 33 | "react-native": "0.68.2", 34 | "react-native-safe-area-context": "4.2.4", 35 | "react-query": "^3.39.1", 36 | "zustand": "^4.0.0-rc.3" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.12.9", 40 | "@types/react": "~17.0.21", 41 | "@types/react-native": "~0.67.6" 42 | }, 43 | "private": true 44 | } 45 | -------------------------------------------------------------------------------- /apps/mobile/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Provider } from './components'; 3 | import { useAppReady } from './hooks'; 4 | import { LoginScreen } from './screens'; 5 | import { useStore } from './store'; 6 | import './utils/ignore-warnings'; 7 | 8 | const App: FC = () => { 9 | const { isAppReady } = useStore(); 10 | useAppReady(); 11 | 12 | if (!isAppReady) { 13 | return null; 14 | } 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /apps/mobile/src/components/LoginOptions.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dimensions, 3 | Text, 4 | TouchableOpacity, 5 | View, 6 | ViewStyle, 7 | Platform, 8 | Alert, 9 | } from 'react-native'; 10 | import { useAuth } from '../hooks'; 11 | import MaterialIcon from '@expo/vector-icons/MaterialCommunityIcons'; 12 | import { FC } from 'react'; 13 | import * as Haptics from 'expo-haptics'; 14 | import * as AppleAuthentication from 'expo-apple-authentication'; 15 | 16 | export const LoginOptions = () => { 17 | const { googleSignIn, githubSignIn } = useAuth(); 18 | 19 | return ( 20 | <> 21 | 26 | 27 | 32 | 33 | {Platform.OS === 'ios' && } 34 | 35 | ); 36 | }; 37 | 38 | const BUTTON_ROOT: ViewStyle = { 39 | borderWidth: 1, 40 | borderColor: '#00000050', 41 | borderRadius: 5, 42 | }; 43 | 44 | const INNER_BOUTTON: ViewStyle = { 45 | height: 50, 46 | width: Dimensions.get('screen').width - 24, 47 | maxWidth: 350, 48 | justifyContent: 'center', 49 | alignItems: 'center', 50 | flexDirection: 'row', 51 | }; 52 | 53 | interface OAuthLoginButtonProps { 54 | onPress(): void; 55 | disabled: boolean; 56 | provider: 'google' | 'github' | 'apple'; 57 | } 58 | 59 | const OAuthLoginButton: FC = ({ 60 | onPress, 61 | disabled, 62 | provider, 63 | }) => { 64 | function handleOnPress() { 65 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); 66 | onPress(); 67 | } 68 | return ( 69 | 70 | 75 | 76 | 77 | 78 | Continue with{' '} 79 | {provider} 80 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | const AppleLoginButton = () => { 87 | // const { appleSignIn } = useAuth(); 88 | 89 | function signIn() { 90 | Alert.alert('NOT IMPLEMENTED'); 91 | return; 92 | } 93 | return ( 94 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /apps/mobile/src/components/Provider.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'expo-status-bar'; 2 | import { FC, useState } from 'react'; 3 | import { 4 | initialWindowMetrics, 5 | SafeAreaProvider, 6 | } from 'react-native-safe-area-context'; 7 | import { QueryClient, QueryClientProvider } from 'react-query'; 8 | import { useStore } from '../store'; 9 | import { getJWT } from '../utils/secure-store'; 10 | import { transformer, trpc } from '../utils/trpc'; 11 | 12 | export const Provider: FC = ({ children }) => { 13 | const { token, setToken } = useStore(); 14 | const [queryClient] = useState(() => new QueryClient()); 15 | const [trpcClient] = useState(() => 16 | trpc.createClient({ 17 | url: `${process.env.NEXT_API_URL}/api/trpc`, 18 | async headers() { 19 | if (token) { 20 | return { 21 | Authorization: token, 22 | }; 23 | } 24 | try { 25 | const localToken = await getJWT(); 26 | if (localToken) { 27 | setToken(localToken); 28 | return { 29 | Authorization: localToken, 30 | }; 31 | } 32 | 33 | return { 34 | Authorization: '', 35 | }; 36 | } catch (err) { 37 | console.log({ CREATE_TRPC_CLIENT_HEADER: err }); 38 | return { 39 | Authorization: '', 40 | }; 41 | } 42 | }, 43 | transformer, 44 | }) 45 | ); 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | {children} 53 | 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /apps/mobile/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Provider'; 2 | export * from './LoginOptions'; 3 | -------------------------------------------------------------------------------- /apps/mobile/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAuth'; 2 | export * from './useAppReady'; 3 | -------------------------------------------------------------------------------- /apps/mobile/src/hooks/useAppReady.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Poppins_400Regular, 3 | Poppins_500Medium, 4 | Poppins_600SemiBold, 5 | Poppins_700Bold, 6 | Poppins_800ExtraBold, 7 | useFonts, 8 | } from '@expo-google-fonts/poppins'; 9 | import * as SplashScreen from 'expo-splash-screen'; 10 | import { useEffect } from 'react'; 11 | import { useStore } from '../store'; 12 | 13 | SplashScreen.preventAutoHideAsync(); 14 | 15 | export const useAppReady = () => { 16 | const { setIsAppReady, isAppReady } = useStore(); 17 | const customFonts = useFonts({ 18 | poppins: Poppins_400Regular, 19 | poppins500: Poppins_500Medium, 20 | poppins600: Poppins_600SemiBold, 21 | poppins700: Poppins_700Bold, 22 | poppins800: Poppins_800ExtraBold, 23 | }); 24 | 25 | useEffect(() => { 26 | if (customFonts[1]) { 27 | console.error({ fontsLoadingError: customFonts[1] }); 28 | } 29 | if (customFonts[0] && !isAppReady) { 30 | setIsAppReady(true); 31 | SplashScreen.hideAsync(); 32 | } 33 | }, [customFonts]); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/mobile/src/hooks/useAuth.tsx: -------------------------------------------------------------------------------- 1 | import * as AppleAuthentication from 'expo-apple-authentication'; 2 | import { 3 | AuthSessionResult, 4 | makeRedirectUri, 5 | useAuthRequest, 6 | } from 'expo-auth-session'; 7 | import * as Google from 'expo-auth-session/providers/google'; 8 | import { useEffect } from 'react'; 9 | import { Alert } from 'react-native'; 10 | import { useStore } from '../store'; 11 | import { clearToken, saveJWT } from '../utils/secure-store'; 12 | import { inferMutationInput, trpc } from '../utils/trpc'; 13 | 14 | const redirectToExpoAppUri = makeRedirectUri({ 15 | useProxy: true, 16 | }); 17 | 18 | type SignInResponseInput = inferMutationInput<'expo-auth.signIn'>['response']; 19 | type SignInProvider = inferMutationInput<'expo-auth.signIn'>['provider']; 20 | 21 | export const useAuth = () => { 22 | const { setSession, setToken, setLoadingSession } = useStore(); 23 | 24 | const googleSignIn = useGoogleAuth(); 25 | const githubSignIn = useGithubAuth(); 26 | const appleSignIn = useAppleAuth(); 27 | 28 | const utils = trpc.useContext(); 29 | trpc.useQuery(['expo-auth.getSession'], { 30 | onSuccess(data) { 31 | setSession(data); 32 | }, 33 | onError(err) { 34 | if (err.message === 'Token expired') { 35 | console.log(err); 36 | signOut(); 37 | } 38 | }, 39 | }); 40 | 41 | async function signOut() { 42 | setLoadingSession(true); 43 | try { 44 | await clearToken(); 45 | utils.queryClient.resetQueries(); // or utils.invalidateQueries() 46 | setSession(null); 47 | setToken(null); 48 | } catch (err) { 49 | console.log({ SIGN_OUT_ERROR: err }); 50 | } finally { 51 | setLoadingSession(false); 52 | } 53 | } 54 | 55 | return { 56 | googleSignIn, 57 | githubSignIn, 58 | appleSignIn, 59 | signOut, 60 | }; 61 | }; 62 | 63 | const useGoogleAuth = () => { 64 | const { signIn } = useSignIn(); 65 | const [request, response, promptAsync] = Google.useAuthRequest({ 66 | expoClientId: process.env.GOOGLE_CLIENT_ID, 67 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 68 | scopes: ['openid', 'profile', 'email'], 69 | redirectUri: redirectToExpoAppUri, 70 | }); 71 | 72 | useEffect(() => { 73 | if (response?.type === 'cancel') { 74 | console.log('useGoogleAuth', 'CANCEL'); 75 | } 76 | if (response?.type === 'error') { 77 | console.log('useGoogleAuth', 'ERROR'); 78 | } 79 | if (response?.type === 'locked') { 80 | console.log('useGoogleAuth', 'LOCKED'); 81 | } 82 | 83 | if (response?.type === 'success') { 84 | signIn(response, 'google'); 85 | } 86 | }, [response]); 87 | 88 | return { isDisabled: !request, promptAsync: () => promptAsync() }; 89 | }; 90 | 91 | const GITHUB_DISCOVERY = { 92 | authorizationEndpoint: 'https://github.com/login/oauth/authorize', 93 | tokenEndpoint: 'https://github.com/login/oauth/access_token', 94 | revocationEndpoint: `https://github.com/settings/connections/applications/${process.env.GITHUB_ID}`, 95 | }; 96 | 97 | const useGithubAuth = () => { 98 | const { signIn } = useSignIn(); 99 | const [request, response, promptAsync] = useAuthRequest( 100 | { 101 | clientId: process.env.GITHUB_ID, 102 | scopes: ['read:user', 'user:email'], 103 | redirectUri: redirectToExpoAppUri, 104 | }, 105 | GITHUB_DISCOVERY 106 | ); 107 | 108 | useEffect(() => { 109 | if (response?.type === 'success') { 110 | signIn(response, 'github'); 111 | } 112 | }, [response]); 113 | 114 | return { 115 | isDisabled: !request, 116 | promptAsync: () => promptAsync({ useProxy: true }), 117 | }; 118 | }; 119 | 120 | const useAppleAuth = () => { 121 | const { signIn } = useSignIn(); 122 | 123 | async function promptAsync() { 124 | try { 125 | // type AppleAuthenticationCredential 126 | const credential: AppleAuthenticationCredential = 127 | await AppleAuthentication.signInAsync({ 128 | requestedScopes: [ 129 | AppleAuthentication.AppleAuthenticationScope.FULL_NAME, 130 | AppleAuthentication.AppleAuthenticationScope.EMAIL, 131 | ], 132 | }); 133 | 134 | signIn( 135 | { 136 | type: 'success', 137 | authentication: { accessToken: credential.authorizationCode }, 138 | params: credential, 139 | }, 140 | 'apple' 141 | ); 142 | } catch (err) { 143 | console.log('useAppleAuth', err); 144 | } 145 | } 146 | 147 | return { 148 | isDisabled: false, 149 | promptAsync, 150 | }; 151 | }; 152 | 153 | type AppleAuthenticationCredential = { 154 | user: string; 155 | state: string | null; 156 | fullName: AppleAuthentication.AppleAuthenticationFullName | null; 157 | email: string | null; 158 | realUserStatus: AppleAuthentication.AppleAuthenticationUserDetectionStatus; 159 | identityToken: string | null; 160 | authorizationCode: string | null; 161 | }; 162 | 163 | interface AppleAuthSessionResult { 164 | type: 'success' | 'error'; 165 | authentication: { accessToken: string | null }; 166 | params: AppleAuthentication.AppleAuthenticationCredential; 167 | } 168 | 169 | const useSignIn = () => { 170 | const { setLoadingSession, setSession, setToken } = useStore(); 171 | 172 | const signInMutation = trpc.useMutation(['expo-auth.signIn'], { 173 | onError(error, variables, context) { 174 | if (error.message === 'Failed to authenticate') { 175 | console.log({ 176 | HANDLE_THIS_AUTH_ERROR: error, 177 | variables, 178 | context, 179 | }); 180 | } else { 181 | console.log({ 182 | HANDLE_ALL_OTHER_AUTH_ERRORS: error, 183 | variables, 184 | context, 185 | }); 186 | } 187 | }, 188 | }); 189 | 190 | async function signIn( 191 | response: AuthSessionResult | AppleAuthSessionResult, 192 | provider: SignInProvider 193 | ) { 194 | setLoadingSession(true); 195 | try { 196 | const result = await signInMutation.mutateAsync({ 197 | response: response as SignInResponseInput, 198 | provider, 199 | }); 200 | if (!result?.jwt) { 201 | Alert.alert('ERROR', 'Unable to login at this time.'); 202 | return; 203 | } 204 | setSession(result?.currentUser); 205 | setToken(result?.jwt); 206 | saveJWT(result?.jwt); 207 | } catch (err) { 208 | console.error('Error: useSignIn', err); 209 | } finally { 210 | setLoadingSession(false); 211 | } 212 | } 213 | 214 | return { signIn }; 215 | }; 216 | -------------------------------------------------------------------------------- /apps/mobile/src/screens/LoginScreen.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from 'react'; 2 | import { Button, Image, Text, View, ViewStyle } from 'react-native'; 3 | import { LoginOptions } from '../components'; 4 | import { useAuth } from '../hooks'; 5 | import { useStore } from '../store'; 6 | import { trpc } from '../utils/trpc'; 7 | import NetInfo from '@react-native-community/netinfo'; 8 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 9 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 10 | 11 | const CENTER_CENTER: ViewStyle = { 12 | alignItems: 'center', 13 | justifyContent: 'center', 14 | }; 15 | 16 | const ROOT: ViewStyle = { 17 | flex: 1, 18 | backgroundColor: '#fff', 19 | ...CENTER_CENTER, 20 | }; 21 | 22 | export const LoginScreen: FC = () => { 23 | const { bottom } = useSafeAreaInsets(); 24 | const { 25 | session, 26 | loadingSession, 27 | hasInternetConnection, 28 | setHasInternetConnection, 29 | } = useStore(); 30 | const hello = trpc.useQuery(['example.hello', { text: 'from tRPC' }]); 31 | const { signOut } = useAuth(); 32 | 33 | useEffect(() => { 34 | const unsubscribe = NetInfo.addEventListener((state) => { 35 | setHasInternetConnection(Boolean(state.isConnected)); 36 | }); 37 | 38 | return () => { 39 | unsubscribe(); 40 | }; 41 | }, []); 42 | 43 | if (loadingSession) 44 | return ( 45 | 46 | Loading session... 47 | 48 | ); 49 | 50 | return ( 51 | 52 | 53 | 54 | 59 | 65 | Turbo-t3-expo 66 | 67 | 68 | {!hello.data ? 'Loading tRPC query...' : hello.data.greeting} 69 | 70 | 71 | 72 | {!session ? ( 73 | 74 | ) : ( 75 | 76 | Successfully authenticated! 77 | Name: {session.name} 78 | Email: {session.email} 79 | ID: {session.id} 80 | 81 | 82 | 87 | 88 | 93 | 27 | ) : ( 28 | 29 | )} 30 |

This stack uses

31 |
32 |
33 |

NextJS

34 |

35 | The React framework for production 36 |

37 | 43 | Documentation 44 | 45 |
46 |
47 |

TypeScript

48 |

49 | Strongly typed programming language that builds on JavaScript, 50 | giving you better tooling at any scale 51 |

52 | 58 | Documentation 59 | 60 |
61 |
62 |

TailwindCSS

63 |

64 | Rapidly build modern websites without ever leaving your HTML 65 |

66 | 72 | Documentation 73 | 74 |
75 |
76 |

tRPC

77 |

78 | End-to-end typesafe APIs made easy 79 |

80 | 86 | Documentation 87 | 88 |
89 |
90 |
91 | {hello.data ?

{hello.data.greeting}

:

Loading..

} 92 |
93 | ; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/ui-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = "file:./db.sqlite" 11 | // url = env("DATABASE_URL") 12 | } 13 | 14 | model Example { 15 | id String @id @default(cuid()) 16 | } 17 | 18 | // Necessary for Next auth 19 | model Account { 20 | id String @id @default(cuid()) 21 | userId String 22 | type String 23 | provider String 24 | providerAccountId String 25 | refresh_token String? 26 | access_token String? 27 | expires_at Int? 28 | token_type String? 29 | scope String? 30 | id_token String? 31 | session_state String? 32 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 33 | 34 | @@unique([provider, providerAccountId]) 35 | } 36 | 37 | model Session { 38 | id String @id @default(cuid()) 39 | sessionToken String @unique 40 | userId String 41 | expires DateTime 42 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 43 | } 44 | 45 | model User { 46 | id String @id @default(cuid()) 47 | name String? 48 | email String? @unique 49 | emailVerified DateTime? 50 | image String? 51 | accounts Account[] 52 | sessions Session[] 53 | } 54 | 55 | model VerificationToken { 56 | identifier String 57 | token String @unique 58 | expires DateTime 59 | 60 | @@unique([identifier, token]) 61 | } 62 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "[1/2] Deleting all node_modules..." 3 | find . -type d -name "node_modules" -exec rm -rf '{}' + 4 | 5 | echo "[2/2] Deleting yarn.lock..." 6 | rm -rf yarn.lock 7 | 8 | echo "Cleanup Done! ✅" 9 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline": { 3 | "build": { 4 | "dependsOn": ["^build"], 5 | "outputs": ["dist/**", ".next/**"] 6 | }, 7 | "lint": { 8 | "outputs": [] 9 | }, 10 | "dev": { 11 | "cache": false 12 | } 13 | } 14 | } 15 | --------------------------------------------------------------------------------