From dab66be7dcd19cd035eb88732128ba1fb1117c68 Mon Sep 17 00:00:00 2001 From: BellCube <33764825+BellCubeDev@users.noreply.github.com> Date: Sat, 16 Nov 2024 21:43:20 -0500 Subject: [PATCH] Big Fixes + ENV Rework + Asset/CDN prefix + there's still more * Fixed icon & font rendering on native * Code now ensures NEXT_PUBLIC_ and EXPO_PUBLIC_ ENV vars are duplicated across both names * Createed util for getting public ENV vars * Added asset/CDN prefix * Fixed some type errors * Added Dripsy-ified variants of used `@expo/vector-icons` components (also see: expo/vector-icons#317) * Updated Solito fork to use a `'link'` variant by default for the ``'s child `

` component * Renamed `'a'` variant to `'link'` * Updated Solito fork to fix a mistake in Solito's typing --- .env.example | 10 + .gitignore | 4 + docker-compose-setup/.env.example | 41 +++- docker-compose-setup/docker-compose.yaml | 1 + package.json | 5 +- patches/@react-native__assets-registry.patch | 101 ++++++++ patches/next-sitemap.patch | 16 ++ pnpm-lock.yaml | 54 ++++- scripts/build-nextjs-site.ps1 | 3 + src/forks/assets-registry/package.json | 11 - src/forks/assets-registry/path-support.js | 90 ------- src/forks/assets-registry/registry.js | 25 -- src/forks/solito | 2 +- src/interface/components/FontAwesomeIcon.tsx | 43 ---- .../components/icons/dripsy-icons.tsx | 225 ++++++++++++++++++ .../features/getting-started-screen.tsx | 3 +- src/interface/features/home-screen.tsx | 15 +- src/interface/features/login-dialog.tsx | 22 +- src/interface/features/unsupported-screen.tsx | 2 +- src/interface/hooks/useCookies.web.ts | 3 +- src/interface/package.json | 1 + .../provider/auth/authentication.tsx | 2 +- src/interface/provider/tRPC-provider.tsx | 2 +- src/interface/provider/theme.tsx | 2 +- src/lib/env-schema.ts | 24 ++ src/lib/env-utils.d.ts | 17 ++ src/lib/env-utils.js | 58 +++++ src/platforms/expo/app.config.ts | 2 + src/platforms/expo/app/web/_layout.tsx | 6 +- src/platforms/expo/metro.config.js | 15 +- src/platforms/next/app/robots.ts | 17 ++ .../next/app/web/login/LoginPage.tsx | 3 +- src/platforms/next/instrumentation.ts | 101 +++++++- src/platforms/next/next.config.ts | 6 + src/platforms/next/package.json | 2 +- 35 files changed, 714 insertions(+), 220 deletions(-) create mode 100644 patches/@react-native__assets-registry.patch create mode 100644 patches/next-sitemap.patch delete mode 100644 src/forks/assets-registry/package.json delete mode 100644 src/forks/assets-registry/path-support.js delete mode 100644 src/forks/assets-registry/registry.js delete mode 100644 src/interface/components/FontAwesomeIcon.tsx create mode 100644 src/interface/components/icons/dripsy-icons.tsx create mode 100644 src/lib/env-utils.d.ts create mode 100644 src/lib/env-utils.js create mode 100644 src/platforms/next/app/robots.ts diff --git a/.env.example b/.env.example index 3d3698c..280780f 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,16 @@ IS_SAAS = false EXPO_PUBLIC_DEFAULT_SERVER = 'https://10.0.0.128/' NEXT_PUBLIC_BASEURL = 'https://10.0.0.128/' +# OPTIONAL: Prefix for the asset URLs. May be a path or a full URL. +# +# Assets include fonts, images, Next.js scripts, static pages, +# and other things that only change when the site/app is rebuilt. +# +# Examples: +# - "/assets/" +# - "https://cdn.example.com/" +NEXT_PUBLIC_ASSET_PREFIX = 'https://10.0.0.128/' + SITE_PORT=3000 SUPABASE_STUDIO_PORT=4080 diff --git a/.gitignore b/.gitignore index 37c0879..5046d73 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ src/interface/components/icons/material-symbols/.commit-sha docker-compose-setup/supabase_volumes/db/init/* !docker-compose-setup/supabase_volumes/db/init/data.sql +/src/platforms/next/public/robots.txt +/src/platforms/next/public/sitemap*.xml + + # Test Caching! (caching resources used in tests, not caching test results) tests/cache diff --git a/docker-compose-setup/.env.example b/docker-compose-setup/.env.example index f7ffd7b..7a9b87d 100644 --- a/docker-compose-setup/.env.example +++ b/docker-compose-setup/.env.example @@ -90,6 +90,39 @@ DOCKER_SOCKET_LOCATION=/run/user/1000/docker.sock + +# OPTIONAL: Base URL of your web server, used for SEO metadata and the Web App Manifest. +# +# For an example, the default is "https://stockedhome.app/" +NEXT_PUBLIC_BASEURL= + + + +# OPTIONAL: Prefix for the asset URLs. May be a path or a full URL. +# +# Assets include fonts, images, Next.js scripts, static pages, +# and other things that only change when the site/app is rebuilt. +# +# Examples: +# - "/assets/" +# - "https://cdn.example.com/" +NEXT_PUBLIC_ASSET_PREFIX= + + + + + + + + + + + + + + + + ############ # Database - You can change these to any PostgreSQL database that has logical replication enabled. ############ @@ -140,11 +173,3 @@ SUPABASE_PUBLIC_URL=http://localhost:${SUPABASE_STUDIO_PORT} # Google Cloud Project details. Uncomment both in here and in Docker Compose to use. #GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID #GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER - - - - -# OPTIONAL: Base URL of your web server, used for SEO metadata and the Web App Manifest. -# -# For an example, the default is "https://stockedhome.app/" -NEXT_PUBLIC_BASEURL = '' diff --git a/docker-compose-setup/docker-compose.yaml b/docker-compose-setup/docker-compose.yaml index c16d519..4b65459 100644 --- a/docker-compose-setup/docker-compose.yaml +++ b/docker-compose-setup/docker-compose.yaml @@ -38,6 +38,7 @@ services: USE_SAAS_UX: ${USE_SAAS_UX} NODE_ENV: production NEXT_PUBLIC_BASEURL: ${NEXT_PUBLIC_BASEURL:-https://stockedhome.app} + NEXT_PUBLIC_ASSET_PREFIX: ${NEXT_PUBLIC_ASSET_PREFIX:-} studio: diff --git a/package.json b/package.json index 4ee694a..9d64de3 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@gorhom/bottom-sheet": "^4.6.4", "@prisma/client": "^5.21.1", "@react-native-cookies/cookies": "^6.2.1", - "@react-native/assets-registry": "file:./src/forks/assets-registry", "@stockedhome/codegen": "link:codegen/results", "@stockedhome/expo-app": "file:./src/platforms/expo", "@stockedhome/next-app": "file:./src/platforms/next", @@ -114,7 +113,9 @@ "pnpm": { "patchedDependencies": { "@hexagon/base64": "patches/@hexagon__base64.patch", - "react-native-safe-area-context": "patches/react-native-safe-area-context.patch" + "react-native-safe-area-context": "patches/react-native-safe-area-context.patch", + "@react-native/assets-registry": "patches/@react-native__assets-registry.patch", + "next-sitemap": "patches/next-sitemap.patch" }, "overrides": { "next": "^15.0.1", diff --git a/patches/@react-native__assets-registry.patch b/patches/@react-native__assets-registry.patch new file mode 100644 index 0000000..82655ae --- /dev/null +++ b/patches/@react-native__assets-registry.patch @@ -0,0 +1,101 @@ +diff --git a/path-support.js b/path-support.js +index f0a85af33ff98d3d6d8a9c279378eb2676af160a..87f5df3ccd7e2358ac83b8c6005f8ded60d28bf8 100755 +--- a/path-support.js ++++ b/path-support.js +@@ -10,7 +10,7 @@ + + 'use strict'; + +-import type {PackagerAsset} from './registry.js'; ++//import type {PackagerAsset} from './registry.js'; + + const androidScaleSuffix = { + '0.75': 'ldpi', +@@ -27,7 +27,7 @@ const ANDROID_BASE_DENSITY = 160; + * FIXME: using number to represent discrete scale numbers is fragile in essence because of + * floating point numbers imprecision. + */ +-function getAndroidAssetSuffix(scale: number): string { ++function getAndroidAssetSuffix(scale) { + if (scale.toString() in androidScaleSuffix) { + return androidScaleSuffix[scale.toString()]; + } +@@ -52,9 +52,9 @@ const drawableFileTypes = new Set([ + ]); + + function getAndroidResourceFolderName( +- asset: PackagerAsset, +- scale: number, +-): string | $TEMPORARY$string<'raw'> { ++ asset,//: PackagerAsset, ++ scale,//: number, ++) { + if (!drawableFileTypes.has(asset.type)) { + return 'raw'; + } +@@ -72,7 +72,7 @@ function getAndroidResourceFolderName( + return 'drawable-' + suffix; + } + +-function getAndroidResourceIdentifier(asset: PackagerAsset): string { ++function getAndroidResourceIdentifier(asset) { + return (getBasePath(asset) + '/' + asset.name) + .toLowerCase() + .replace(/\//g, '_') // Encode folder structure in file name +@@ -80,7 +80,7 @@ function getAndroidResourceIdentifier(asset: PackagerAsset): string { + .replace(/^assets_/, ''); // Remove "assets_" prefix + } + +-function getBasePath(asset: PackagerAsset): string { ++function getBasePath(asset) { + const basePath = asset.httpServerLocation; + return basePath.startsWith('/') ? basePath.slice(1) : basePath; + } +diff --git a/registry.js b/registry.js +index 02470da3c4962ad1bbdc62d9ed295c19ca4905fe..584c758f2d18f3dd611bcf2614528240346c8747 100755 +--- a/registry.js ++++ b/registry.js +@@ -10,28 +10,28 @@ + + 'use strict'; + +-export type PackagerAsset = { +- +__packager_asset: boolean, +- +fileSystemLocation: string, +- +httpServerLocation: string, +- +width: ?number, +- +height: ?number, +- +scales: Array, +- +hash: string, +- +name: string, +- +type: string, +- ... +-}; ++//export type PackagerAsset = { ++// +__packager_asset: boolean, ++// +fileSystemLocation: string, ++// +httpServerLocation: string, ++// +width: ?number, ++// +height: ?number, ++// +scales: Array, ++// +hash: string, ++// +name: string, ++// +type: string, ++// ... ++//}; + +-const assets: Array = []; ++const assets = []; + +-function registerAsset(asset: PackagerAsset): number { ++function registerAsset(asset) { + // `push` returns new array length, so the first asset will + // get id 1 (not 0) to make the value truthy + return assets.push(asset); + } + +-function getAssetByID(assetId: number): PackagerAsset { ++function getAssetByID(assetId) { + return assets[assetId - 1]; + } + diff --git a/patches/next-sitemap.patch b/patches/next-sitemap.patch new file mode 100644 index 0000000..48289b0 --- /dev/null +++ b/patches/next-sitemap.patch @@ -0,0 +1,16 @@ +diff --git a/package.json b/package.json +index 64d04d7664700d58e7914e8a01ee2a19227fd303..c22ec3aa572b2a36bea0b678679e72414842c60d 100644 +--- a/package.json ++++ b/package.json +@@ -10,6 +10,11 @@ + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "types": "./dist/@types/index.d.ts" ++ }, ++ "./*": { ++ "import": "./dist/esm/*.js", ++ "require": "./dist/cjs/*.js", ++ "types": "./dist/@types/*.d.ts" + } + }, + "files": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b61195c..3b126f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,12 @@ patchedDependencies: '@hexagon/base64': hash: bk3b67xqvbwrbmxnq32kp4kgs4 path: patches/@hexagon__base64.patch + '@react-native/assets-registry': + hash: pqbn5nxwvxzhe4b5dcl3zwyaam + path: patches/@react-native__assets-registry.patch + next-sitemap: + hash: ewhoppzled4v3me6np6nkrah5y + path: patches/next-sitemap.patch react-native-safe-area-context: hash: zbp4gquixe4lcc7ew6wgdqo7cy path: patches/react-native-safe-area-context.patch @@ -75,9 +81,6 @@ importers: '@react-native-cookies/cookies': specifier: ^6.2.1 version: 6.2.1(react-native@0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.3.1)) - '@react-native/assets-registry': - specifier: file:./src/forks/assets-registry - version: link:src/forks/assets-registry '@react-navigation/core': specifier: '*' version: 6.4.17(react@18.3.1) @@ -265,8 +268,6 @@ importers: codegen/results: {} - src/forks/assets-registry: {} - src/forks/react-native-passkeys: dependencies: '@hexagon/base64': @@ -327,6 +328,9 @@ importers: '@hexagon/base64': specifier: ^1.1.28 version: 1.1.28(patch_hash=bk3b67xqvbwrbmxnq32kp4kgs4) + '@stockehome/codegen': + specifier: link:../../codegen/results + version: link:../../codegen/results dripsy: specifier: ^4.3.5 version: 4.3.5 @@ -517,9 +521,6 @@ importers: '@expo/next-adapter': specifier: 6.0.0 version: 6.0.0(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(react-native-web@0.19.13(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(webpack@5.95.0) - '@react-native/assets-registry': - specifier: link:../../forks/assets-registry - version: link:../../forks/assets-registry '@trpc/client': specifier: 11.0.0-rc.413 version: 11.0.0-rc.413(@trpc/server@11.0.0-rc.413) @@ -553,6 +554,9 @@ importers: next-images: specifier: ^1.8.5 version: 1.8.5(webpack@5.95.0) + next-sitemap: + specifier: ^4.2.3 + version: 4.2.3(patch_hash=ewhoppzled4v3me6np6nkrah5y)(next@15.0.1(@babel/core@7.25.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)(sass@1.80.4)) raf: specifier: ^3.4.1 version: 3.4.1 @@ -1527,6 +1531,9 @@ packages: '@changesets/write@0.3.2': resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==} + '@corex/deepmerge@4.0.43': + resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -2382,6 +2389,9 @@ packages: '@motionone/utils@10.18.0': resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==} + '@next/env@13.5.7': + resolution: {integrity: sha512-uVuRqoj28Ys/AI/5gVEgRAISd0KWI0HRjOO1CTpNgmX3ZsHb5mdn14Y59yk0IxizXdo7ZjsI2S7qbWnO+GNBcA==} + '@next/env@15.0.1': resolution: {integrity: sha512-lc4HeDUKO9gxxlM5G2knTRifqhsY6yYpwuHspBZdboZe0Gp+rZHBNNSIjmQKDJIdRXiXGyVnSD6gafrbQPvILQ==} @@ -6844,6 +6854,13 @@ packages: peerDependencies: webpack: ^4.0.0 || ^5.0.0 + next-sitemap@4.2.3: + resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + next: ^15.0.1 + next@15.0.1: resolution: {integrity: sha512-PSkFkr/w7UnFWm+EP8y/QpHrJXMqpZzAXpergB/EqLPOh4SGPJXv1wj4mslr2hUZBAS9pX7/9YLIdxTv6fwytw==} engines: {node: '>=18.18.0'} @@ -11208,6 +11225,8 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@corex/deepmerge@4.0.43': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -12408,6 +12427,8 @@ snapshots: hey-listen: 1.0.8 tslib: 2.8.0 + '@next/env@13.5.7': {} + '@next/env@15.0.1': {} '@next/eslint-plugin-next@14.2.3': @@ -13034,7 +13055,7 @@ snapshots: invariant: 2.2.4 react-native: 0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.3.1) - '@react-native/assets-registry@0.74.87': {} + '@react-native/assets-registry@0.74.87(patch_hash=pqbn5nxwvxzhe4b5dcl3zwyaam)': {} '@react-native/babel-plugin-codegen@0.74.87(@babel/preset-env@7.24.7(@babel/core@7.25.2))': dependencies: @@ -13727,7 +13748,6 @@ snapshots: '@stockedhome/next-app@file:src/platforms/next(kft327xdhmujbepfpv6ec4wrbm)': dependencies: '@expo/next-adapter': 6.0.0(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2)))(react-native-web@0.19.13(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(webpack@5.95.0) - '@react-native/assets-registry': link:../../forks/assets-registry '@trpc/client': 11.0.0-rc.413(@trpc/server@11.0.0-rc.413) '@trpc/next': 11.0.0-rc.413(@tanstack/react-query@5.59.15(react@18.3.1))(@trpc/client@11.0.0-rc.413(@trpc/server@11.0.0-rc.413))(@trpc/react-query@11.0.0-rc.413(@tanstack/react-query@5.59.15(react@18.3.1))(@trpc/client@11.0.0-rc.413(@trpc/server@11.0.0-rc.413))(@trpc/server@11.0.0-rc.413)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/server@11.0.0-rc.413)(next@15.0.1(@babel/core@7.25.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)(sass@1.80.4))(react-dom@18.2.0(react@18.3.1))(react@18.3.1) '@trpc/react-query': 11.0.0-rc.413(@tanstack/react-query@5.59.15(react@18.3.1))(@trpc/client@11.0.0-rc.413(@trpc/server@11.0.0-rc.413))(@trpc/server@11.0.0-rc.413)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) @@ -13739,6 +13759,7 @@ snapshots: next-compose-plugins: 2.2.1 next-fonts: 1.5.1(webpack@5.95.0) next-images: 1.8.5(webpack@5.95.0) + next-sitemap: 4.2.3(patch_hash=ewhoppzled4v3me6np6nkrah5y)(next@15.0.1(@babel/core@7.25.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)(sass@1.80.4)) raf: 3.4.1 sass: 1.80.4 setimmediate: 1.0.5 @@ -14548,6 +14569,7 @@ snapshots: '@expo-google-fonts/rubik': 0.2.3 '@expo/html-elements': 0.10.1 '@hexagon/base64': 1.1.28(patch_hash=bk3b67xqvbwrbmxnq32kp4kgs4) + '@stockehome/codegen': link:../../codegen/results dripsy: 4.3.5 expo-font: 12.0.10(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))) expo-splash-screen: 0.27.6(expo-modules-autolinking@1.11.3)(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))) @@ -18946,6 +18968,14 @@ snapshots: url-loader: 4.1.1(file-loader@6.2.0(webpack@5.95.0))(webpack@5.95.0) webpack: 5.95.0 + next-sitemap@4.2.3(patch_hash=ewhoppzled4v3me6np6nkrah5y)(next@15.0.1(@babel/core@7.25.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)(sass@1.80.4)): + dependencies: + '@corex/deepmerge': 4.0.43 + '@next/env': 13.5.7 + fast-glob: 3.3.2 + minimist: 1.2.8 + next: 15.0.1(@babel/core@7.25.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)(sass@1.80.4) + next@15.0.1(@babel/core@7.25.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)(sass@1.80.4): dependencies: '@next/env': 15.0.1 @@ -19677,7 +19707,7 @@ snapshots: '@react-native-community/cli': 13.6.9 '@react-native-community/cli-platform-android': 13.6.9 '@react-native-community/cli-platform-ios': 13.6.9 - '@react-native/assets-registry': 0.74.87 + '@react-native/assets-registry': 0.74.87(patch_hash=pqbn5nxwvxzhe4b5dcl3zwyaam) '@react-native/codegen': 0.74.87(@babel/preset-env@7.25.4(@babel/core@7.24.7)) '@react-native/community-cli-plugin': 0.74.87(@babel/core@7.24.7)(@babel/preset-env@7.25.4(@babel/core@7.24.7)) '@react-native/gradle-plugin': 0.74.87 @@ -19727,7 +19757,7 @@ snapshots: '@react-native-community/cli': 13.6.9 '@react-native-community/cli-platform-android': 13.6.9 '@react-native-community/cli-platform-ios': 13.6.9 - '@react-native/assets-registry': 0.74.87 + '@react-native/assets-registry': 0.74.87(patch_hash=pqbn5nxwvxzhe4b5dcl3zwyaam) '@react-native/codegen': 0.74.87(@babel/preset-env@7.25.4(@babel/core@7.25.2)) '@react-native/community-cli-plugin': 0.74.87(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2)) '@react-native/gradle-plugin': 0.74.87 diff --git a/scripts/build-nextjs-site.ps1 b/scripts/build-nextjs-site.ps1 index a8f9496..97f8ad6 100644 --- a/scripts/build-nextjs-site.ps1 +++ b/scripts/build-nextjs-site.ps1 @@ -1,6 +1,9 @@ $ErrorActionPreference = "Stop" Set-Location $PSScriptRoot/.. +Write-Host "Building Next.js site" -ForegroundColor DarkCyan +Write-Host "" + pnpm exec next build src/platforms/next if ($?) { Write-Host "Next.js build successful" -ForegroundColor Green diff --git a/src/forks/assets-registry/package.json b/src/forks/assets-registry/package.json deleted file mode 100644 index ea0d35f..0000000 --- a/src/forks/assets-registry/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@react-native/assets-registry", - "version": "0.72.0", - "description": "Asset support code for React Native.", - "repository": { - "type": "git", - "url": "git@github.com:facebook/react-native.git", - "directory": "packages/assets" - }, - "license": "MIT" -} diff --git a/src/forks/assets-registry/path-support.js b/src/forks/assets-registry/path-support.js deleted file mode 100644 index 911a1d4..0000000 --- a/src/forks/assets-registry/path-support.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow strict - */ - -'use strict'; - -const androidScaleSuffix = { - '0.75': 'ldpi', - '1': 'mdpi', - '1.5': 'hdpi', - '2': 'xhdpi', - '3': 'xxhdpi', - '4': 'xxxhdpi', -}; - -const ANDROID_BASE_DENSITY = 160; - -/** - * FIXME: using number to represent discrete scale numbers is fragile in essence because of - * floating point numbers imprecision. - */ -function getAndroidAssetSuffix(scale) { - if (scale.toString() in androidScaleSuffix) { - return androidScaleSuffix[scale.toString()]; - } - // NOTE: Android Gradle Plugin does not fully support the nnndpi format. - // See https://issuetracker.google.com/issues/72884435 - if (Number.isFinite(scale) && scale > 0) { - return Math.round(scale * ANDROID_BASE_DENSITY) + 'dpi'; - } - throw new Error('no such scale ' + scale.toString()); -} - -// See https://developer.android.com/guide/topics/resources/drawable-resource.html -const drawableFileTypes = new Set([ - 'gif', - 'jpeg', - 'jpg', - 'ktx', - 'png', - 'svg', - 'webp', - 'xml', -]); - -function getAndroidResourceFolderName( - asset, - scale, -) { - if (!drawableFileTypes.has(asset.type)) { - return 'raw'; - } - const suffix = getAndroidAssetSuffix(scale); - if (!suffix) { - throw new Error( - "Don't know which android drawable suffix to use for scale: " + - scale + - '\nAsset: ' + - JSON.stringify(asset, null, '\t') + - '\nPossible scales are:' + - JSON.stringify(androidScaleSuffix, null, '\t'), - ); - } - return 'drawable-' + suffix; -} - -function getAndroidResourceIdentifier(asset) { - return (getBasePath(asset) + '/' + asset.name) - .toLowerCase() - .replace(/\//g, '_') // Encode folder structure in file name - .replace(/([^a-z0-9_])/g, '') // Remove illegal chars - .replace(/^assets_/, ''); // Remove "assets_" prefix -} - -function getBasePath(asset) { - const basePath = asset.httpServerLocation; - return basePath.startsWith('/') ? basePath.slice(1) : basePath; -} - -module.exports = { - getAndroidResourceFolderName, - getAndroidResourceIdentifier, - getBasePath, -}; diff --git a/src/forks/assets-registry/registry.js b/src/forks/assets-registry/registry.js deleted file mode 100644 index 3851d92..0000000 --- a/src/forks/assets-registry/registry.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict - * @format - */ - -'use strict'; - -const assets = []; - -function registerAsset(asset) { - // `push` returns new array length, so the first asset will - // get id 1 (not 0) to make the value truthy - return assets.push(asset); -} - -function getAssetByID(assetId) { - return assets[assetId - 1]; -} - -module.exports = {registerAsset, getAssetByID}; diff --git a/src/forks/solito b/src/forks/solito index 71cab3d..205e4e7 160000 --- a/src/forks/solito +++ b/src/forks/solito @@ -1 +1 @@ -Subproject commit 71cab3d370a6377d6e09571363df2836d104479f +Subproject commit 205e4e731f6fa6138785db919d1bb45870459c25 diff --git a/src/interface/components/FontAwesomeIcon.tsx b/src/interface/components/FontAwesomeIcon.tsx deleted file mode 100644 index 66a2f9f..0000000 --- a/src/interface/components/FontAwesomeIcon.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import React, { ComponentProps } from 'react'; -import { createThemedComponent, useDripsyTheme, ColorPath, StyledProps } from 'dripsy'; -import { get } from 'dripsy/build/core/css/get'; -import { FontAwesomeIcon as RFontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; - -const DripsyFontAwesomeIcon = createThemedComponent(RFontAwesomeIcon, { - themeKey: 'images', - defaultVariant: 'icon', -}); - -type InputProps = React.ComponentPropsWithoutRef; -type ColorKeys = keyof Pick< - InputProps, - 'color' | 'secondaryColor' ->; - -export type DripsyTextInputProps = StyledProps<'missing'> & - Omit, ColorKeys> & - { - [key in ColorKeys]?: (string & {}) | ColorPath - }; - -const colorKeys: Record = { - color: true, - secondaryColor: true, -}; - -export function FontAwesomeIcon({ ...props }: Parameters[0]) { - const { theme } = useDripsyTheme(); - - const propsToPass = React.useMemo(() => { - const generatedProps = { ...props }; - Object.keys(colorKeys).forEach((key: ColorKeys) => { - if (props[key] && theme?.colors) - generatedProps[key] = get(theme.colors, props[key] as string) ?? props[key]; - }); - return generatedProps; - }, [theme, props]); - - return ; -}; diff --git a/src/interface/components/icons/dripsy-icons.tsx b/src/interface/components/icons/dripsy-icons.tsx new file mode 100644 index 0000000..3519b35 --- /dev/null +++ b/src/interface/components/icons/dripsy-icons.tsx @@ -0,0 +1,225 @@ +'use client'; + +import React from 'react'; +import { createThemedComponent, useDripsyTheme, ColorPath, StyledProps } from 'dripsy'; +import { get } from 'dripsy/build/core/css/get'; +import { FontAwesomeIcon as FontAwesome } from '@fortawesome/react-native-fontawesome'; +import MaterialSymbols from './MaterialSymbols'; +import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; +import Octicons from '@expo/vector-icons/Octicons'; +import FontAwesome5 from '@expo/vector-icons/FontAwesome5'; +import FontAwesome6 from '@expo/vector-icons/FontAwesome6'; + +// Really, Expo? Major typing blunder right---multi-font icons are declared as `any`. +// Might as well fix it here since I have an opportunity. +import type { IconProps } from '@expo/vector-icons/build/createIconSet'; +import type FA5GlyphMap from '@expo/vector-icons/build/vendor/react-native-vector-icons/glyphmaps/FontAwesome5Free.json'; +import type FA6GlyphMap from '@expo/vector-icons/build/vendor/react-native-vector-icons/glyphmaps/FontAwesome6Free.json'; + +const DripsyFontAwesomeIcon = createThemedComponent(FontAwesome, { + themeKey: 'images', + defaultVariant: 'icon', +}); + +type FAIconProps = React.ComponentPropsWithoutRef; +type FAColorKeys = keyof Pick< + FAIconProps, + 'color' | 'secondaryColor' +>; + +export type DripsyFontAwesomeIconProps = StyledProps<'images'> & + Omit & + { + [key in FAColorKeys]?: (string & {}) | ColorPath + }; + +const faColorKeys: Record = { + color: true, + secondaryColor: true, +}; + +export function FontAwesomeIcon({ ...props }: DripsyFontAwesomeIconProps) { + const { theme } = useDripsyTheme(); + + const propsToPass = React.useMemo(() => { + const generatedProps = { ...props }; + Object.keys(faColorKeys).forEach((key: FAColorKeys) => { + if (props[key] && theme?.colors) + generatedProps[key] = get(theme.colors, props[key] as string) ?? props[key]; + }); + return generatedProps; + }, [theme, props]); + + return ; +}; + +const DripsyMaterialSymbolsIcon = createThemedComponent(MaterialSymbols, { + themeKey: 'images', + defaultVariant: 'icon', +}); + +export type MaterialSymbolsProps = React.ComponentPropsWithoutRef; +type MaterialSymbolsColorKeys = keyof Pick< + MaterialSymbolsProps, + 'color' +>; + +export type DripsyMaterialSymbolsIconProps = StyledProps<'images'> & + Omit & + { + [key in MaterialSymbolsColorKeys]?: (string & {}) | ColorPath + }; + +const materialSymbolsColorKeys: Record = { + color: true, +}; + +export function MaterialSymbolsIcon({ ...props }: DripsyMaterialSymbolsIconProps) { + const { theme } = useDripsyTheme(); + + const propsToPass = React.useMemo(() => { + const generatedProps = { ...props }; + Object.keys(materialSymbolsColorKeys).forEach((key: MaterialSymbolsColorKeys) => { + if (props[key] && theme?.colors) + generatedProps[key] = get(theme.colors, props[key] as string) ?? props[key]; + }); + return generatedProps; + }, [theme, props]); + + return ; +}; + +const DripsyMaterialCommunityIcon = createThemedComponent(MaterialCommunityIcons, { + themeKey: 'images', + defaultVariant: 'icon', +}); + +export type MaterialCommunityIconProps = React.ComponentPropsWithoutRef; +type MaterialCommunityIconColorKeys = keyof Pick< + MaterialCommunityIconProps, + 'color' +>; + +export type DripsyMaterialCommunityIconProps = StyledProps<'images'> & + Omit & + { + [key in MaterialCommunityIconColorKeys]?: (string & {}) | ColorPath + }; + +const materialCommunityIconColorKeys: Record = { + color: true, +}; + +export function MaterialCommunityIcon({ ...props }: DripsyMaterialCommunityIconProps) { + const { theme } = useDripsyTheme(); + + const propsToPass = React.useMemo(() => { + const generatedProps = { ...props }; + Object.keys(materialCommunityIconColorKeys).forEach((key: MaterialCommunityIconColorKeys) => { + if (props[key] && theme?.colors) + generatedProps[key] = get(theme.colors, props[key] as string) ?? props[key]; + }); + return generatedProps; + }, [theme, props]); + + return ; +}; + +const DripsyOcticonsIcon = createThemedComponent(Octicons, { + themeKey: 'images', + defaultVariant: 'icon', +}); + +export type OcticonsIconProps = React.ComponentPropsWithoutRef; + +export type DripsyOcticonsIconProps = StyledProps<'images'> & + Omit & + { + color?: (string & {}) | ColorPath + }; + +export function OcticonsIcon({ ...props }: DripsyOcticonsIconProps) { + const { theme } = useDripsyTheme(); + + const propsToPass = React.useMemo(() => { + const generatedProps = { ...props }; + if (props.color && theme?.colors) + generatedProps.color = get(theme.colors, props.color as string) ?? props.color; + return generatedProps; + }, [theme, props]); + + return ; +} + +const DripsyFontAwesome5Icon = createThemedComponent(FontAwesome5, { + themeKey: 'images', + defaultVariant: 'icon', +}); + +export type FontAwesome5IconProps = IconProps; +type FontAwesome5IconColorKeys = keyof Pick< + FontAwesome5IconProps, + 'color' +>; + +export type DripsyFontAwesome5IconProps = StyledProps<'images'> & + Omit & + { + [key in FontAwesome5IconColorKeys]?: (string & {}) | ColorPath + }; + +const fontAwesome5IconColorKeys: Record = { + color: true, +}; + +export function FontAwesome5Icon({ ...props }: DripsyFontAwesome5IconProps) { + const { theme } = useDripsyTheme(); + + const propsToPass = React.useMemo(() => { + const generatedProps = { ...props }; + Object.keys(fontAwesome5IconColorKeys).forEach((key: FontAwesome5IconColorKeys) => { + if (props[key] && theme?.colors) + generatedProps[key] = get(theme.colors, props[key] as string) ?? props[key]; + }); + return generatedProps; + }, [theme, props]); + + return ; +}; + +const DripsyFontAwesome6Icon = createThemedComponent(FontAwesome6, { + themeKey: 'images', + defaultVariant: 'icon', +}); + + +export type FontAwesome6IconProps = IconProps; +type FontAwesome6IconColorKeys = keyof Pick< + FontAwesome6IconProps, + 'color' +>; + +export type DripsyFontAwesome6IconProps = StyledProps<'images'> & + Omit & + { + [key in FontAwesome6IconColorKeys]?: (string & {}) | ColorPath + }; + +const fontAwesome6IconColorKeys: Record = { + color: true, +}; + +export function FontAwesome6Icon({ ...props }: DripsyFontAwesome6IconProps) { + const { theme } = useDripsyTheme(); + + const propsToPass = React.useMemo(() => { + const generatedProps = { ...props }; + Object.keys(fontAwesome6IconColorKeys).forEach((key: FontAwesome6IconColorKeys) => { + if (props[key] && theme?.colors) + generatedProps[key] = get(theme.colors, props[key] as string) ?? props[key]; + }); + return generatedProps; + }, [theme, props]); + + return ; +}; diff --git a/src/interface/features/getting-started-screen.tsx b/src/interface/features/getting-started-screen.tsx index 132bc44..324bc7e 100644 --- a/src/interface/features/getting-started-screen.tsx +++ b/src/interface/features/getting-started-screen.tsx @@ -5,6 +5,7 @@ import { TextLink } from 'solito/link'; import { Image } from '../components/image/Image'; import { OptionallyScrollable } from '../components/TopLevelScreenView'; import { useAuthentication } from '../provider/auth/authentication'; +import { getPublicEnvValue } from 'lib/env-utils'; export function GettingStartedScreen() { return @@ -80,7 +81,7 @@ function GettingStartedScreenInternal() { Stockedhome logo @@ -18,9 +20,11 @@ export function HomeScreen() { function HomeScreenInternal() { const sx = useSx(); const auth = useAuthentication(); - const { showLogInScreen } = useLogInScreen(); + const { showLogInScreen } = useLogInDialog(); return <> + +

