Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Show alert dialog when Mapeo is updated #555

Merged
merged 7 commits into from
Apr 22, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
"description": "Title of dialog that shows when cancelling a new observation",
"message": "Discard observation?"
},
"hooks.updateNotifier.dialogMessage": {
"description": "Message shown in dialog shown to the user when Mapeo is updated",
"message": "Mapeo was successfully updated from version {previousVersion} to {currentVersion} on {updateDateTime}"
},
"hooks.updateNotifier.dialogOK": {
"description": "Button label to dismiss dialog shown to the user when Mapeo is updated",
"message": "OK"
},
"hooks.updateNotifier.dialogTitle": {
"description": "Title of dialog shown to the user when Mapeo is updated",
"message": "Mapeo Updated"
},
"screens.AboutMapeo.androidBuild": {
"description": "Label for Android build number",
"message": "Android build number"
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"mapeo-offline-map": "^2.0.0",
"mapeo-schema": "^2.0.2",
"nodejs-mobile-react-native": "^0.6.1",
"p-is-promise": "^4.0.0",
"patch-package": "^6.2.2",
"path": "^0.12.7",
"prop-types": "^15.7.2",
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IntlProvider } from "./context/IntlContext";
import AppProvider from "./context/AppProvider";
import bugsnag from "./lib/logger";
import IS_E2E from "./lib/is-e2e";
import useUpdateNotifierEffect from "./hooks/useUpdateNotifierEffect";

// Turn off warnings about require cycles
YellowBox.ignoreWarnings(["Require cycle:"]);
Expand Down Expand Up @@ -101,6 +102,11 @@ class ErrorBoundary extends React.Component<
}
}

const UpdateNotifier = () => {
useUpdateNotifierEffect();
return null;
};

