Skip to content

Commit 4f5e190

Browse files
committed
feat(app): enforce minimum version
1 parent 1950be2 commit 4f5e190

14 files changed

Lines changed: 681 additions & 8 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const getApp = jest.fn(() => ({
2+
name: '[DEFAULT]',
3+
options: {},
4+
analytics: jest.fn(() => {}),
5+
inAppMessaging: jest.fn(() => {}),
6+
messaging: jest.fn(() => {}),
7+
perf: jest.fn(() => {}),
8+
}));
9+
10+
export const initializeApp = jest.fn(() => ({
11+
name: '[DEFAULT]',
12+
options: {},
13+
}));
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default jest.fn();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const getRemoteConfig = jest.fn(() => ({
2+
setDefaults: jest.fn().mockImplementation(() => Promise.resolve()),
3+
}));
4+
5+
export const setConfigSettings = jest.fn();
6+
export const fetchAndActivate = jest.fn().mockResolvedValue(true);
7+
export const getAll = jest.fn().mockReturnValue({});
8+
export const getValue = jest.fn();

app.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface AppVariantConfig {
1414
androidGoogleServicesFile: string;
1515
}
1616

17-
type Variant = 'development' | 'preview' | 'test' | 'production';
17+
export type Variant = 'development' | 'preview' | 'test' | 'production';
1818

1919
// https://docs.expo.dev/tutorial/eas/multiple-app-variants
2020
const APP_VARIANT_CONFIG: Record<Variant, AppVariantConfig> = {
@@ -64,7 +64,7 @@ const APP_VARIANT_CONFIG: Record<Variant, AppVariantConfig> = {
6464

6565
const variant = (process.env.APP_VARIANT as Variant) || 'production';
6666

67-
const VERSION = '0.0.35';
67+
const VERSION = '0.0.36';
6868

6969
const appConfig = APP_VARIANT_CONFIG[variant];
7070

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@
249249
"react-native-url-polyfill": "^2.0.0",
250250
"react-test-renderer": "19.1.0",
251251
"reassure": "^1.4.0",
252-
"semver": "^7.7.2",
253252
"sharp-cli": "^5.2.0",
254253
"svgo": "^4.0.0",
255254
"ts-jest": "^29.4.0",

src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import { useLayoutEffect } from 'react';
99
import { LogBox } from 'react-native';
1010
import { enableFreeze, enableScreens } from 'react-native-screens';
1111
import { Providers } from './Providers/Providers';
12+
import { ForceUpdateModal } from './components/ForceUpdateModal/ForceUpdateModal';
1213
import { OTAUpdates } from './components/OTAUpdates';
1314
import { useChangeScreenOrientation } from './hooks/useChangeScreenOrientation';
1415
import { useClearExpiredStorageItems } from './hooks/useClearExpiredStorageItems';
16+
import { useForceUpdate } from './hooks/useForceUpdate';
1517
import { useOnAppStateChange } from './hooks/useOnAppStateChange';
1618
import { useOnReconnect } from './hooks/useOnReconnect';
1719
import { useRecoveredFromError } from './hooks/useRecoveredFromError';
@@ -60,6 +62,8 @@ function App() {
6062
}
6163
});
6264

