Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
07c018e
chore(release): 11.2.1 [skip ci]
ChristopherGabba Oct 6, 2025
f7aca3f
fix: Remove `useFonts` and embed them as part of the App Binary for i…
ChristopherGabba Oct 6, 2025
104c144
fix: prettier circle ci failure
ChristopherGabba Oct 6, 2025
f25a59a
fix: circle ci type error
ChristopherGabba Oct 6, 2025
0320212
feat: remove specific android font from config plugin, not needed
ChristopherGabba Oct 9, 2025
5bbf8dd
fix: new approach to load fonts for all platforms
ChristopherGabba Oct 15, 2025
2acb4c1
revert: version bump on accident
ChristopherGabba Oct 15, 2025
17ffdf2
fix: rename files and simplify imports
ChristopherGabba Oct 15, 2025
a5ae257
Merge branch 'master' into feat/precompile-fonts
ChristopherGabba Oct 15, 2025
5c97a05
fix: precompiled fonts on ios
ChristopherGabba Oct 16, 2025
2c98262
fix: using postscript name on fonts for consistency
ChristopherGabba Oct 16, 2025
d75e7a1
fix: removal of splash screen (accidentally copied over it)
ChristopherGabba Oct 16, 2025
13561b7
fix: comment discrepency
ChristopherGabba Oct 16, 2025
2f75af3
feat: SDK 56
frankcalise Jun 6, 2026
de6fc9f
docs: update readme versions
frankcalise Jun 6, 2026
169273e
feat(boilerplate): SDK 56 expo router version
frankcalise Jun 6, 2026
857e1b2
fix(cli): update lint package to match
frankcalise Jun 6, 2026
ffe1e6e
fix(boilerplate): rn jest dev dep
frankcalise Jun 7, 2026
19714eb
Merge upstream PR #3038: feat: SDK 56
fikretsengul Jul 2, 2026
fe135e4
Merge upstream PR #3003: precompile fonts
fikretsengul Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ Nothing makes it into Ignite unless it's been proven on projects that Infinite R