Stockedhome (Authentication)

@@ -54,5 +58,12 @@ function HomeScreenInternal() { GitHub Repo + + Stockedhome logo ; } diff --git a/src/interface/features/login-dialog.tsx b/src/interface/features/login-dialog.tsx index b9738d8..dd334b3 100644 --- a/src/interface/features/login-dialog.tsx +++ b/src/interface/features/login-dialog.tsx @@ -16,6 +16,7 @@ import { useUpdatedRef } from '../hooks/useUpdatedRef'; export interface LogInScreenContext { showLogInScreen(e?: {preventDefault?(): unknown}): void; + hideLogInScreen(): void; isLogInScreenVisible: boolean; } @@ -31,7 +32,7 @@ declare global { } } -export function LogInScreenProvider({ children }: { readonly children: React.ReactNode }) { +export function LogInDialogProvider({ children }: { readonly children: React.ReactNode }) { const [isLogInScreenVisible, setIsLogInScreenVisible] = React.useState(false); const showLogInScreen = React.useCallback((e?: Record & {preventDefault?(): unknown}) => { @@ -43,7 +44,11 @@ export function LogInScreenProvider({ children }: { readonly children: React.Rea setIsLogInScreenVisible(false); }, []); - const value = React.useMemo(() => ({ showLogInScreen, isLogInScreenVisible }), [showLogInScreen, isLogInScreenVisible]); + const value = React.useMemo(() => ({ + showLogInScreen, + hideLogInScreen, + isLogInScreenVisible + }), [showLogInScreen, hideLogInScreen, isLogInScreenVisible]); React.useEffect(() => { window.loginProvider = value; @@ -55,7 +60,7 @@ export function LogInScreenProvider({ children }: { readonly children: React.Rea ; } -export function useLogInScreen() { +export function useLogInDialog() { return React.useContext(logInScreenContext); } @@ -158,17 +163,18 @@ export function LogInScreenComponent({ hideLogInScreen, isLogInScreenVisible = t const auth = useAuthentication(); const config = useConfig(); - const [username, setUsername] = React.useState(auth.user?.username); + const lastUsername = auth.lastUsername; + const [username, setUsername] = React.useState(lastUsername); const [error, setError] = React.useState(null); React.useEffect(() => { if (isLogInScreenVisible) { - setUsername(auth.user?.username); + setUsername(lastUsername); } else { setUsername(''); setError(null); } - }, [isLogInScreenVisible, auth.user?.username]); + }, [isLogInScreenVisible, lastUsername]); const hideLogInScreenRef = useUpdatedRef(hideLogInScreen); const logIn = React.useCallback(async () => { @@ -207,7 +213,7 @@ export function LogInScreenComponent({ hideLogInScreen, isLogInScreenVisible = t

Don’t have an account?

Have an account but not a passkey?

(null); diff --git a/src/interface/package.json b/src/interface/package.json index 3263447..838fae8 100644 --- a/src/interface/package.json +++ b/src/interface/package.json @@ -2,6 +2,7 @@ "name": "app", "main": "index.ts", "dependencies": { + "@stockehome/codegen": "link:../../codegen/results", "@expo-google-fonts/rubik": "^0.2.3", "@hexagon/base64": "^1.1.28", "dripsy": "^4.3.5", diff --git a/src/interface/provider/auth/authentication.tsx b/src/interface/provider/auth/authentication.tsx index b5e51af..1037a65 100644 --- a/src/interface/provider/auth/authentication.tsx +++ b/src/interface/provider/auth/authentication.tsx @@ -246,7 +246,7 @@ function AuthenticationProviderInternal({ children, trpc }: { readonly children: if (_expiresInMs < 1000 * 60 * 60) // 60 minutes timeoutDuration = Math.max(0, Math.min(1000 * 60 * 10 - timeSinceLastAsk, )); // 10 minutes since last ask, or immediately if it's been longer else - timeoutDuration = _expiresInMs - 1000 * 60 * 60; // 60 minutes before expiration + timeoutDuration = _expiresInMs - (1000 * 60 * 60); // 60 minutes before expiration const intervalTimeout = setTimeout(() => { diff --git a/src/interface/provider/tRPC-provider.tsx b/src/interface/provider/tRPC-provider.tsx index bdb5c40..bc2837e 100644 --- a/src/interface/provider/tRPC-provider.tsx +++ b/src/interface/provider/tRPC-provider.tsx @@ -34,7 +34,7 @@ function createTRPCClient(primaryConfig: Config, supplementaryConfig: Config | n return trpc.createClient({ links: [ httpBatchLink({ - url: `${primaryConfig.canonicalRoot}/api/`, + url: new URL('api', primaryConfig.canonicalRoot), transformer: superjson, }), ], diff --git a/src/interface/provider/theme.tsx b/src/interface/provider/theme.tsx index 3359da3..dffc4bd 100644 --- a/src/interface/provider/theme.tsx +++ b/src/interface/provider/theme.tsx @@ -163,7 +163,7 @@ const theme = makeTheme({ color: 'textBright', marginBottom: 12, }, - a: { + link: { color: 'accent', }, buttonText: { diff --git a/src/lib/env-schema.ts b/src/lib/env-schema.ts index bed1f80..16adeb6 100644 --- a/src/lib/env-schema.ts +++ b/src/lib/env-schema.ts @@ -189,6 +189,30 @@ or development mode (running the server locally, usually from source code direct OPTIONAL: Base URL of your web server, used for SEO metadata and the Web App Manifest. For example, "https://stockedhome.app/" +`.trim()), + + /** + * OPTIONAL: Prefix for the asset URLs. May be a path or a full URL. + * + * Assets include fonts, images, Next.js scripts, static pages, + * and other things that only change when the site/app is rebuilt. + * + * Automatically covers files in the `_next/static`, but does not cover `public`. + * + * Examples: + * - "/assets/" + * - "https://cdn.example.com/" + */ + NEXT_PUBLIC_ASSET_PREFIX: z.string().optional().describe(` +OPTIONAL: Prefix for the asset URLs. May be a path or a full URL. + + +Assets include fonts, images, Next.js scripts, static pages, +and other things that only change when the site/app is rebuilt. + +Examples: +- "/assets/" +- "https://cdn.example.com/" `.trim()), }).merge(z.object({})); diff --git a/src/lib/env-utils.d.ts b/src/lib/env-utils.d.ts new file mode 100644 index 0000000..88082a4 --- /dev/null +++ b/src/lib/env-utils.d.ts @@ -0,0 +1,17 @@ +// This file is a split js/d.ts file so it can be used in Expo's config files +// which do not support TypeScript imports + +import type { Env } from "./env-schema"; + +/** + * Ensures any NEXT_PUBLIC_ and EXPO_PUBLIC environment variables are accessible from both prefixes + * + * Called in the configuration files for each platform (next.config.ts, metro.config.js, etc.) +*/ +export function normalizeEnv(): void; + +type PublicEnv = {[TKey in keyof Env as TKey extends `NEXT_PUBLIC_${infer TNewKeyNext}` ? TNewKeyNext : TKey extends `EXPO_PUBLIC_${infer TNewKeyExpo}` ? TNewKeyExpo : never]: Env[TKey]} & {NEXT_PUBLIC_TEST: string}; +type NonNullablePublicEnv = {[TKey in keyof PublicEnv as Exclude extends never ? TKey : never]: PublicEnv[TKey]}; // do NOT ask me how this type works because I can't quite tell you + +export function getPublicEnvValue(key: keyof NonNullablePublicEnv): PublicEnv[typeof key] +export function getPublicEnvValue(key: keyof PublicEnv, defaultValue: NonNullable): NonNullable diff --git a/src/lib/env-utils.js b/src/lib/env-utils.js new file mode 100644 index 0000000..e2dadca --- /dev/null +++ b/src/lib/env-utils.js @@ -0,0 +1,58 @@ +// This file is a split js/d.ts file so it can be used in Expo's config files +// which do not support TypeScript imports + +/** + * Ensures any NEXT_PUBLIC_ and EXPO_PUBLIC environment variables are accessible from both prefixes + * + * Called in the configuration files for each platform (next.config.ts, metro.config.js, etc.) +*/ +function normalizeEnv() { + const env = process.env; + for (const [key, value] of Object.entries(env)) { + + // Remove empty values so we can use empty values to unset variables + if (!value) + delete env[key]; + + if (key.startsWith('NEXT_PUBLIC_')) { + const otherKey = key.replace('NEXT_PUBLIC_', 'EXPO_PUBLIC_'); + if (!(otherKey in env)) { + env[otherKey] = value; + } else if (env[otherKey] !== value) { + throw new Error(`Environment variable ${key} conflicts with ${otherKey}`, { + [key]: value, + [otherKey]: env[otherKey], + }); + } + } + if (key.startsWith('EXPO_PUBLIC_')) { + const otherKey = key.replace('EXPO_PUBLIC_', 'NEXT_PUBLIC_'); + if (!(otherKey in env)) { + env[otherKey] = value; + } else if (env[otherKey] !== value) { + throw new Error(`Environment variable ${key} conflicts with ${otherKey}`, { + [key]: value, + [otherKey]: env[otherKey], + }); + } + } + } + + console.log('Normalized environment variables:', process.env); +} + + +function getPublicEnvValue(key, defaultValue) { + const value = process.env[`NEXT_PUBLIC_${key}`] ?? process.env[`EXPO_PUBLIC_${key}`] ?? defaultValue; + + if (value === null || value === undefined) { + throw new Error(`Public environment variable ${key} (NEXT_PUBLIC_${key} / EXPO_PUBLIC_${key}) appears to be missing, but no default value was provided to getPublicEnvValue()`); + } + + return value; +} + +module.exports = { + normalizeEnv, + getPublicEnvValue, +}; diff --git a/src/platforms/expo/app.config.ts b/src/platforms/expo/app.config.ts index b8d8a1d..4d3f333 100644 --- a/src/platforms/expo/app.config.ts +++ b/src/platforms/expo/app.config.ts @@ -2,6 +2,7 @@ import type { ExpoConfig, ConfigContext } from 'expo/config'; import rootPackageJson from '../../../package.json'; import fs from 'fs'; import path from 'path'; +import { normalizeEnv } from 'lib/env-utils'; const IS_DEV = process.env.APP_VARIANT === 'development'; @@ -19,6 +20,7 @@ if (!fs.existsSync(path.join(expoRoot, './.env'))) { } } +normalizeEnv(); export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, diff --git a/src/platforms/expo/app/web/_layout.tsx b/src/platforms/expo/app/web/_layout.tsx index 1628ed7..c5b4225 100644 --- a/src/platforms/expo/app/web/_layout.tsx +++ b/src/platforms/expo/app/web/_layout.tsx @@ -1,11 +1,11 @@ -import { useSx, View } from "dripsy"; +import { Text, useSx, View } from "dripsy"; import { Slot, usePathname } from 'expo-router'; import { TopLevelScreenView } from "interface/components/TopLevelScreenView"; -import { Link } from "solito/link"; import { useAuthentication } from "interface/provider/auth/authentication"; import React from "react"; //import MaterialSymbols from "interface/components/icons/material-symbols/MaterialSymbols"; import Octicons from '@expo/vector-icons/Octicons'; +import { Link } from "solito/link"; export default function ActualMobileLayout() { const sx = useSx(); @@ -14,7 +14,7 @@ export default function ActualMobileLayout() { return - {!auth.user ? null : + {!auth.user ? null : path.resolve(projectRoot, '.pnpm', dir, 'node_modules')), - path.resolve(expoRoot, 'node_modules'), path.resolve(projectRoot, 'node_modules'), + path.resolve(expoRoot, 'node_modules'), + path.resolve(projectRoot, 'src/lib/node_modules'), + path.resolve(projectRoot, 'src/interface/node_modules'), ].flat(); +// Metro didn't like the codegen package normally so here we go +config.resolver.extraNodeModules = { + '@stockedhome/codegen': path.resolve(projectRoot, 'codegen/results'), +}; + config.resolver.assetExts = [ ...config.resolver.assetExts ?? [], ...config.resolver.assetExts?.map(ext => `/index${ext}`) ?? [], diff --git a/src/platforms/next/app/robots.ts b/src/platforms/next/app/robots.ts new file mode 100644 index 0000000..53c3bb9 --- /dev/null +++ b/src/platforms/next/app/robots.ts @@ -0,0 +1,17 @@ +import type { MetadataRoute } from 'next'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: [ + { + userAgent: '*', + disallow: [ + '/api', + '/_next', + '/login/request-passkey' + ], + }, + ], + //sitemap: 'https://acme.com/sitemap.xml', -- TODO: See comments in instrumentation.ts regarding sitemap generation + }; +} diff --git a/src/platforms/next/app/web/login/LoginPage.tsx b/src/platforms/next/app/web/login/LoginPage.tsx index 5bf2b9b..5254789 100644 --- a/src/platforms/next/app/web/login/LoginPage.tsx +++ b/src/platforms/next/app/web/login/LoginPage.tsx @@ -5,6 +5,7 @@ import { Image } from "interface/components/image/Image"; import { View } from "dripsy"; import { useAuthentication } from "interface/provider/auth/authentication"; import { redirect } from "next/navigation"; +import { getPublicEnvValue } from "lib/env-utils"; export function LoginPageClient() { const auth = useAuthentication(); @@ -14,6 +15,6 @@ export function LoginPageClient() { - Stockedhome logo + Stockedhome logo ; } diff --git a/src/platforms/next/instrumentation.ts b/src/platforms/next/instrumentation.ts index 228d003..6ef21af 100644 --- a/src/platforms/next/instrumentation.ts +++ b/src/platforms/next/instrumentation.ts @@ -3,6 +3,11 @@ import * as nextStyleLogging from 'next/dist/build/output/log'; import { db } from 'lib/db'; import type { Config } from 'lib/config/schema'; +import type { IConfig } from 'next-sitemap'; +import { isError } from 'node:util'; + + + async function registerForReal() { console.log(' ❃ Before the server starts, we need to do some setup!'); @@ -56,6 +61,95 @@ async function registerForReal() { nextStyleLogging.event('Connected to database!'); }); + // Generate Sitemap! + + + if (process.env.NODE_ENV !== 'production') { // only generate sitemap in prod please and thank you +// +// TODO: Get this working inside of the Docker container (works running in the host OS just fine) +// Once sitemap generation is working, be sure to remove the app/robots.ts file +// +// const [ +// { toChunks }, // = import('next-sitemap/utils/array'), +// { ConfigParser }, // = import('next-sitemap/parsers/config-parser'), +// { ManifestParser }, // = import('next-sitemap/parsers/manifest-parser'), +// { UrlSetBuilder }, // = import('next-sitemap/builders/url-set-builder'), +// { ExportableBuilder }, // = import('next-sitemap/builders/exportable-builder'), +// { getRuntimePaths }, // = import('next-sitemap/utils/path'), +// {default: path}, // = import('node:path'), +// {default: url}, // = import('node:url'), +// {default: fs}, // = import('node:fs/promises'), +// ] = await Promise.all([ +// import('next-sitemap/utils/array'), +// import('next-sitemap/parsers/config-parser'), +// import('next-sitemap/parsers/manifest-parser'), +// import('next-sitemap/builders/url-set-builder'), +// import('next-sitemap/builders/exportable-builder'), +// import('next-sitemap/utils/path'), +// import('node:path'), +// import('node:url'), +// import('node:fs/promises') +// ]); +// +// const thisFilePath = url.fileURLToPath(import.meta.url); +// const thisDirPath = path.dirname(thisFilePath); +// +// const sitemapConfigRaw = { +// siteUrl: config.canonicalRoot.href, +// changefreq: 'monthly', +// sourceDir: thisFilePath.endsWith('/.next/server/instrumentation.js') ? path.resolve(thisDirPath, '../..') : path.resolve(thisDirPath, '.next'), +// outDir: thisFilePath.endsWith('/.next/server/instrumentation.js') ? path.resolve(thisDirPath, '../../../public') : path.join(thisDirPath, 'public'), // so we don't have to gitignore it +// generateIndexSitemap: false, +// sitemapSize: 50000, +// +// generateRobotsTxt: true, +// robotsTxtOptions: { +// policies: [ +// { +// userAgent: '*', +// disallow: [ +// '/api', +// '/_next', +// '/login/request-passkey' +// ], +// }, +// ], +// } +// } as const satisfies IConfig; +// +// try { +// await fs.access(sitemapConfigRaw.outDir); +// } catch (err) { +// if (err instanceof Error && 'code' in err && err.code === 'ENOENT') await fs.mkdir(sitemapConfigRaw.outDir, { recursive: true }); +// else throw err; +// } +// +// const configParser = new ConfigParser(); +// const sitemapConfig = await configParser.validateConfig(sitemapConfigRaw); +// const runtimePaths = getRuntimePaths(sitemapConfig); +// +// const manifestParser = new ManifestParser(sitemapConfig, runtimePaths); +// const manifest = await manifestParser.loadManifest(); +// +// const urlSetBuilder = new UrlSetBuilder(sitemapConfig, manifest); +// const urlSet = await urlSetBuilder.createUrlSet(); +// +// const chunks = toChunks(urlSet, sitemapConfig.sitemapSize!); +// +// const exportBuilder = new ExportableBuilder(sitemapConfig, runtimePaths); +// +// await exportBuilder.registerSitemaps(chunks); +// +// if (sitemapConfig.generateIndexSitemap) +// await exportBuilder.registerIndexSitemap(); +// +// if (sitemapConfig?.generateRobotsTxt) +// await exportBuilder.registerRobotsTxt(); +// +// await exportBuilder.exportAll(); + } + + // end Node.js runtime } @@ -67,10 +161,11 @@ async function registerForReal() { } export async function register() { + let hasError = true; try { await registerForReal(); - } catch (err) { - console.error(err); - throw new Error('See the logs above for more information.'); + hasError = false; + } finally { + if (hasError) nextStyleLogging.error('An error occurred during server setup. Please see the above error messages for more information.'); } } diff --git a/src/platforms/next/next.config.ts b/src/platforms/next/next.config.ts index 79b5bbc..ae04de1 100644 --- a/src/platforms/next/next.config.ts +++ b/src/platforms/next/next.config.ts @@ -5,6 +5,8 @@ import fs from 'fs'; import url from 'url'; import path from 'path'; import type { NextConfig } from "next"; +import { env } from "lib/env-schema"; +import { normalizeEnv } from "lib/env-utils"; console.log('Entering Next.js config 🎇'); @@ -16,12 +18,16 @@ if (!fs.existsSync(path.join(nextRoot, './.env'))) { fs.symlinkSync(path.join(projectRoot, './.env'), path.join(nextRoot, './.env'), 'file'); } +normalizeEnv(); + const baseConfig: NextConfig = { // // ~~reanimated (and thus, Moti) doesn't work with strict mode currently...~~ // fixed // ~~https://github.com/nandorojo/moti/issues/224~~ // // https://github.com/necolas/react-native-web/pull/2330 reactStrictMode: true, + assetPrefix: env.NEXT_PUBLIC_ASSET_PREFIX, + images: { contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", disableStaticImages: true, diff --git a/src/platforms/next/package.json b/src/platforms/next/package.json index 7e7395d..6ed04d5 100644 --- a/src/platforms/next/package.json +++ b/src/platforms/next/package.json @@ -5,13 +5,13 @@ "___COMMENT_ON_DEPENDENCIES": "Any local packages MUST use the `link` protocol for the Next.js platform; otherwise, it doesn't watch files for changes properly", "dependencies": { "@expo/next-adapter": "6.0.0", - "@react-native/assets-registry": "link:../../forks/assets-registry", "interface": "link:../../interface", "json-loader": "^0.5.7", "lib": "link:../../lib", "next-compose-plugins": "^2.2.1", "next-fonts": "^1.5.1", "next-images": "^1.8.5", + "next-sitemap": "^4.2.3", "raf": "^3.4.1", "sass": "^1.80.4", "setimmediate": "^1.0.5"