65+
const { updateRequired, minimumVersion } = useForceUpdate();
66+
6367
/**
6468
* Before we show the app, we have to wait for our state to be ready
6569
* In the meantime, don't render anything. This will be the background color set in
@@ -69,6 +73,10 @@ function App() {
6973
*/
7074
return (
7175
<Providers>
76+
<ForceUpdateModal
77+
isVisible={updateRequired}
78+
minimumVersion={minimumVersion}
79+
/>
7280
<AppNavigator
7381
onStateChange={onNavigationStateChange}
7482
onReady={() => {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { getStoreUrlAsync } from '@app/screens/DevTools/utils/getStoreUrlAsync';
2+
import { openLinkInBrowser } from '@app/utils/browser/openLinkInBrowser';
3+
import * as Application from 'expo-application';
4+
import { useState } from 'react';
5+
import { Modal as RNModal, View } from 'react-native';
6+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
7+
import { StyleSheet } from 'react-native-unistyles';
8+
import { Button } from '../Button';
9+
import { IconSymbol } from '../IconSymbol/IconSymbol';
10+
import { Typography } from '../Typography';
11+
12+
interface ForceUpdateModalProps {
13+
isVisible: boolean;
14+
minimumVersion: string;
15+
}
16+
17+
export function ForceUpdateModal({
18+
isVisible,
19+
minimumVersion,
20+
}: ForceUpdateModalProps) {
21+
const [isLoading, setIsLoading] = useState(false);
22+
const insets = useSafeAreaInsets();
23+
24+
const handleUpdatePress = async () => {
25+
setIsLoading(true);
26+
try {
27+
const storeUrl = await getStoreUrlAsync();
28+
if (storeUrl) {
29+
openLinkInBrowser(storeUrl);
30+
}
31+
} finally {
32+
setIsLoading(false);
33+
}
34+
};
35+
36+
const currentVersion = Application.nativeApplicationVersion ?? 'Unknown';
37+
38+
return (
39+
<RNModal
40+
animationType="fade"
41+
transparent
42+
visible={isVisible}
43+
statusBarTranslucent
44+
>
45+
<View style={[styles.overlay, { paddingTop: insets.top }]}>
46+
<View style={styles.card}>
47+
<View style={styles.iconContainer}>
48+
<IconSymbol name="arrow.up" />
49+
</View>
50+
51+
<Typography color="gray" size="xl" fontWeight="bold" align="center">
52+
Update Required
53+
</Typography>
54+
55+
<Typography
56+
color="gray.textLow"
57+
size="sm"
58+
align="left"
59+
style={styles.subtitle}
60+
>
61+
A new version of Foam is available. Please update to continue using
62+
the app.
63+
</Typography>
64+
65+
<View style={styles.versionInfo}>
66+
<View style={styles.versionRow}>
67+
<Typography color="gray.textLow" size="xs">
68+
Current version
69+
</Typography>
70+
<Typography color="gray" size="xs" fontWeight="semiBold">
71+
{currentVersion}
72+
</Typography>
73+
</View>
74+
<View style={styles.versionRow}>
75+
<Typography color="gray.textLow" size="xs">
76+
Minimum required
77+
</Typography>
78+
<Typography color="gray" size="xs" fontWeight="semiBold">
79+
{minimumVersion}
80+
</Typography>
81+
</View>
82+
</View>
83+
84+
<Button
85+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
86+
onPress={handleUpdatePress}
87+
style={styles.updateButton}
88+
disabled={isLoading}
89+
>
90+
<Typography color="accent" contrast size="md" fontWeight="semiBold">
91+
{isLoading ? 'Opening Store...' : 'Update Now'}
92+
</Typography>
93+
</Button>
94+
</View>
95+
</View>
96+
</RNModal>
97+
);
98+
}
99+
100+
const styles = StyleSheet.create(theme => ({
101+
overlay: {
102+
flex: 1,
103+
backgroundColor: 'rgba(0, 0, 0, 0.85)',
104+
justifyContent: 'center' as const,
105+
alignItems: 'center' as const,
106+
paddingHorizontal: theme.spacing.lg,
107+
},
108+
card: {
109+
backgroundColor: theme.colors.gray.bgAlt,
110+
borderRadius: theme.radii.lg,
111+
paddingHorizontal: theme.spacing.xl,
112+
paddingVertical: theme.spacing.xl,
113+
width: '100%',
114+
maxWidth: 340,
115+
alignItems: 'center' as const,
116+
borderWidth: 1,
117+
borderColor: theme.colors.gray.border,
118+
},
119+
iconContainer: {
120+
width: 72,
121+
height: 72,
122+
borderRadius: 36,
123+
backgroundColor: theme.colors.accent.ui,
124+
justifyContent: 'center' as const,
125+
alignItems: 'center' as const,
126+
marginBottom: theme.spacing.lg,
127+
},
128+
icon: {
129+
fontSize: 32,
130+
},
131+
subtitle: {
132+
marginBottom: theme.spacing.lg,
133+
marginTop: theme.spacing.sm,
134+
lineHeight: 20,
135+
},
136+
versionInfo: {
137+
width: '100%',
138+
backgroundColor: theme.colors.gray.ui,
139+
borderRadius: theme.radii.md,
140+
padding: theme.spacing.md,
141+
marginBottom: theme.spacing.lg,
142+
gap: theme.spacing.xs,
143+
},
144+
versionRow: {
145+
flexDirection: 'row' as const,
146+
justifyContent: 'space-between' as const,
147+
alignItems: 'center' as const,
148+
},
149+
updateButton: {
150+
width: '100%',
151+
backgroundColor: theme.colors.accent.accent,
152+
borderRadius: theme.radii.md,
153+
paddingVertical: theme.spacing.md,
154+
alignItems: 'center' as const,
155+
justifyContent: 'center' as const,
156+
},
157+
}));

0 commit comments

Comments
 (0)