Skip to content

Commit

Permalink
feat: add react native web support (#141)
Browse files Browse the repository at this point in the history
Co-authored-by: Jakob Löw <[email protected]>
  • Loading branch information
Robert27 and M4GNV5 authored Dec 15, 2024
1 parent 4e543c1 commit c688528
Show file tree
Hide file tree
Showing 93 changed files with 1,708 additions and 638 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
ios
android
dist
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ name: 'CodeQL'

on:
push:
branches: ['main']
branches: ['main', 'develop']
pull_request:
branches: ['main']
branches: ['main', 'develop']
schedule:
- cron: '19 20 * * 3'

Expand Down
54 changes: 54 additions & 0 deletions .github/workflows/deploy-webapp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Create and publish a Docker image

# Configures this workflow to run every time a change is pushed to the branch called `release`.
on:
push:
branches: ['main', 'develop']

# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
id-token: write
contents: read
attestations: write
packages: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/[email protected]
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
EXPO_PUBLIC_THI_API_KEY=${{ secrets.EXPO_PUBLIC_THI_API_KEY }}
EXPO_PUBLIC_NEULAND_GRAPHQL_ENDPOINT=${{ github.ref == 'refs/heads/develop' && vars.GRAPHQL_ENDPOINT_DEV || vars.GRAPHQL_ENDPOINT_PROD }}
EXPO_PUBLIC_APTABASE_KEY=${{ secrets.EXPO_PUBLIC_APTABASE_KEY }}
43 changes: 43 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Base stage: Install dependencies and build the project
FROM oven/bun:1 AS bun
WORKDIR /usr/src/app

# Copy necessary files and install dependencies
COPY package.json bun.lockb ./
COPY patches patches
RUN bun install --ignore-scripts --freeze-lockfile

# Build stage: Use a Node.js image for the build process
FROM node:23 AS build
WORKDIR /usr/src/app

# Copy dependencies and project files
COPY --from=bun /usr/src/app/node_modules ./node_modules
COPY . .

# Build the project
ARG EXPO_PUBLIC_THI_API_KEY
ARG EXPO_PUBLIC_NEULAND_GRAPHQL_ENDPOINT
ARG EXPO_PUBLIC_APTABASE_KEY
ENV EXPO_PUBLIC_THI_API_KEY=${EXPO_PUBLIC_THI_API_KEY}
ENV EXPO_PUBLIC_NEULAND_GRAPHQL_ENDPOINT=${EXPO_PUBLIC_NEULAND_GRAPHQL_ENDPOINT}
ENV EXPO_PUBLIC_APTABASE_KEY=${EXPO_PUBLIC_APTABASE_KEY}
ENV NODE_ENV=production

RUN npx expo export -p web -c

# Final stage: Serve static files using npx serve
FROM node:23 AS final
WORKDIR /usr/src/app

# Copy the build files
COPY --from=build /usr/src/app/dist ./dist

# Install serve
RUN npm install -g serve

# Expose the port
EXPOSE 3000

# Serve the static files
CMD ["serve", "-s", "dist", "-l", "3000"]
8 changes: 8 additions & 0 deletions app.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@
"eas": {
"projectId": "b0ef9e3f-3115-44b0-abc7-99dd75821353"
}
},
"web": {
"favicon": "./src/assets/web/favicon.png",
"shortName": "Neuland Next",
"name": "Neuland Next - Deine inoffizielle App für die THI",
"description": "Neuland Next ist eine inoffizielle App für die Technische Hochschule Ingolstadt. Sie bietet dir alle Funktionen, die du für dein Studium benötigst, an einem Ort.",
"lang": "de",
"preferRelatedApplications": true
}
}
}
14 changes: 13 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ module.exports = function (api) {
api.cache(true)
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
plugins: [
['react-native-reanimated/plugin'],
[
'transform-inline-environment-variables',
{
include: [
'EXPO_PUBLIC_THI_API_KEY',
'EXPO_PUBLIC_NEULAND_GRAPHQL_ENDPOINT',
'EXPO_PUBLIC_APTABASE_KEY',
],
},
],
],
}
}
Binary file modified bun.lockb
Binary file not shown.
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"start": "EXPO_USE_FAST_RESOLVER=1 expo",
"atlas": "EXPO_UNSTABLE_ATLAS=true npx expo export --platform all",
"web": "expo start --web --port 3000",
"android": "expo run:android --device",
"ios": "expo run:ios --device",
"build:android": "bun licences && eas build --platform android --local",
Expand All @@ -26,11 +27,12 @@
"@aptabase/react-native": "^0.3.10",
"@babel/runtime": "^7.26.0",
"@bottom-tabs/react-navigation": "^0.7.6",
"@expo/metro-runtime": "^4.0.0",
"@expo/vector-icons": "^14.0.4",
"@gorhom/bottom-sheet": "^5.0.6",
"@howljs/calendar-kit": "^2.2.1",
"@kichiyaki/react-native-barcode-generator": "^0.6.7",
"@maplibre/maplibre-react-native": "^10.0.0-beta.8",
"@maplibre/maplibre-react-native": "^10.0.0-beta.9",
"@react-native-community/datetimepicker": "8.2.0",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/native": "^7.0.14",
Expand Down Expand Up @@ -62,8 +64,9 @@
"expo-splash-screen": "~0.29.18",
"expo-system-ui": "~4.0.6",
"fuse.js": "^7.0.0",
"graphql": "^16.9.0",
"graphql": "^16.10.0",
"i18next": "^24.1.0",
"lucide-react-native": "^0.468.0",
"metro": "^0.81.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",
Expand Down Expand Up @@ -91,6 +94,7 @@
"react-native-svg": "15.8.0",
"react-native-unistyles": "^2.20.0",
"react-native-view-shot": "~4.0.3",
"react-native-web": "^0.19.13",
"react-native-webview": "13.12.5",
"rn-quick-actions": "^0.0.3",
"sanitize-html": "^2.13.1",
Expand All @@ -99,13 +103,14 @@
"zustand": "^5.0.2"
},
"devDependencies": {
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"@babel/core": "^7.26.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/plugin-transform-arrow-functions": "^7.25.9",
"@babel/plugin-transform-shorthand-properties": "^7.25.9",
"@babel/plugin-transform-template-literals": "^7.25.9",
"@commitlint/cli": "^19.6.0",
"@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0",
"@expo/ngrok": "^4.1.3",
"@graphql-codegen/cli": "5.0.3",
Expand Down
5 changes: 4 additions & 1 deletion src/api/neuland-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
UNIVERSITY_SPORTS_QUERY,
} from './gql-documents'

