├── vercel.json ├── config ├── locales │ └── en.json ├── config.ts └── globalStyles.ts ├── docs ├── demo.jpg └── github_preview.jpg ├── public ├── favicon.ico ├── favicon.png ├── app-icon.png ├── app-splash.png ├── app-icon-adaptive.png └── manifest.json ├── babel.config.js ├── next-env.d.ts ├── tsconfig.json ├── .github ├── pull_request_template.md └── workflows │ ├── main.yml │ ├── production.yml │ ├── production-android.yml │ └── pull_request.yml ├── pages ├── _app.tsx ├── page2.tsx ├── api │ └── test.ts ├── _document.tsx └── index.tsx ├── types └── global.d.ts ├── metro.config.js ├── components ├── webElements.tsx ├── VideoPlayer.tsx └── page │ ├── Page.tsx │ └── PageHead.tsx ├── .gitignore ├── App.tsx ├── LICENSE.md ├── eas.json ├── next.config.js ├── hooks ├── useUniqueDeviceID.ts ├── useI18N.ts └── useAnalytics.tsx ├── lib ├── handleRestRequest.ts └── makeRestRequest.ts ├── app.json ├── package.json └── README.md /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "nextjs" 3 | } 4 | -------------------------------------------------------------------------------- /config/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "CANCEL": "Cancel" 3 | } 4 | -------------------------------------------------------------------------------- /docs/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsoderlund/reactnative-nextjs-template/HEAD/docs/demo.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsoderlund/reactnative-nextjs-template/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsoderlund/reactnative-nextjs-template/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsoderlund/reactnative-nextjs-template/HEAD/public/app-icon.png -------------------------------------------------------------------------------- /public/app-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsoderlund/reactnative-nextjs-template/HEAD/public/app-splash.png -------------------------------------------------------------------------------- /docs/github_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsoderlund/reactnative-nextjs-template/HEAD/docs/github_preview.jpg -------------------------------------------------------------------------------- /public/app-icon-adaptive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsoderlund/reactnative-nextjs-template/HEAD/public/app-icon-adaptive.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "incremental": true, 7 | "module": "esnext", 8 | "isolatedModules": true, 9 | "jsx": "preserve" 10 | }, 11 | "include": [ 12 | "next-env.d.ts", 13 | "**/*.ts", 14 | "**/*.tsx" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Related ticket:** https://trello.com/c/... 2 | 3 | ### What this PR includes 4 | 5 | - [e.g. Added a new screen that does...] 6 | - [Mark PR as 🚧 DRAFT if you don’t want it to be merged] 7 | 8 | ### Checklist 9 | 10 | - [ ] I have compared design with Figma sketches 11 | - [ ] I have run `yarn fix` and solved any linting issues 12 | - [ ] There are no merge conflicts -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | 3 | import Page from '../components/page/Page' 4 | 5 | function MyApp ({ Component, pageProps, router }: AppProps): React.ReactElement { 6 | // props (Server + Client): Component, err, pageProps, router 7 | return ( 8 | 11 | 15 | 16 | ) 17 | } 18 | export default MyApp 19 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | // ----- Modules ----- 2 | 3 | declare module '*.png' 4 | declare module '*.jpg' 5 | 6 | declare module '*.svg' { 7 | import React from 'react' 8 | import { SvgProps } from 'react-native-svg' 9 | const content: React.FC 10 | export default content 11 | } 12 | 13 | // ----- Other data types ----- 14 | 15 | interface PageMetaProps { 16 | title?: string 17 | description?: string 18 | imageUrl?: string 19 | iconUrl?: string 20 | path?: string 21 | } 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Native Next.js", 3 | "short_name": "RN-N", 4 | "description": "Build native apps (iOS/Android/Windows/macOS) and an SEO-optimized web app from the same React codebase", 5 | "theme_color": "#663399", 6 | "background_color": "#663399", 7 | "icons": [ 8 | { 9 | "src": "favicon.png", 10 | "sizes": "512x512", 11 | "type": "image/png" 12 | } 13 | ], 14 | "display": "standalone", 15 | "orientation": "portrait", 16 | "scope": "/", 17 | "start_url": "/" 18 | } -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config') 2 | module.exports = (() => { 3 | const config = getDefaultConfig(__dirname) 4 | const { transformer, resolver } = config 5 | 6 | config.transformer = { 7 | ...transformer, 8 | babelTransformerPath: require.resolve('react-native-svg-transformer') 9 | } 10 | 11 | config.resolver = { 12 | ...resolver, 13 | assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'), 14 | sourceExts: [...resolver.sourceExts, 'svg'] 15 | // extraNodeModules: { 16 | // crypto: require('react-native-crypto') 17 | // } 18 | } 19 | 20 | return config 21 | })() 22 | -------------------------------------------------------------------------------- /components/webElements.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Text } from 'react-native' 3 | 4 | export const H1 = (props: any): React.ReactElement => 5 | export const H2 = (props: any): React.ReactElement => 6 | export const H3 = (props: any): React.ReactElement => 7 | 8 | export const Heading = (props: any): React.ReactElement => { 9 | const { children, level, ...otherProps } = props 10 | return ( 11 | 16 | {children} 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | node_modules/**/* 3 | .expo/* 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | web-report/ 13 | .next/* 14 | 15 | # dependencies 16 | /node_modules 17 | /.pnp 18 | .pnp.js 19 | 20 | # testing 21 | /coverage 22 | 23 | # next.js 24 | /.next/ 25 | /out/ 26 | 27 | # production 28 | /build 29 | 30 | # misc 31 | .DS_Store 32 | *.pem 33 | 34 | # debug 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | 39 | # local env files 40 | .env.local 41 | .env.development.local 42 | .env.test.local 43 | .env.production.local 44 | 45 | # vercel 46 | .vercel 47 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native' 2 | import { ToastProvider } from 'react-native-toast-notifications' 3 | 4 | import { GLOBAL_STYLES } from './config/globalStyles' 5 | // import { AnalyticsContextProvider } from './hooks/useAnalytics' 6 | // import Page from './components/page/Page' 7 | import IndexPage from './pages/index' 8 | 9 | export default function App (): React.ReactElement { 10 | return ( 11 | 16 | 17 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Tom Söderlund 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | 7 | Source: http://opensource.org/licenses/ISC 8 | -------------------------------------------------------------------------------- /pages/page2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * JustAWebPage – attempt at making a “regular” page page and still embed React Native components. 3 | */ 4 | 5 | import React from 'react' 6 | 7 | import { config } from '../config/config' 8 | 9 | import PageHead from '../components/page/PageHead' 10 | 11 | export default function JustAWebPage ({ title = config.appName, description }: PageMetaProps): React.ReactElement { 12 | return ( 13 | <> 14 | 18 |
Web page
19 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /pages/api/test.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | import { handleRestRequest, CustomError } from '../../lib/handleRestRequest' 4 | 5 | export default async (req: NextApiRequest, res: NextApiResponse): Promise => await handleRestRequest(async (req, res) => { 6 | switch (req.method) { 7 | case 'GET': 8 | await getTestResource(req, res) 9 | break 10 | default: 11 | throw new CustomError('Method not allowed', 405) 12 | } 13 | }, { req, res }) 14 | 15 | const formatTime = (dateObj = new Date()): string => `${`0${dateObj.getHours()}`.slice(-2)}:${`0${dateObj.getMinutes()}`.slice(-2)}` 16 | 17 | const getTestResource = async (req: NextApiRequest, res: NextApiResponse): Promise => { 18 | // const data = req.body 19 | // Return results 20 | res.statusCode = 200 21 | res.json({ message: `Hello from API at ${formatTime()}` }) 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Commit to `main` branch → Publish to Expo channel “staging” 2 | 3 | name: main → Expo staging 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 🏗 Setup repository 15 | uses: actions/checkout@v3 16 | 17 | - name: 🏗 Setup Node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.x 21 | cache: yarn 22 | 23 | - name: 🏗 Setup Expo 24 | uses: expo/expo-github-action@v8 25 | with: 26 | expo-version: latest 27 | token: ${{ secrets.EXPO_TOKEN }} 28 | 29 | - name: 📦 Install dependencies 30 | run: yarn install 31 | 32 | - name: 🚀 Publish app to Expo/EAS 33 | uses: expo/expo-github-action/preview@v8 34 | with: 35 | command: eas update --auto 36 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 5.2.0", 4 | "promptToConfigurePushNotifications": false, 5 | "requireCommit": true 6 | }, 7 | "build": { 8 | "development": { 9 | "channel": "development", 10 | "distribution": "internal", 11 | "developmentClient": true, 12 | "ios": { 13 | "resourceClass": "m-medium" 14 | } 15 | }, 16 | "preview": { 17 | "channel": "preview", 18 | "distribution": "internal", 19 | "ios": { 20 | "resourceClass": "m-medium" 21 | } 22 | }, 23 | "production": { 24 | "channel": "production", 25 | "ios": { 26 | "resourceClass": "m-medium" 27 | } 28 | } 29 | }, 30 | "submit": { 31 | "production": { 32 | "ios": { 33 | "appleId": "my@email.com", 34 | "appleTeamId": "[hex team id]", 35 | "ascAppId": "[Numeric “Apple ID” on App Store Connect]" 36 | }, 37 | "android": { 38 | "track": "internal", 39 | "releaseStatus": "draft", 40 | "serviceAccountKeyPath": "./appstores/googleplay/pc-api-XXXXX.json" 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { withExpo } = require('@expo/next-adapter') 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | i18n: { 6 | locales: ['en'], 7 | defaultLocale: 'en' 8 | }, 9 | typescript: { 10 | // TODO: Warning: This allows production builds to successfully complete even if your project has TypeScript errors 11 | ignoreBuildErrors: true 12 | }, 13 | reactStrictMode: true, 14 | swcMinify: true, 15 | transpilePackages: [ 16 | 'react-native', 17 | 'expo', 18 | // Add more React Native / Expo packages here... 19 | 'react-native-elements', 20 | 'react-native-safe-area-context', 21 | 'react-native-vector-icons' 22 | ], 23 | experimental: { 24 | forceSwcTransforms: true 25 | } 26 | // For font support: 27 | // webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { 28 | // config.module.rules.push({ 29 | // test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/, 30 | // loader: 'file-loader?name=assets/[name].[hash].[ext]' 31 | // }) 32 | // return config 33 | // } 34 | } 35 | 36 | module.exports = withExpo(nextConfig) 37 | -------------------------------------------------------------------------------- /hooks/useUniqueDeviceID.ts: -------------------------------------------------------------------------------- 1 | /* 2 | From: https://stackoverflow.com/a/68553627/449227 3 | 4 | import useUniqueDeviceID from '../../hooks/useUniqueDeviceID' 5 | const uniqueDeviceID = useUniqueDeviceID() 6 | */ 7 | 8 | import { useState, useEffect } from 'react' 9 | import * as SecureStore from 'expo-secure-store' 10 | import 'react-native-get-random-values' 11 | import { v4 as uuidv4 } from 'uuid' 12 | 13 | const STORAGE_KEY_UNIQUE_DEVICE_ID = 'uniqueDeviceID' 14 | 15 | export default function useUniqueDeviceID (): string | undefined { 16 | const [uniqueDeviceID, setUniqueDeviceID] = useState() 17 | 18 | useEffect(() => { 19 | async function getUniqueDeviceID (): Promise { 20 | const fetchUUID = await SecureStore.getItemAsync(STORAGE_KEY_UNIQUE_DEVICE_ID) 21 | if (fetchUUID !== null) { 22 | setUniqueDeviceID(JSON.parse(fetchUUID)) 23 | } else { 24 | const newUUID = uuidv4() 25 | await SecureStore.setItemAsync(STORAGE_KEY_UNIQUE_DEVICE_ID, JSON.stringify(newUUID)) 26 | setUniqueDeviceID(newUUID) 27 | } 28 | } 29 | void getUniqueDeviceID() 30 | }, []) 31 | 32 | return uniqueDeviceID 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | # Commit to `production` branch → build with EAS 2 | 3 | name: production → build iOS app 4 | 5 | on: 6 | push: 7 | branches: 8 | - production 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 🏗 Setup repository 15 | uses: actions/checkout@v3 16 | 17 | - name: 🏗 Setup Node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.x 21 | cache: yarn 22 | 23 | - name: 🏗 Setup Expo and EAS 24 | uses: expo/expo-github-action@v8 25 | with: 26 | expo-version: latest 27 | eas-version: latest 28 | token: ${{ secrets.EXPO_TOKEN }} 29 | 30 | - name: 📦 Install dependencies 31 | run: yarn install 32 | 33 | - name: 🚀 Publish app to Expo/EAS 34 | uses: expo/expo-github-action/preview@v8 35 | with: 36 | command: eas update --auto 37 | 38 | - name: 🛠️ Build app 39 | run: eas build --non-interactive --profile production --platform ios 40 | 41 | - name: 🚚 Submit app to TestFlight 42 | run: eas submit --latest --platform ios 43 | -------------------------------------------------------------------------------- /components/VideoPlayer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react' 2 | import { StyleSheet, View, Button } from 'react-native' 3 | import { Video } from 'expo-av' 4 | 5 | interface VideoPlayerProps { 6 | videoUrl: string 7 | } 8 | 9 | const VideoPlayer = ({ videoUrl }: VideoPlayerProps): React.ReactElement => { 10 | const video = useRef(null) 11 | const [status, setStatus] = useState({}) 12 | return ( 13 | <> 14 |