Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/actions/setup/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ runs:
- name: 🏗 Setup Node
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
cache: yarn
- name: 🏗 Setup Expo
uses: expo/expo-github-action@v7
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ android

# Ignore changes to the package-lock
package-lock.json
.yarn/install-state.gz
.yarnrc.yml
6 changes: 3 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {TabNavigatorParamList} from 'routes';
import {colorLookup} from 'theme';
import {AvalancheCenterID, AvalancheCenterWebsites} from 'types/nationalAvalancheCenter';

require('date-time-format-timezone');
import 'date-time-format-timezone';

import axios, {AxiosRequestConfig} from 'axios';
import {QUERY_CACHE_ASYNC_STORAGE_KEY} from 'data/asyncStorageKeys';
Expand Down Expand Up @@ -357,7 +357,7 @@ const BaseApp: React.FunctionComponent<{
Lato_900Black,
Lato_900Black_Italic,
// typing the return of `require()` here does nothing for us
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports
NAC_Icons: require('./assets/fonts/nac-icons.ttf'),
});

Expand Down Expand Up @@ -445,7 +445,7 @@ const BaseApp: React.FunctionComponent<{
height: '100%',
resizeMode: Constants.expoConfig?.splash?.resizeMode || 'contain',
}}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports
source={require('./assets/splash.png')}
/>
<Center style={{position: 'absolute', top: 0, bottom: 0, left: 0, right: 0}}>
Expand Down
59 changes: 34 additions & 25 deletions Preferences.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import {renderHook} from '@testing-library/react-hooks';
import {waitFor} from '@testing-library/react-native';

import {PREFERENCES_KEY} from 'data/asyncStorageKeys';
import {PreferencesProvider, resetPreferencesForTests, usePreferences} from 'Preferences';

// Mock out AsyncStorage for tests
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-require-imports
jest.mock('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage/jest/async-storage-mock'));