const GRAPHQL_ENDPOINT: string = 'https://api.neuland.app/graphql'
const GRAPHQL_ENDPOINT: string =
process.env.EXPO_PUBLIC_NEULAND_GRAPHQL_ENDPOINT ??
'https://api.neuland.app/graphql'
console.info('Using GraphQL endpoint:', GRAPHQL_ENDPOINT)
const ASSET_ENDPOINT: string = 'https://assets.neuland.app'
const USER_AGENT = `neuland.app-native/${packageInfo.version} (+${packageInfo.homepage})`

Expand Down
65 changes: 32 additions & 33 deletions src/api/thi-session-handler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { storage } from '@/utils/storage'
import * as SecureStore from 'expo-secure-store'
import { deleteSecure, loadSecure, saveSecure, storage } from '@/utils/storage'
import { Platform } from 'react-native'

import API from './anonymous-api'

const SESSION_EXPIRES = 3 * 60 * 60 * 1000

async function save(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value)
}

function load(key: string): string | null {
return SecureStore.getItem(key)
}

/**
* Thrown when the user is not logged in.
*/
Expand Down Expand Up @@ -53,11 +45,12 @@ export async function createSession(
}

storage.set('sessionCreated', Date.now().toString())

await save('session', session)
console.debug('Session created at', storage.getString('sessionCreated'))
await saveSecure('session', session, true)
console.debug('Session saved')
if (stayLoggedIn) {
await save('username', username)
await save('password', password)
await saveSecure('username', username)
await saveSecure('password', password)
}
return isStudent
}
Expand All @@ -69,7 +62,7 @@ export async function createGuestSession(forget = true): Promise<void> {
if (forget) {
await forgetSession()
}
await save('session', 'guest')
await saveSecure('session', 'guest', true)
}

