diff --git a/boilerplate/app.config.ts b/boilerplate/app.config.ts index 658141ffd..b14a6361d 100644 --- a/boilerplate/app.config.ts +++ b/boilerplate/app.config.ts @@ -1,4 +1,22 @@ import { ExpoConfig, ConfigContext } from "@expo/config" +import fs from "fs" +import path from "path" + +/** + * Automatically discover all fonts under `assets/fonts` + * so we never have to hardcode them. + */ +function getFontPathsFromAssetsFolder(): string[] { + const fontsDir = path.resolve(__dirname, "assets/fonts") + if (!fs.existsSync(fontsDir)) { + console.warn("FONT_WARNING: No custom fonts detected.") + return [] + } + + return fs + .readdirSync(fontsDir) + .map((file) => `./assets/fonts/${file}`) +} /** * Use tsx/cjs here so we can use TypeScript for our Config Plugins @@ -16,6 +34,7 @@ import "tsx/cjs" */ module.exports = ({ config }: ConfigContext): Partial => { const existingPlugins = config.plugins ?? [] + const fontPaths = getFontPathsFromAssetsFolder() return { ...config, @@ -36,6 +55,14 @@ module.exports = ({ config }: ConfigContext): Partial => { ], }, }, - plugins: [...existingPlugins], + plugins: [ + ...existingPlugins, + [ + "expo-font", + { + fonts: fontPaths, + }, + ], + ], } } diff --git a/boilerplate/app.json b/boilerplate/app.json index a477e655f..7a6819c50 100644 --- a/boilerplate/app.json +++ b/boilerplate/app.json @@ -35,7 +35,6 @@ }, "plugins": [ "expo-localization", - "expo-font", [ "expo-splash-screen", { diff --git a/boilerplate/app/app.tsx b/boilerplate/app/app.tsx index b7fea621f..c741d35b6 100644 --- a/boilerplate/app/app.tsx +++ b/boilerplate/app/app.tsx @@ -19,17 +19,15 @@ if (__DEV__) { import "./utils/gestureHandler" import { useEffect, useState } from "react" -import { useFonts } from "expo-font" import * as Linking from "expo-linking" import { KeyboardProvider } from "react-native-keyboard-controller" import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context" - import { AuthProvider } from "./context/AuthContext" // @demo remove-current-line import { initI18n } from "./i18n" import { AppNavigator } from "./navigators/AppNavigator" import { useNavigationPersistence } from "./navigators/navigationUtilities" import { ThemeProvider } from "./theme/context" -import { customFontsToLoad } from "./theme/typography" +import { useCustomFonts } from "./theme/typography" import { loadDateFnsLocale } from "./utils/formatDate" import * as storage from "./utils/storage" @@ -68,7 +66,7 @@ export function App() { isRestored: isNavigationStateRestored, } = useNavigationPersistence(storage, NAVIGATION_PERSISTENCE_KEY) - const [areFontsLoaded, fontLoadError] = useFonts(customFontsToLoad) + const [areFontsLoaded, fontLoadError] = useCustomFonts() const [isI18nInitialized, setIsI18nInitialized] = useState(false) useEffect(() => { @@ -83,7 +81,7 @@ export function App() { // In iOS: application:didFinishLaunchingWithOptions: // In Android: https://stackoverflow.com/a/45838109/204044 // You can replace with your own loading component if you wish. - if (!isNavigationStateRestored || !isI18nInitialized || (!areFontsLoaded && !fontLoadError)) { + if (!isNavigationStateRestored || !isI18nInitialized || !areFontsLoaded || fontLoadError) { return null } diff --git a/boilerplate/app/theme/typography.ts b/boilerplate/app/theme/typography.ts index 70842e0fb..9cf8328eb 100644 --- a/boilerplate/app/theme/typography.ts +++ b/boilerplate/app/theme/typography.ts @@ -2,30 +2,53 @@ // markdown file and add links from here import { Platform } from "react-native" -import { - SpaceGrotesk_300Light as spaceGroteskLight, - SpaceGrotesk_400Regular as spaceGroteskRegular, - SpaceGrotesk_500Medium as spaceGroteskMedium, - SpaceGrotesk_600SemiBold as spaceGroteskSemiBold, - SpaceGrotesk_700Bold as spaceGroteskBold, -} from "@expo-google-fonts/space-grotesk" +import { FontSource, useFonts } from "expo-font" -export const customFontsToLoad = { - spaceGroteskLight, - spaceGroteskRegular, - spaceGroteskMedium, - spaceGroteskSemiBold, - spaceGroteskBold, +/** + * The naming here is important. Most font files come with + * a name like `SpaceGrotesk_300Light`, but iOS uses the font PostScript + * name (in this case `SpaceGrotesk-Light`). To keep the imports the + * same on both platforms use the PostScript name. + * + * For more info: https://docs.expo.dev/develop/user-interface/fonts/#how-to-determine-which-font-family-name-to-use + */ +export const customFontsToLoad: Record = + Platform.OS === "web" + ? { + "SpaceGrotesk-Light": require("../../assets/fonts/SpaceGrotesk-Light.ttf"), + "SpaceGrotesk-Regular": require("../../assets/fonts/SpaceGrotesk-Regular.ttf"), + "SpaceGrotesk-Medium": require("../../assets/fonts/SpaceGrotesk-Medium.ttf"), + "SpaceGrotesk-SemiBold": require("../../assets/fonts/SpaceGrotesk-SemiBold.ttf"), + "SpaceGrotesk-Bold": require("../../assets/fonts/SpaceGrotesk-Bold.ttf"), + } + : {} + +/** + * On iOS and Android, the fonts are embedded as part of the app binary + * using the expo config plugin in `app.config.ts`. See the project + * [`app.config.ts`](../../app.config.ts) for the expo-fonts configuration. The assets + * are added via the `app/assets/fonts` folder. This config plugin + * does NOT work for web, so we have to dynamically load the fonts via this hook. + * + * For more info: https://docs.expo.dev/versions/latest/sdk/font/ + */ +export const useCustomFonts = (): [boolean, Error | null] => { + const [areFontsLoaded, fontError] = useFonts(customFontsToLoad) + if (Platform.OS === "web") { + return [areFontsLoaded, fontError] + } + + // On native, fonts are precompiled and ready + return [true, null] } const fonts = { spaceGrotesk: { - // Cross-platform Google font. - light: "spaceGroteskLight", - normal: "spaceGroteskRegular", - medium: "spaceGroteskMedium", - semiBold: "spaceGroteskSemiBold", - bold: "spaceGroteskBold", + light: "SpaceGrotesk-Light", + normal: "SpaceGrotesk-Regular", + medium: "SpaceGrotesk-Medium", + semiBold: "SpaceGrotesk-SemiBold", + bold: "SpaceGrotesk-Bold", }, helveticaNeue: { // iOS only font. diff --git a/boilerplate/assets/fonts/SpaceGrotesk-Bold.ttf b/boilerplate/assets/fonts/SpaceGrotesk-Bold.ttf new file mode 100644 index 000000000..8a8611a58 Binary files /dev/null and b/boilerplate/assets/fonts/SpaceGrotesk-Bold.ttf differ diff --git a/boilerplate/assets/fonts/SpaceGrotesk-Light.ttf b/boilerplate/assets/fonts/SpaceGrotesk-Light.ttf new file mode 100644 index 000000000..0f03f08b3 Binary files /dev/null and b/boilerplate/assets/fonts/SpaceGrotesk-Light.ttf differ diff --git a/boilerplate/assets/fonts/SpaceGrotesk-Medium.ttf b/boilerplate/assets/fonts/SpaceGrotesk-Medium.ttf new file mode 100644 index 000000000..e530cf836 Binary files /dev/null and b/boilerplate/assets/fonts/SpaceGrotesk-Medium.ttf differ diff --git a/boilerplate/assets/fonts/SpaceGrotesk-Regular.ttf b/boilerplate/assets/fonts/SpaceGrotesk-Regular.ttf new file mode 100644 index 000000000..8215f81e1 Binary files /dev/null and b/boilerplate/assets/fonts/SpaceGrotesk-Regular.ttf differ diff --git a/boilerplate/assets/fonts/SpaceGrotesk-SemiBold.ttf b/boilerplate/assets/fonts/SpaceGrotesk-SemiBold.ttf new file mode 100644 index 000000000..e05b9673a Binary files /dev/null and b/boilerplate/assets/fonts/SpaceGrotesk-SemiBold.ttf differ diff --git a/boilerplate/package.json b/boilerplate/package.json index 62617e2af..2c28f9b3e 100644 --- a/boilerplate/package.json +++ b/boilerplate/package.json @@ -31,7 +31,6 @@ "build:android:prod": "eas build --profile production --platform android --local" }, "dependencies": { - "@expo-google-fonts/space-grotesk": "^0.4.0", "@expo/metro-runtime": "~6.1.2", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", diff --git a/boilerplate/src/app/_layout.tsx b/boilerplate/src/app/_layout.tsx index 66b2b1750..7c52245c1 100644 --- a/boilerplate/src/app/_layout.tsx +++ b/boilerplate/src/app/_layout.tsx @@ -1,12 +1,11 @@ import { useEffect, useState } from "react" import { Slot, SplashScreen } from "expo-router" -import { useFonts } from "@expo-google-fonts/space-grotesk" import { KeyboardProvider } from "react-native-keyboard-controller" import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context" import { initI18n } from "@/i18n" import { ThemeProvider } from "@/theme/context" -import { customFontsToLoad } from "@/theme/typography" +import { useCustomFonts } from "@/theme/typography" import { loadDateFnsLocale } from "@/utils/formatDate" SplashScreen.preventAutoHideAsync() @@ -19,7 +18,8 @@ if (__DEV__) { } export default function Root() { - const [fontsLoaded, fontError] = useFonts(customFontsToLoad) + + const [areFontsLoaded, fontLoadError] = useCustomFonts() const [isI18nInitialized, setIsI18nInitialized] = useState(false) useEffect(() => { @@ -28,11 +28,11 @@ export default function Root() { .then(() => loadDateFnsLocale()) }, []) - const loaded = fontsLoaded && isI18nInitialized + const loaded = areFontsLoaded && isI18nInitialized useEffect(() => { - if (fontError) throw fontError - }, [fontError]) + if (fontLoadError ) throw fontLoadError + }, [fontLoadError]) useEffect(() => { if (loaded) {