Skip to content

Commit

Permalink
feat: misc screens UI and logic updates (#361)
Browse files Browse the repository at this point in the history
* feat: misc screens UI and logic updates

* chore: add translation to update cta

* test: fix tests due to component updates
  • Loading branch information
tsyirvo authored Dec 26, 2023
1 parent 793a899 commit b544f7d
Show file tree
Hide file tree
Showing 15 changed files with 325 additions and 14 deletions.
4 changes: 4 additions & 0 deletions __mocks__/react-native-device-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
getBundleId: jest.fn(),
getVersion: jest.fn(),
};
3 changes: 3 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const plugins: ExpoConfig['plugins'] = [
{
ios: {
flipper: isDevelopmentEnv,
infoPlist: {
LSApplicationQueriesSchemes: ['itms-apps'],
},
},
},
],
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"react-dom": "18.2.0",
"react-i18next": "14.0.0",
"react-native": "0.72.6",
"react-native-device-info": "10.12.0",
"react-native-flipper": "0.212.0",
"react-native-gesture-handler": "~2.12.0",
"react-native-keyboard-aware-scroll-view": "0.9.5",
Expand All @@ -109,6 +110,7 @@
"react-native-toast-message": "2.2.0",
"semver": "7.5.4",
"sentry-expo": "~7.1.0",
"sp-react-native-in-app-updates": "1.3.1",
"zod": "3.22.4"
},
"devDependencies": {
Expand Down Expand Up @@ -180,7 +182,8 @@
"@testing-library/jest-native/extend-expect"
],
"setupFiles": [
"./node_modules/react-native-gesture-handler/jestSetup.js"
"./node_modules/react-native-gesture-handler/jestSetup.js",
"./src/core/testing/setup.ts"
],
"testPathIgnorePatterns": [
"<rootDir>/node_modules/",
Expand Down
3 changes: 3 additions & 0 deletions src/core/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const env = Env.APP_ENV;
const version = Env.VERSION;
const buildNumber = Constants.expoConfig?.ios?.buildNumber;
const runtimeVersion = Constants.expoConfig?.runtimeVersion;
const androidPackageName = Constants.expoConfig?.android?.package;
const sentryDsn = Env.SENTRY_DSN;
const mixpanelToken = Env.MIXPANEL_TOKEN;
const flagsmithKey = Env.FLAGSMITH_KEY;
Expand All @@ -18,9 +19,11 @@ export const config = {
supportedLocales: ['en', 'fr'],
// Config
env,
isDebug: env === 'development',
version,
buildNumber,
runtimeVersion,
androidPackageName,
// SDK
sentryDsn,
mixpanelToken,
Expand Down
12 changes: 12 additions & 0 deletions src/core/i18n/resources/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,17 @@
"changeLocale": {
"failure": "The language could not be changed",
"success": "The language has been changed"
},
"updateAvailable": {
"nativePrompt": {
"title": "Update available",
"message": "A new version is available. Do you want to update now?",
"updateCta": "Update"
},
"banner": {
"compareVersions": "Version {{storeVersion}} of the app is now available. You are currently on version {{currentVersion}}.",
"defaultTitle": "A new version of the app is available.",
"updateCta": "Update now"
}
}
}
4 changes: 4 additions & 0 deletions src/core/testing/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
12 changes: 11 additions & 1 deletion src/core/testing/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@ import { ThemeProvider } from '@shopify/restyle';
import type { RenderAPI } from '@testing-library/react-native';
import { cleanup, render as rtlRender } from '@testing-library/react-native';
import type { ReactElement } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import { theme } from '$core/theme';

afterEach(cleanup);

export const customRender = (component: ReactElement): RenderAPI => {
const wrapper = <ThemeProvider theme={theme}>{component}</ThemeProvider>;
const wrapper = (
<SafeAreaProvider
initialMetrics={{
frame: { x: 0, y: 0, width: 0, height: 0 },
insets: { top: 0, left: 0, right: 0, bottom: 0 },
}}
>
<ThemeProvider theme={theme}>{component}</ThemeProvider>
</SafeAreaProvider>
);

return rtlRender(wrapper);
};
33 changes: 25 additions & 8 deletions src/features/home/Version.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { config } from '$core/constants';
import { StoreUpdateBanner } from '$shared/components/StoreUpdateBanner';
import { Box, Text } from '$shared/uiKit/primitives';

export function Version() {
if (!config.version || !config.buildNumber) {
const insets = useSafeAreaInsets();

if (!config.version) {
return null;
}

return (
<Box alignItems="flex-end" pt="spacing_32">
<Text variant="small">
{`Version: v${config.version}:${config.buildNumber}`}
</Text>
<Box
pb="spacing_8"
style={{
marginBottom: insets.bottom,
}}
>
<Box px="spacing_24">
<StoreUpdateBanner />
</Box>

<Box alignItems="flex-end" pt="spacing_32" px="spacing_24">
<Text variant="small">
{`Version: v${config.version}${
config.buildNumber ? `:${config.buildNumber}` : ''
}`}
</Text>

{typeof config.runtimeVersion === 'string' && (
<Text variant="small">{`Runtime: v${config.runtimeVersion}`}</Text>
)}
{typeof config.runtimeVersion === 'string' && (
<Text variant="small">{`Runtime: v${config.runtimeVersion}`}</Text>
)}
</Box>
</Box>
);
}
4 changes: 2 additions & 2 deletions src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ export function HomeScreen({ navigation }: HomeScreenProps) {
</Box>

<Informations />

<Version />
</Box>

<Version />
</Screen>
);
}
33 changes: 31 additions & 2 deletions src/shared/components/AppUpdateNeeded.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Linking } from 'react-native';
import semverGte from 'semver/functions/gte';

