1// @ts-ignore: uses flow 2import normalizeColor from '@react-native/normalize-color'; 3// @ts-ignore 4import Debug from 'debug'; 5import { ExpoConfig } from 'expo/config'; 6import { 7 ConfigPlugin, 8 createRunOncePlugin, 9 AndroidConfig, 10 withStringsXml, 11 WarningAggregator, 12 withAndroidColors, 13 withAndroidStyles, 14} from 'expo/config-plugins'; 15import { 16 NavigationBarVisibility, 17 NavigationBarBehavior, 18 NavigationBarPosition, 19 NavigationBarButtonStyle, 20} from 'expo-navigation-bar'; 21 22const debug = Debug('expo:system-navigation-bar:plugin'); 23 24const pkg = require('expo-navigation-bar/package.json'); 25 26export type Props = { 27 borderColor?: string; 28 backgroundColor?: string | null; 29 barStyle?: NavigationBarButtonStyle | null; 30 visibility?: NavigationBarVisibility; 31 behavior?: NavigationBarBehavior; 32 position?: NavigationBarPosition; 33 legacyVisible?: NonNullable<NonNullable<ExpoConfig['androidNavigationBar']>['visible']>; 34}; 35 36// strings.xml keys, this should not change. 37const BORDER_COLOR_KEY = 'expo_navigation_bar_border_color'; 38const VISIBILITY_KEY = 'expo_navigation_bar_visibility'; 39const POSITION_KEY = 'expo_navigation_bar_position'; 40const BEHAVIOR_KEY = 'expo_navigation_bar_behavior'; 41const LEGACY_VISIBLE_KEY = 'expo_navigation_bar_legacy_visible'; 42 43// styles.xml value 44const NAVIGATION_BAR_COLOR = 'navigationBarColor'; 45 46const LEGACY_BAR_STYLE_MAP: Record< 47 NonNullable<NonNullable<ExpoConfig['androidNavigationBar']>['barStyle']>, 48 NavigationBarButtonStyle 49> = { 50 // Match expo-status-bar 51 'dark-content': 'dark', 52 'light-content': 'light', 53}; 54 55function convertColorAndroid(input: string): number { 56 let color = normalizeColor(input); 57 if (!color) { 58 throw new Error('Invalid color value: ' + input); 59 } 60 color = ((color << 24) | (color >>> 8)) >>> 0; 61 62 // Android use 32 bit *signed* integer to represent the color 63 // We utilize the fact that bitwise operations in JS also operates on 64 // signed 32 bit integers, so that we can use those to convert from 65 // *unsigned* to *signed* 32bit int that way. 66 return color | 0x0; 67} 68 69export function resolveProps( 70 config: Pick<ExpoConfig, 'androidNavigationBar'>, 71 _props: Props | void 72): Props { 73 let props: Props; 74 if (!_props) { 75 props = { 76 backgroundColor: config.androidNavigationBar?.backgroundColor, 77 barStyle: config.androidNavigationBar?.barStyle 78 ? LEGACY_BAR_STYLE_MAP[config.androidNavigationBar?.barStyle] 79 : undefined, 80 // Resources for: 81 // - sticky-immersive: https://youtu.be/cBi8fjv90E4?t=416 -- https://developer.android.com/training/system-ui/immersive#sticky-immersive 82 // - immersive: https://youtu.be/cBi8fjv90E4?t=168 -- https://developer.android.com/training/system-ui/immersive#immersive 83 // - leanback: https://developer.android.com/training/system-ui/immersive#leanback 84 legacyVisible: config.androidNavigationBar?.visible, 85 }; 86 if (props.legacyVisible) { 87 // Using legacyVisible can break the setPositionAsync method: 88 // https://developer.android.com/reference/androidx/core/view/WindowCompat#setDecorFitsSystemWindows(android.view.Window,%20boolean) 89 WarningAggregator.addWarningAndroid( 90 'androidNavigationBar.visible', 91 'property is deprecated in Android 11 (API 30) and will be removed from Expo SDK', 92 'https://expo.fyi/android-navigation-bar-visible-deprecated' 93 ); 94 } 95 } else { 96 props = _props; 97 } 98 return props; 99} 100 101/** 102 * Ensure the Expo Go manifest is updated when the project is using config plugin properties instead 103 * of the static values that Expo Go reads from (`androidNavigationBar`). 104 */ 105export const withAndroidNavigationBarExpoGoManifest: ConfigPlugin<Props> = (config, props) => { 106 if (!config.androidNavigationBar) { 107 // Remap the config plugin props so Expo Go knows how to apply them. 108 config.androidNavigationBar = { 109 backgroundColor: props.backgroundColor ?? undefined, 110 barStyle: Object.entries(LEGACY_BAR_STYLE_MAP).find( 111 ([, v]) => v === props.barStyle 112 )?.[0] as keyof typeof LEGACY_BAR_STYLE_MAP, 113 visible: props.legacyVisible, 114 }; 115 } 116 return config; 117}; 118 119const withNavigationBar: ConfigPlugin<Props | void> = (config, _props) => { 120 const props = resolveProps(config, _props); 121 122 config = withAndroidNavigationBarExpoGoManifest(config, props); 123 124 debug('Props:', props); 125 126 // TODO: Add this to expo/config-plugins 127 // Elevate props to a static value on extra so Expo Go can read it. 128 if (!config.extra) { 129 config.extra = {}; 130 } 131 config.extra[pkg.name] = props; 132 133 // Use built-in styles instead of Expo custom properties, this makes the project hopefully a bit more predictable for bare users. 134 config = withNavigationBarColors(config, props); 135 config = withNavigationBarStyles(config, props); 136 137 return withStringsXml(config, (config) => { 138 config.modResults = setStrings(config.modResults, props); 139 return config; 140 }); 141}; 142 143export function setStrings( 144 strings: AndroidConfig.Resources.ResourceXML, 145 { 146 borderColor, 147 visibility, 148 position, 149 behavior, 150 legacyVisible, 151 }: Omit<Props, 'backgroundColor' | 'barStyle'> 152): AndroidConfig.Resources.ResourceXML { 153 const pairs = [ 154 [BORDER_COLOR_KEY, borderColor ? convertColorAndroid(borderColor) : null], 155 [VISIBILITY_KEY, visibility], 156 [POSITION_KEY, position], 157 [BEHAVIOR_KEY, behavior], 158 [LEGACY_VISIBLE_KEY, legacyVisible], 159 ] as [string, any][]; 160 161 const stringItems: AndroidConfig.Resources.ResourceItemXML[] = []; 162 for (const [key, value] of pairs) { 163 if (value == null) { 164 // Since we're using custom strings, we can remove them for convenience between prebuilds. 165 strings = AndroidConfig.Strings.removeStringItem(key, strings); 166 } else { 167 stringItems.push( 168 AndroidConfig.Resources.buildResourceItem({ 169 name: key, 170 value: String(value), 171 translatable: false, 172 }) 173 ); 174 } 175 } 176 177 return AndroidConfig.Strings.setStringItem(stringItems, strings); 178} 179 180const withNavigationBarColors: ConfigPlugin<Pick<Props, 'backgroundColor'>> = (config, props) => { 181 return withAndroidColors(config, (config) => { 182 config.modResults = setNavigationBarColors(props, config.modResults); 183 return config; 184 }); 185}; 186 187const withNavigationBarStyles: ConfigPlugin<Pick<Props, 'backgroundColor' | 'barStyle'>> = ( 188 config, 189 props 190) => { 191 return withAndroidStyles(config, (config) => { 192 config.modResults = setNavigationBarStyles(props, config.modResults); 193 return config; 194 }); 195}; 196 197export function setNavigationBarColors( 198 { backgroundColor }: Pick<Props, 'backgroundColor'>, 199 colors: AndroidConfig.Resources.ResourceXML 200): AndroidConfig.Resources.ResourceXML { 201 if (backgroundColor) { 202 colors = AndroidConfig.Colors.setColorItem( 203 AndroidConfig.Resources.buildResourceItem({ 204 name: NAVIGATION_BAR_COLOR, 205 value: backgroundColor, 206 }), 207 colors 208 ); 209 } 210 return colors; 211} 212 213export function setNavigationBarStyles( 214 { backgroundColor, barStyle }: Pick<Props, 'backgroundColor' | 'barStyle'>, 215 styles: AndroidConfig.Resources.ResourceXML 216): AndroidConfig.Resources.ResourceXML { 217 styles = AndroidConfig.Styles.assignStylesValue(styles, { 218 add: !!backgroundColor, 219 parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(), 220 name: `android:${NAVIGATION_BAR_COLOR}`, 221 value: `@color/${NAVIGATION_BAR_COLOR}`, 222 }); 223 224 styles = AndroidConfig.Styles.assignStylesValue(styles, { 225 // Adding means the buttons will be darker to account for a light background color. 226 // `setButtonStyleAsync('dark')` should do the same thing. 227 add: barStyle === 'dark', 228 parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(), 229 name: 'android:windowLightNavigationBar', 230 value: 'true', 231 }); 232 233 return styles; 234} 235 236export default createRunOncePlugin(withNavigationBar, pkg.name, pkg.version); 237