├── 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 |