import { config } from '$core/constants';
import { config, isIOS } from '$core/constants';
import { Logger } from '$core/logger';
import { useRunOnMount } from '$shared/hooks/useRunOnMount';
import { Button } from '$shared/uiKit/button';
import { Box, Text } from '$shared/uiKit/primitives';

export function AppUpdateNeeded() {
Expand All @@ -12,6 +15,7 @@ export function AppUpdateNeeded() {
const { t } = useTranslation('miscScreens');

useRunOnMount(() => {
// TODO(prod): Save correct value to feature flag
// const lastSupportedVersion: string = FeatureFlags.getFlagValue(
// 'lastSupportedAppVersion',
// );
Expand All @@ -31,6 +35,25 @@ export function AppUpdateNeeded() {
}
});

const onPress = async () => {
try {
// TODO(prod): Replace with real iTunes item ID
const itunesItemId = '';

await Linking.openURL(
isIOS
? `https://apps.apple.com/app/apple-store/id${itunesItemId}`
: `market://details?id=${config.androidPackageName}&showAllReviews=true`,
);
} catch (error) {
Logger.error({
error,
message: 'Failed to open app store',
level: 'warning',
});
}
};

if (!isAppUnsupported) {
return null;
}
Expand All @@ -48,7 +71,13 @@ export function AppUpdateNeeded() {
{t('appUpdate.title')}
</Text>

<Text textAlign="center">{t('appUpdate.description')}</Text>
<Text mb="spacing_16" textAlign="center">
{t('appUpdate.description')}
</Text>

<Button.Text onPress={onPress}>
{t('updateAvailable.banner.updateCta', { ns: 'settings' })}
</Button.Text>
</Box>
);
}
1 change: 1 addition & 0 deletions src/shared/components/MaintenanceMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function MaintenanceMode() {
const { t } = useTranslation('miscScreens');

useRunOnMount(() => {
// TODO(prod): Save correct value to feature flag
// const isMaintenanceModeEnabled =
// FeatureFlags.getFlagValue('isMaintenanceMode');

Expand Down
63 changes: 63 additions & 0 deletions src/shared/components/StoreUpdateBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

import { Logger } from '$core/logger';
import { useRunOnMount } from '$shared/hooks/useRunOnMount';
import { Button } from '$shared/uiKit/button';
import { Box, Text } from '$shared/uiKit/primitives';
import { checkForNativeUpdate } from '$shared/utils/checkForAppUpdates';

type UpdateStatus = {
shouldUpdate: boolean;
startUpdate: () => Promise<void>;
storeVersion: string | null;
currentVersion: string | null;
};

export function StoreUpdateBanner() {
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>();

const { t } = useTranslation('settings');

useRunOnMount(() => {
checkForNativeUpdate({
title: t('updateAvailable.nativePrompt.title'),
message: t('updateAvailable.nativePrompt.message'),
buttonUpgradeText: t('updateAvailable.nativePrompt.updateCta'),
buttonCancelText: t('cancel', {
ns: 'common',
}),
})
.then((status) => {
setUpdateStatus(status);
})
.catch((error: Error) => {
Logger.error({
error,
level: 'warning',
message: 'Failed to check for native update',
});
});
});

if (!updateStatus?.shouldUpdate) return null;

return (
<Box bg="yellow" borderRadius="radius_8" px="spacing_16" py="spacing_8">
<Text textAlign="center">
{updateStatus.storeVersion
? t('updateAvailable.banner.compareVersions', {
currentVersion: updateStatus.currentVersion,
storeVersion: updateStatus.storeVersion,
})
: t('updateAvailable.banner.defaultTitle')}
</Text>

<Box alignItems="center" pt="spacing_8">
<Button.Text onPress={updateStatus.startUpdate}>
{t('updateAvailable.banner.updateCta')}
</Button.Text>
</Box>
</Box>
);
}
2 changes: 2 additions & 0 deletions src/shared/components/splashscreen/hooks/useLoadAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useCallback } from 'react';

import { Logger } from '$core/logger';
import { useRunOnMount } from '$shared/hooks/useRunOnMount';
import { checkForOtaUpdate } from '$shared/utils/checkForAppUpdates';

export const useLoadAssets = () => {
const [areFontsLoaded] = useFonts({
Expand All @@ -29,6 +30,7 @@ export const useLoadAssets = () => {
const onLayoutRootView = useCallback(() => {
(async () => {
if (areFontsLoaded) {
await checkForOtaUpdate();
await SplashScreen.hideAsync();
}
})().catch((error) => {
Expand Down
64 changes: 64 additions & 0 deletions src/shared/utils/checkForAppUpdates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as Application from 'expo-application';
import * as Updates from 'expo-updates';
import type { StartUpdateOptions } from 'sp-react-native-in-app-updates';
import SpInAppUpdates, { IAUUpdateKind } from 'sp-react-native-in-app-updates';

import { config, isAndroid } from '$core/constants';
import { Logger } from '$core/logger';

import { sleep } from './sleep';

export const checkForOtaUpdate = async () => {
try {
const update = await Updates.checkForUpdateAsync();

if (update.isAvailable) {
const ONE_SECOND = 1_000;

await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
await sleep(ONE_SECOND);
}
} catch (error) {
Logger.error({
error,
level: 'warning',
message: 'Error fetching latest Expo update',
});
}
};

export const checkForNativeUpdate = async (
updateOptionsOverwrites: StartUpdateOptions,
) => {
const inAppUpdates = new SpInAppUpdates(config.isDebug);

const currentVersion = Application.nativeApplicationVersion;
let updateOptions: StartUpdateOptions = {};
const { shouldUpdate, storeVersion } = await inAppUpdates.checkNeedsUpdate();

if (shouldUpdate) {
if (isAndroid) {
updateOptions = {
...updateOptionsOverwrites,
updateType: IAUUpdateKind.FLEXIBLE,
};
} else {
updateOptions = updateOptionsOverwrites;
}

return {
shouldUpdate,
startUpdate: async () => inAppUpdates.startUpdate(updateOptions),
storeVersion,
currentVersion,
};
}

return {
shouldUpdate: false,
startUpdate: async () => {},
storeVersion,
currentVersion,
};
};
Loading

0 comments on commit b544f7d

Please sign in to comment.