| Library | Category | Version | Description |
| -------------------------------- | -------------------- | ------- | ---------------------------------------------- |
| React Native | Mobile Framework | v0.81 | The best cross-platform mobile framework |
| React Native | Mobile Framework | v0.85 | The best cross-platform mobile framework |
| React | UI Framework | v19 | The most popular UI framework in the world |
| TypeScript | Language | v5 | Static typechecking |
| TypeScript | Language | v6 | Static typechecking |
| React Navigation | Navigation | v7 | Performant and consistent navigation framework |
| Expo | SDK | v55 | Allows (optional) Expo modules |
| Expo Font | Custom Fonts | v14 | Import custom fonts |
| Expo Localization | Internationalization | v17 | i18n support (including RTL!) |
| Expo | SDK | v56 | Allows (optional) Expo modules |
| Expo Font | Custom Fonts | v56 | Import custom fonts |
| Expo Localization | Internationalization | v56 | i18n support (including RTL!) |
| RN Reanimated | Animations | v4 | Beautiful and performant animations |
| MMKV | Persistence | v3 | State persistence |
| apisauce | REST client | v3 | Communicate with back-end |
Expand Down
29 changes: 28 additions & 1 deletion boilerplate/app.config.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,6 +34,7 @@ import "tsx/cjs"
*/
module.exports = ({ config }: ConfigContext): Partial<ExpoConfig> => {
const existingPlugins = config.plugins ?? []
const fontPaths = getFontPathsFromAssetsFolder()

return {
...config,
Expand All @@ -36,6 +55,14 @@ module.exports = ({ config }: ConfigContext): Partial<ExpoConfig> => {
],
},
},
plugins: [...existingPlugins],
plugins: [
...existingPlugins,
[
"expo-font",
{
fonts: fontPaths,
},
],
],
}
}
6 changes: 1 addition & 5 deletions boilerplate/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
"updates": {
"fallbackToCacheTimeout": 0
},
"newArchEnabled": true,
"jsEngine": "hermes",
"assetBundlePatterns": [
"**/*"
],
Expand All @@ -21,8 +19,7 @@
"foregroundImage": "./assets/images/app-icon-android-adaptive-foreground.png",
"backgroundImage": "./assets/images/app-icon-android-adaptive-background.png"
},
"allowBackup": false,
"edgeToEdgeEnabled": true
"allowBackup": false
},
"ios": {
"icon": "./assets/images/app-icon-ios.png",
Expand All @@ -35,7 +32,6 @@
},
"plugins": [
"expo-localization",
"expo-font",
[
"expo-splash-screen",
{
Expand Down
8 changes: 3 additions & 5 deletions boilerplate/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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(() => {
Expand All @@ -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
}

Expand Down
41 changes: 12 additions & 29 deletions boilerplate/app/components/Screen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useRef, useState } from "react"
import { ReactNode, useMemo, useRef, useState } from "react"
import {
KeyboardAvoidingView,
KeyboardAvoidingViewProps,
Expand Down Expand Up @@ -117,37 +117,24 @@ function useAutoPreset(props: AutoScreenProps): {
const { preset, scrollEnabledToggleThreshold } = props
const { percent = 0.92, point = 0 } = scrollEnabledToggleThreshold || {}

const scrollViewHeight = useRef<null | number>(null)
const scrollViewContentHeight = useRef<null | number>(null)
const [scrollEnabled, setScrollEnabled] = useState(true)
const [scrollViewHeight, setScrollViewHeight] = useState<number | null>(null)
const [scrollViewContentHeight, setScrollViewContentHeight] = useState<number | null>(null)

function updateScrollState() {
if (scrollViewHeight.current === null || scrollViewContentHeight.current === null) return

// check whether content fits the screen then toggle scroll state according to it
const contentFitsScreen = (function () {
if (point) {
return scrollViewContentHeight.current < scrollViewHeight.current - point
} else {
return scrollViewContentHeight.current < scrollViewHeight.current * percent
}
})()

// content is less than the size of the screen, so we can disable scrolling
if (scrollEnabled && contentFitsScreen) setScrollEnabled(false)

// content is greater than the size of the screen, so let's enable scrolling
if (!scrollEnabled && !contentFitsScreen) setScrollEnabled(true)
}
const scrollEnabled = useMemo(() => {
if (scrollViewHeight === null || scrollViewContentHeight === null) return true
const contentFitsScreen = point
? scrollViewContentHeight < scrollViewHeight - point
: scrollViewContentHeight < scrollViewHeight * percent
return !contentFitsScreen
}, [scrollViewHeight, scrollViewContentHeight, point, percent])

/**
* @param {number} w - The width of the content.
* @param {number} h - The height of the content.
*/
function onContentSizeChange(w: number, h: number) {
// update scroll-view content height
scrollViewContentHeight.current = h
updateScrollState()
setScrollViewContentHeight(h)
}

/**
Expand All @@ -156,13 +143,9 @@ function useAutoPreset(props: AutoScreenProps): {
function onLayout(e: LayoutChangeEvent) {
const { height } = e.nativeEvent.layout
// update scroll-view height
scrollViewHeight.current = height
updateScrollState()
setScrollViewHeight(height)
}

// update scroll state on every render
if (preset === "auto") updateScrollState()

return {
scrollEnabled: preset === "auto" ? scrollEnabled : true,
onContentSizeChange,
Expand Down
10 changes: 5 additions & 5 deletions boilerplate/app/components/Toggle/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback } from "react"
import { useEffect, useCallback, useState } from "react"
import { Image, ImageStyle, Animated, StyleProp, View, ViewStyle } from "react-native"

import { useAppTheme } from "@/theme/context"
Expand Down Expand Up @@ -50,15 +50,15 @@ function CheckboxInput(props: CheckboxInputProps) {
theme: { colors },
} = useAppTheme()

const opacity = useRef(new Animated.Value(0))
const [opacity] = useState(() => new Animated.Value(0))

useEffect(() => {
Animated.timing(opacity.current, {
Animated.timing(opacity, {
toValue: on ? 1 : 0,
duration: 300,
useNativeDriver: true,
}).start()
}, [on])
}, [on, opacity])

const offBackgroundColor = [
disabled && colors.palette.neutral400,
Expand Down Expand Up @@ -98,7 +98,7 @@ function CheckboxInput(props: CheckboxInputProps) {
$styles.toggleInner,
{ backgroundColor: onBackgroundColor },
$innerStyleOverride,
{ opacity: opacity.current },
{ opacity },
]}
>
<Image
Expand Down
10 changes: 5 additions & 5 deletions boilerplate/app/components/Toggle/Radio.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"
import { useEffect, useState } from "react"
import { StyleProp, View, ViewStyle, Animated } from "react-native"

import { useAppTheme } from "@/theme/context"
Expand Down Expand Up @@ -38,15 +38,15 @@ function RadioInput(props: RadioInputProps) {
theme: { colors },
} = useAppTheme()

const opacity = useRef(new Animated.Value(0))
const [opacity] = useState(() => new Animated.Value(0))

useEffect(() => {
Animated.timing(opacity.current, {
Animated.timing(opacity, {
toValue: on ? 1 : 0,
duration: 300,
useNativeDriver: true,
}).start()
}, [on])
}, [on, opacity])

const offBackgroundColor = [
disabled && colors.palette.neutral400,
Expand Down Expand Up @@ -86,7 +86,7 @@ function RadioInput(props: RadioInputProps) {
$styles.toggleInner,
{ backgroundColor: onBackgroundColor },
$innerStyleOverride,
{ opacity: opacity.current },
{ opacity },
]}
>
<View
Expand Down
20 changes: 10 additions & 10 deletions boilerplate/app/components/Toggle/Switch.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useEffect, useMemo, useRef, useCallback } from "react"
import { useEffect, useMemo, useCallback, useState } from "react"
import { Animated, Image, ImageStyle, Platform, StyleProp, View, ViewStyle } from "react-native"

import { iconRegistry } from "@/components/Icon"
import { isRTL } from "@/i18n"
import type { ThemedStyle } from "@/theme/types"
import { useAppTheme } from "@/theme/context"
import { $styles } from "@/theme/styles"
import type { ThemedStyle } from "@/theme/types"

import { $inputOuterBase, BaseToggleInputProps, Toggle, ToggleProps } from "./Toggle"

Expand Down Expand Up @@ -56,24 +56,24 @@ function SwitchInput(props: SwitchInputProps) {
themed,
} = useAppTheme()

const animate = useRef(new Animated.Value(on ? 1 : 0)) // Initial value is set based on isActive
const opacity = useRef(new Animated.Value(0))
const [animate] = useState(() => new Animated.Value(on ? 1 : 0)) // Initial value is set based on isActive
const [opacity] = useState(() => new Animated.Value(0))

useEffect(() => {
Animated.timing(animate.current, {
Animated.timing(animate, {
toValue: on ? 1 : 0,
duration: 300,
useNativeDriver: true, // Enable native driver for smoother animations
}).start()
}, [on])
}, [animate, on])

useEffect(() => {
Animated.timing(opacity.current, {
Animated.timing(opacity, {
toValue: on ? 1 : 0,
duration: 300,
useNativeDriver: true,
}).start()
}, [on])
}, [opacity, on])

const knobSizeFallback = 2

Expand Down Expand Up @@ -137,7 +137,7 @@ function SwitchInput(props: SwitchInputProps) {
: [offsetLeft, +(knobWidth || 0) + offsetRight]
: [rtlAdjustment * offsetLeft, rtlAdjustment * (+(knobWidth || 0) + offsetRight)]

const $animatedSwitchKnob = animate.current.interpolate({
const $animatedSwitchKnob = animate.interpolate({
inputRange: [0, 1],
outputRange,
})
Expand All @@ -149,7 +149,7 @@ function SwitchInput(props: SwitchInputProps) {
$themedSwitchInner,
{ backgroundColor: onBackgroundColor },
$innerStyleOverride,
{ opacity: opacity.current },
{ opacity },
]}
/>

Expand Down
12 changes: 6 additions & 6 deletions boilerplate/app/screens/DemoPodcastListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import { isRTL } from "@/i18n"
import { translate } from "@/i18n/translate"
import { DemoTabScreenProps } from "@/navigators/navigationTypes"
import type { EpisodeItem } from "@/services/api/types"
import type { ThemedStyle } from "@/theme/types"
import { useAppTheme } from "@/theme/context"
import { $styles } from "@/theme/styles"
import type { ThemedStyle } from "@/theme/types"
import { delay } from "@/utils/delay"
import { openLinkInBrowser } from "@/utils/openLinkInBrowser"

Expand Down Expand Up @@ -146,9 +146,9 @@ const EpisodeCard = ({
const { isFavorite, datePublished, duration, parsedTitleAndSubtitle } = useEpisode(episode)

const liked = useSharedValue(isFavorite ? 1 : 0)
const imageUri = useMemo<ImageSourcePropType>(() => {
return rnrImages[Math.floor(Math.random() * rnrImages.length)]
}, [])
const [imageUri] = useState<ImageSourcePropType>(
() => rnrImages[Math.floor(Math.random() * rnrImages.length)],
)

// Grey heart
const animatedLikeButtonStyles = useAnimatedStyle(() => {
Expand Down Expand Up @@ -176,8 +176,8 @@ const EpisodeCard = ({

const handlePressFavorite = useCallback(() => {
onPressFavorite()
liked.value = withSpring(liked.value ? 0 : 1)
}, [liked, onPressFavorite])
liked.set((prev) => withSpring(prev ? 0 : 1))
}, [onPressFavorite, liked])

/**
* Android has a "longpress" accessibility action. iOS does not, so we just have to use a hint.
Expand Down
3 changes: 1 addition & 2 deletions boilerplate/app/screens/LoginScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface LoginScreenProps extends AppStackScreenProps<"Login"> {}
export const LoginScreen: FC<LoginScreenProps> = () => {
const authPasswordInput = useRef<TextInput>(null)

const [authPassword, setAuthPassword] = useState("")
const [authPassword, setAuthPassword] = useState("ign1teIsAwes0m3")
const [isAuthPasswordHidden, setIsAuthPasswordHidden] = useState(true)
const [isSubmitted, setIsSubmitted] = useState(false)
const [attemptsCount, setAttemptsCount] = useState(0)
Expand All @@ -32,7 +32,6 @@ export const LoginScreen: FC<LoginScreenProps> = () => {
// Here is where you could fetch credentials from keychain or storage
// and pre-fill the form fields.
setAuthEmail("ignite@infinite.red")
setAuthPassword("ign1teIsAwes0m3")
}, [setAuthEmail])

const error = isSubmitted ? validationError : ""
Expand Down
Loading