/**
Expand All @@ -84,7 +77,7 @@ export async function createGuestSession(forget = true): Promise<void> {
export async function callWithSession<T>(
method: (session: string) => Promise<T>
): Promise<T> {
let session = load('session')
let session = loadSecure('session')
const sessionCreated = parseInt(storage.getString('sessionCreated') ?? '0')
// redirect user if he never had a session
if (session == null) {
Expand All @@ -93,14 +86,20 @@ export async function callWithSession<T>(
throw new UnavailableSessionError()
}

const username = load('username')
if (username === null) {
throw new UnavailableSessionError()
}
const username = loadSecure('username')
const password = loadSecure('password')
if (Platform.OS === 'web') {
if (session === 'guest' || session == null) {
throw new NoSessionError()
}
} else {
if (username === null) {
throw new UnavailableSessionError()
}

const password = load('password')
if (password === null) {
throw new UnavailableSessionError()
if (password === null) {
throw new UnavailableSessionError()
}
}
// log in if the session is older than SESSION_EXPIRES
if (
Expand All @@ -116,7 +115,7 @@ export async function callWithSession<T>(
)
session = newSession

await save('session', session)
await saveSecure('session', session)
storage.set('sessionCreated', Date.now().toString())
storage.set('isStudent', isStudent.toString())
} catch (e) {
Expand All @@ -143,7 +142,7 @@ export async function callWithSession<T>(
password
)
session = newSession
await save('session', session)
await saveSecure('session', session)
storage.set('sessionCreated', Date.now().toString())
storage.set('isStudent', isStudent.toString())
} catch (e) {
Expand All @@ -169,11 +168,11 @@ export async function callWithSession<T>(
* @param {object} router Next.js router object
*/
export async function obtainSession(router: object): Promise<string | null> {
let session = load('session')
let session = loadSecure('session')
const age = parseInt(storage.getString('sessionCreated') ?? '0')

const username = load('username')
const password = load('password')
const username = loadSecure('username')
const password = loadSecure('password')

// invalidate expired session
if (age + SESSION_EXPIRES < Date.now() || !(await API.isAlive(session))) {
Expand All @@ -191,7 +190,7 @@ export async function obtainSession(router: object): Promise<string | null> {
password
)
session = newSession
await save('session', session)
await saveSecure('session', session)
storage.set('sessionCreated', Date.now().toString())
storage.set('isStudent', isStudent.toString())
} catch (e) {
Expand All @@ -207,7 +206,7 @@ export async function obtainSession(router: object): Promise<string | null> {
* Logs out the user by deleting the session from localStorage.
*/
export async function forgetSession(): Promise<void> {
const session = load('session')
const session = loadSecure('session')
if (session === null) {
console.debug('No session to forget')
} else {
Expand All @@ -219,9 +218,9 @@ export async function forgetSession(): Promise<void> {
}

await Promise.all([
SecureStore.deleteItemAsync('session'),
SecureStore.deleteItemAsync('username'),
SecureStore.deleteItemAsync('password'),
deleteSecure('session'),
deleteSecure('username'),
deleteSecure('password'),
])

// clear the general storage (cache)
Expand Down
Loading

0 comments on commit c688528

Please sign in to comment.