/* IntlProvider needs to be first so that error messages are translated */
const App = () => (
<IntlProvider>
Expand All @@ -114,6 +120,7 @@ const App = () => (
persistNavigationState={persistNavigationState}
loadNavigationState={loadNavigationState}
/>
<UpdateNotifier />
</AppProvider>
</AppLoading>
</PermissionsProvider>
Expand Down
42 changes: 42 additions & 0 deletions src/frontend/hooks/useDeviceInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @flow
import React from "react";
import DeviceInfo from "react-native-device-info";
import isPromise from "p-is-promise";
import debug from "debug";

type State = "loading" | "ready" | "error";

const log = debug("mapeo:useDeviceInfo");

export default function useDeviceInfo(prop: string) {
const methodName = "get" + prop.replace(/^[a-z]/, m => m.toUpperCase());
const result = React.useMemo(() => DeviceInfo[methodName](), [methodName]);
const resultIsPromise = isPromise(result);

const [value, setValue] = React.useState(resultIsPromise ? null : result);
const [state, setState] = React.useState<State>(
resultIsPromise ? "loading" : "ready"
);
const [context, setContext] = React.useState();

React.useEffect(() => {
if (typeof result !== "object") return;
result
.then(v => {
setState("ready");
setValue(v);
})
.catch(e => {
log(`Error reading DeviceInfo.${methodName}(): ${e.message}`);
setState("error");
setContext(e);
setValue(undefined);
});
}, [result, methodName]);

return React.useMemo(() => ({ value, state, context }), [
value,
state,
context,
]);
}
101 changes: 101 additions & 0 deletions src/frontend/hooks/useUpdateNotifierEffect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// @flow
import React from "react";
import { Alert } from "react-native";
import { useIntl, defineMessages } from "react-intl";
import createPersistedState from "./usePersistedState";
import useDeviceInfo from "./useDeviceInfo";
import { formats } from "../context/IntlContext";

// Array of [versionName, installTime (millseconds since unix epoc)]
type SavedVersionInfo = [string, number][];

const m = defineMessages({
dialogTitle: {
id: "hooks.updateNotifier.dialogTitle",
description: "Title of dialog shown to the user when Mapeo is updated",
defaultMessage: "Mapeo Updated \u2705",
},
dialogMessage: {
id: "hooks.updateNotifier.dialogMessage",
description:
"Message shown in dialog shown to the user when Mapeo is updated",
defaultMessage:
"Mapeo was successfully updated from version {previousVersion} to {currentVersion} on {updateDateTime}",
},
dialogOk: {
id: "hooks.updateNotifier.dialogOK",
description:
"Button label to dismiss dialog shown to the user when Mapeo is updated",
defaultMessage: "OK",
},
});

// Changing this will cause the upgrade notification to not show on the next
// update, so it should not be changed without a migration path
const STORE_KEY = "@MapeoVersion@1";

const usePersistedState = createPersistedState(STORE_KEY);

export default function useUpdateNotifierEffect() {
const { formatMessage, formatDate } = useIntl();
const [
savedVersionInfo,
savedVersionStatus,
setSavedVersionInfo,
] = usePersistedState<SavedVersionInfo>([]);
const readableVersion = useDeviceInfo("readableVersion");
const lastUpdateTime = useDeviceInfo("lastUpdateTime");

React.useEffect(() => {
if (
!(
savedVersionStatus === "idle" &&
readableVersion.state === "ready" &&
lastUpdateTime.state === "ready"
)
)
return;

const currentVersion = readableVersion.value;
const currentInstallTime = lastUpdateTime.value;
// Shouldn't get here, but this makes Flow type checking work
if (
typeof currentVersion !== "string" ||
typeof currentInstallTime !== "number"
)
return;

const previousVersionInfo = savedVersionInfo[savedVersionInfo.length - 1];
const versionHasChanged =
previousVersionInfo && previousVersionInfo[0] !== currentVersion;

if (versionHasChanged || typeof previousVersionInfo === "undefined") {
setSavedVersionInfo([
...savedVersionInfo,
[currentVersion, currentInstallTime],
]);
}

if (!versionHasChanged) return;

Alert.alert(
formatMessage(m.dialogTitle),
formatMessage(m.dialogMessage, {
previousVersion: previousVersionInfo[0],
currentVersion,
updateDateTime: formatDate(currentInstallTime, formats.date.long),
}),
[{ text: formatMessage(m.dialogOk) }]
);
}, [
savedVersionStatus,
readableVersion.state,
lastUpdateTime.state,
readableVersion.value,
lastUpdateTime.value,
savedVersionInfo,
setSavedVersionInfo,
formatMessage,
formatDate,
]);
}
29 changes: 10 additions & 19 deletions src/frontend/screens/Settings/AboutMapeo.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// @flow
import React from "react";
import { FormattedMessage, defineMessages, useIntl } from "react-intl";
import DeviceInfo from "react-native-device-info";

import HeaderTitle from "../../sharedComponents/HeaderTitle";
import { List, ListItem, ListItemText } from "../../sharedComponents/List";
import useDeviceInfo from "../../hooks/useDeviceInfo";

const m = defineMessages({
aboutMapeoTitle: {
Expand Down Expand Up @@ -50,33 +50,24 @@ const m = defineMessages({
},
});

// Quick custom hook to
function useDeviceInfo(prop) {
const { formatMessage } = useIntl();
const methodName = "get" + prop.replace(/^[a-z]/, m => m.toUpperCase());
const result = React.useMemo(() => DeviceInfo[methodName](), [methodName]);
const [value, setValue] = React.useState(
// result type is "object" if it's a Promise
typeof result === "object" ? "…" : result
);
React.useEffect(() => {
if (typeof result !== "object") return;
result.then(setValue).catch(e => setValue(formatMessage(m.unknown)));
}, [formatMessage, result]);
return value;
}

const DeviceInfoListItem = ({
label,
deviceProp,
}: {
label: string,
deviceProp: string,
}) => {
const value = useDeviceInfo(deviceProp);
const { formatMessage } = useIntl();
const { value, state } = useDeviceInfo(deviceProp);
const displayValue =
state === "loading"
? "…"
: state === "ready"
? value
: formatMessage(m.unknown);
return (
<ListItem>
<ListItemText primary={label} secondary={value}></ListItemText>
<ListItemText primary={label} secondary={displayValue}></ListItemText>
</ListItem>
);
};
Expand Down