describe('Preferences', () => {
Expand All @@ -21,11 +22,12 @@ describe('Preferences', () => {

it('updates after preferences are loaded', async () => {
await AsyncStorage.setItem(PREFERENCES_KEY, JSON.stringify({center: 'BAC', hasSeenCenterPicker: true}));
const {result, waitForNextUpdate} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
const {result} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
expect(result.current.preferences.center).toEqual('NWAC');

await waitForNextUpdate();
expect(result.current.preferences.center).toEqual('BAC');
await waitFor(() => {
expect(result.current.preferences.center).toEqual('BAC');
});
});

it('falls back to defaults if preferences cannot be parsed', async () => {
Expand All @@ -48,61 +50,68 @@ describe('Preferences', () => {

it('updates to a default value after loading', async () => {
await AsyncStorage.setItem(PREFERENCES_KEY, JSON.stringify({center: 'BAC', hasSeenCenterPicker: true}));
const {result, waitForNextUpdate} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
const {result} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
expect(result.current.preferences.mixpanelUserId).toBeUndefined();

await waitForNextUpdate();
expect(result.current.preferences.mixpanelUserId).toMatch(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/);
await waitFor(() => {
expect(result.current.preferences.mixpanelUserId).toMatch(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/);
});
});

it('does not overwrite a previously saved id', async () => {
await AsyncStorage.setItem(PREFERENCES_KEY, JSON.stringify({center: 'BAC', hasSeenCenterPicker: true, mixpanelUserId: '00000000-0000-0000-0000-000000000000'}));
const {result, waitForNextUpdate} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
const {result} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
expect(result.current.preferences.mixpanelUserId).toBeUndefined();

await waitForNextUpdate();
expect(result.current.preferences.mixpanelUserId).toEqual('00000000-0000-0000-0000-000000000000');
await waitFor(() => {
expect(result.current.preferences.mixpanelUserId).toEqual('00000000-0000-0000-0000-000000000000');
});
});

it('is preserved when clearPreferences is called', async () => {
const userId = 'CE998943-7231-42C4-A22F-24845B2CF567';
await AsyncStorage.setItem(PREFERENCES_KEY, JSON.stringify({center: 'BAC', hasSeenCenterPicker: true, mixpanelUserId: userId}));

// Render the hook to load the preferences. First we'll see the default value, then the saved value
const {result, waitForNextUpdate} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
const {result} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
expect(result.current.preferences.mixpanelUserId).toBeUndefined();
await waitForNextUpdate();
expect(result.current.preferences.center).toEqual('BAC');
expect(result.current.preferences.mixpanelUserId).toEqual(userId);
await waitFor(() => {
expect(result.current.preferences.center).toEqual('BAC');
expect(result.current.preferences.mixpanelUserId).toEqual(userId);
});

// Now clear the preferences. This is not async - we clear them in memory immediately, and lazily persist the change.
result.current.clearPreferences();

// After clearing, the center is reset to the default, but the userId is preserved
expect(result.current.preferences.center).toEqual('NWAC');
expect(result.current.preferences.mixpanelUserId).toEqual(userId);
await waitFor(() => {
// After clearing, the center is reset to the default, but the userId is preserved
expect(result.current.preferences.center).toEqual('NWAC');
expect(result.current.preferences.mixpanelUserId).toEqual(userId);
});
});

it('is lost when preferences are damaged', async () => {
const userId = 'CE998943-7231-42C4-A22F-24845B2CF567';
await AsyncStorage.setItem(PREFERENCES_KEY, JSON.stringify({mixpanelUserId: userId, center: 'this is not a valid center'}));

// Render the hook to load the preferences. First we'll see the default value, then the saved value
const {result, waitForNextUpdate} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
const {result} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
expect(result.current.preferences.mixpanelUserId).toBeUndefined();
await waitForNextUpdate();
// The center is invalid, so preferences parsing will fail. The previous user id is lost, and we get a different UUID in its place.
expect(result.current.preferences.mixpanelUserId).toMatch(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/);
expect(result.current.preferences.mixpanelUserId).not.toEqual(userId);
await waitFor(() => {
// The center is invalid, so preferences parsing will fail. The previous user id is lost, and we get a different UUID in its place.
expect(result.current.preferences.mixpanelUserId).toMatch(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/);
expect(result.current.preferences.mixpanelUserId).not.toEqual(userId);
});
});

it('does overwrite an invalid id', async () => {
await AsyncStorage.setItem(PREFERENCES_KEY, JSON.stringify({center: 'BAC', hasSeenCenterPicker: true, mixpanelUserId: 'not a uuid'}));
const {result, waitForNextUpdate} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
const {result} = renderHook(() => usePreferences(), {wrapper: PreferencesProvider});
expect(result.current.preferences.mixpanelUserId).toBeUndefined();

await waitForNextUpdate();
expect(result.current.preferences.mixpanelUserId).toMatch(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/);
await waitFor(() => {
expect(result.current.preferences.mixpanelUserId).toMatch(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/);
});
});
});

Expand Down
4 changes: 1 addition & 3 deletions clientContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,5 @@ export const stagingHosts = {
export const ClientContext: Context<ClientProps> = React.createContext<ClientProps>({
...productionHosts,
requestedTime: 'latest',
setRequestedTime: () => {
undefined;
},
setRequestedTime: () => {},
});
5 changes: 2 additions & 3 deletions components/AvalancheCenterLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export interface AvalancheCenterLogoProps {
}

export const AvalancheCenterLogo: React.FunctionComponent<AvalancheCenterLogoProps> = ({style, avalancheCenterId}: AvalancheCenterLogoProps) => {
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-require-imports */

const source: Record<AvalancheCenterID, ImageResolvedAssetSource> = {
['BAC']: Image.resolveAssetSource(require('../assets/logos/BAC.png')),
['BTAC']: Image.resolveAssetSource(require('../assets/logos/BTAC.png')),
Expand Down Expand Up @@ -122,7 +122,6 @@ export const AvalancheCenterLogo: React.FunctionComponent<AvalancheCenterLogoPro
};

export const preloadAvalancheCenterLogo = async (queryClient: QueryClient, logger: Logger, avalancheCenter: AvalancheCenterID) => {
/* eslint-disable @typescript-eslint/no-var-requires */
switch (avalancheCenter) {
case 'BAC':
return ImageCache.prefetch(queryClient, logger, Image.resolveAssetSource(require('../assets/logos/BAC.png')).uri);
Expand Down
6 changes: 2 additions & 4 deletions components/AvalancheDangerIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface AvalancheDangerIconProps {
}

const icons: Record<DangerLevel, ImageSourcePropType> = {
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports */
[DangerLevel.GeneralInformation]: require('../assets/danger-icons/0.png'),
[DangerLevel.None]: require('../assets/danger-icons/0.png'),
[DangerLevel.Low]: require('../assets/danger-icons/1.png'),
Expand All @@ -23,8 +23,7 @@ const icons: Record<DangerLevel, ImageSourcePropType> = {
};

const sizes: Record<DangerLevel, ImageResolvedAssetSource> = {
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-require-imports */
[DangerLevel.GeneralInformation]: Image.resolveAssetSource(require('../assets/danger-icons/0.png')),
[DangerLevel.None]: Image.resolveAssetSource(require('../assets/danger-icons/0.png')),
[DangerLevel.Low]: Image.resolveAssetSource(require('../assets/danger-icons/1.png')),
Expand Down Expand Up @@ -57,7 +56,6 @@ export const AvalancheDangerIcon: React.FunctionComponent<AvalancheDangerIconPro
};

export const preloadAvalancheDangerIcons = async (queryClient: QueryClient, logger: Logger) => {
/* eslint-disable @typescript-eslint/no-var-requires */
return Promise.all([
ImageCache.prefetch(queryClient, logger, Image.resolveAssetSource(require('../assets/danger-icons/0.png')).uri),
ImageCache.prefetch(queryClient, logger, Image.resolveAssetSource(require('../assets/danger-icons/1.png')).uri),
Expand Down
5 changes: 3 additions & 2 deletions components/AvalancheForecastZoneMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({cen

// useRef has to be used here. Animation and gesture handlers can't use props and state,
// and aren't re-evaluated on render. Fun!
const mapView = useRef<AnimatedMapView>(null);
const mapView = useRef<AnimatedMapView | null>(null);
const controller = useRef<AnimatedMapWithDrawerController>(new AnimatedMapWithDrawerController(AnimatedDrawerState.Hidden, avalancheCenterMapRegion, mapView, logger)).current;
React.useEffect(() => {
controller.animateUsingUpdatedAvalancheCenterMapRegion(avalancheCenterMapRegion);
Expand Down Expand Up @@ -184,7 +184,7 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({cen
.map(result => result.data) // get data from the results
.filter(data => data) // only operate on results that have succeeded
.forEach(forecast => {
forecast &&
if (forecast && forecast.forecast_zone) {
forecast.forecast_zone?.forEach(({id}) => {
if (zonesById[id]) {
// the map layer will expose old forecasts with their danger level as appropriate, but the map expects to show a card
Expand Down Expand Up @@ -219,6 +219,7 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({cen
}
}
});
}
});
warningResults
.map(result => result.data) // get data from the results
Expand Down
7 changes: 3 additions & 4 deletions components/AvalancheProblemIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface AvalancheProblemIconProps {
}

const icons: Record<AvalancheProblemType, ImageSourcePropType> = {
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports */
[AvalancheProblemType.DryLoose]: require('../assets/problem-icons/DryLoose.png'),
[AvalancheProblemType.StormSlab]: require('../assets/problem-icons/StormSlab.png'),
[AvalancheProblemType.WindSlab]: require('../assets/problem-icons/WindSlab.png'),
Expand All @@ -24,8 +24,8 @@ const icons: Record<AvalancheProblemType, ImageSourcePropType> = {
};

const sizes: Record<AvalancheProblemType, ImageResolvedAssetSource> = {
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-require-imports */

[AvalancheProblemType.DryLoose]: Image.resolveAssetSource(require('../assets/problem-icons/DryLoose.png')),
[AvalancheProblemType.StormSlab]: Image.resolveAssetSource(require('../assets/problem-icons/StormSlab.png')),
[AvalancheProblemType.WindSlab]: Image.resolveAssetSource(require('../assets/problem-icons/WindSlab.png')),
Expand Down Expand Up @@ -55,7 +55,6 @@ export const AvalancheProblemIcon: React.FunctionComponent<AvalancheProblemIconP
};

export const preloadAvalancheProblemIcons = async (queryClient: QueryClient, logger: Logger) => {
/* eslint-disable @typescript-eslint/no-var-requires */
return Promise.all([
ImageCache.prefetch(queryClient, logger, Image.resolveAssetSource(require('../assets/problem-icons/DryLoose.png')).uri),
ImageCache.prefetch(queryClient, logger, Image.resolveAssetSource(require('../assets/problem-icons/StormSlab.png')).uri),
Expand Down
4 changes: 2 additions & 2 deletions components/content/NavigationHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {AntDesign, Entypo} from '@expo/vector-icons';
import {getHeaderTitle} from '@react-navigation/elements';
import {NativeStackHeaderProps} from '@react-navigation/native-stack/lib/typescript/src/types';
import {NativeStackHeaderProps} from '@react-navigation/native-stack';
import {HStack, View} from 'components/core';
import {GenerateObservationShareLink} from 'components/observations/ObservationUrlMapping';
import {Title1Black, Title3Black} from 'components/text';
Expand Down Expand Up @@ -28,7 +28,7 @@ export const NavigationHeader: React.FunctionComponent<
if (!back) {
firstOpen = true;
// set back to not be null since we want a shared obs to have a back button
back = {title: 'Observations'};
back = {title: 'Observations', href: undefined};
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to look into what href should be

}

shareCenterId = reverseLookup(AvalancheCenterWebsites, shareParams.share_url) as AvalancheCenterID;
Expand Down
2 changes: 1 addition & 1 deletion components/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export * from './Center';
export * from './Divider';
// Layout components
export * from './HStack';
export * from './VStack';
export * from './View';
export * from './VStack';
13 changes: 11 additions & 2 deletions components/form/CheckboxSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@ interface CheckboxSelectFieldProps extends ViewProps {
items: Item[];
radio?: boolean; // If true, will default to selecting first item and always enforce selection
disabled?: boolean;
labelComponent?: React.FunctionComponent<TextWrapperProps>;
labelComponent?: (props: TextWrapperProps) => React.ReactNode;
labelSpace?: number;
}

// This component renders a column of checkboxes for the given items
// It's an alternative to SelectField when you don't want a dropdown
export function CheckboxSelectField({name, label, items, disabled, labelComponent = BodyXSmBlack, labelSpace = 4, radio, ...props}: CheckboxSelectFieldProps) {
export function CheckboxSelectField({
name,
label,
items,
disabled,
labelComponent = BodyXSmBlack as (props: TextWrapperProps) => React.ReactNode,
labelSpace = 4,
radio,
...props
}: CheckboxSelectFieldProps) {
const {setValue} = useFormContext();
const {field} = useController({name});
const onChange = useCallback(
Expand Down
22 changes: 12 additions & 10 deletions components/map/AnimatedCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ export class AnimatedMapWithDrawerController {
topElementsHeight = 0;
cardDrawerMaximumHeight = 0;
tabBarHeight = 0;
mapView: RefObject<AnimatedMapView>;
mapView: RefObject<AnimatedMapView | null>;
// We store the last time we logged a region calculation so as to continue logging but not spam
lastLogged: Record<string, string>; // mapping hash of parameters to the time we last logged it

constructor(state = AnimatedDrawerState.Docked, region: Region, mapView: RefObject<AnimatedMapView>, logger: Logger) {
constructor(state = AnimatedDrawerState.Docked, region: Region, mapView: RefObject<AnimatedMapView | null>, logger: Logger) {
this.logger = logger;
this.state = state;
this.baseOffset = AnimatedMapWithDrawerController.OFFSETS[state];
Expand Down Expand Up @@ -475,14 +475,16 @@ export const AnimatedCards = <T, U>(props: AnimatedCardsProps<T, U>) => {
onMomentumScrollEnd={onMomentumScrollEnd}
{...panResponder.panHandlers}
{...flatListProps}
data={items.map(
(i: T): ItemRenderData<T, U> => ({
key: getItemId(i),
item: i,
date: date,
center_id: center_id,
}),
)}
data={
items.map(
(i: T): ItemRenderData<T, U> => ({
key: getItemId(i),
item: i,
date,
center_id,
}),
) as unknown as Animated.WithAnimatedObject<ArrayLike<ItemRenderData<T, U>>>
}
renderItem={renderItemAdapter}
/>
</Animated.View>
Expand Down
2 changes: 1 addition & 1 deletion components/observations/ObservationDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export const ObservationCard: React.FunctionComponent<{
longitude: observation.location_point.lng,
}}
anchor={{x: 0.5, y: 1}}>
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports */}
<Image source={require('assets/map-marker.png')} style={{width: 40, height: 40}} />
</Marker>
</ZoneMap>
Expand Down
Loading
Loading