Skip to content

Commit

Permalink
feat: Add Offix to handle offline-first apollo (#454)
Browse files Browse the repository at this point in the history
* feat: Add Offix to handle offline-first apollo

* Use useQuery and useMutation

* Remove waqi

* Nice error screen details

* Make some stuff work

* Add cache persist

* Fix bug aqicn

* Fix lint

* Add some comments
  • Loading branch information
amaury1093 committed Feb 16, 2020
1 parent 80a1ab4 commit 32387f5
Show file tree
Hide file tree
Showing 21 changed files with 510 additions and 265 deletions.
19 changes: 15 additions & 4 deletions App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ApolloProvider } from '@apollo/react-hooks';
import { ActionSheetProvider } from '@expo/react-native-action-sheet';
import Constants from 'expo-constants';
import * as Font from 'expo-font';
import { ApolloOfflineClient } from 'offix-client';
import React, { useEffect, useState } from 'react';
import { AppState, Platform, StatusBar } from 'react-native';
import * as Sentry from 'sentry-expo';
Expand All @@ -32,8 +33,9 @@ import {
LocationContextProvider
} from './stores';
import { setupAmplitude, track } from './util/amplitude';
import { client } from './util/apollo';
import { getApolloClient } from './util/apollo';
import { IS_SENTRY_SET_UP } from './util/constants';
import { sentryError } from './util/sentry';

// Add Sentry if available
if (IS_SENTRY_SET_UP) {
Expand All @@ -49,6 +51,8 @@ if (IS_SENTRY_SET_UP) {

export function App(): React.ReactElement {
const [ready, setReady] = useState(false);
const [client, setClient] = useState<ApolloOfflineClient>();

useEffect(() => {
Promise.all([
Font.loadAsync({
Expand All @@ -58,12 +62,19 @@ export function App(): React.ReactElement {
// Add Amplitude if available
setupAmplitude()
])

.then(() => setReady(true))
.catch(error => console.error(error));
.catch(sentryError('App'));
}, []);

useEffect(() => {
// Load the Offix client
getApolloClient()
.then(setClient)
.catch(sentryError('App'));
}, []);

useEffect(() => {
// Track user closing/re-opening the app
AppState.addEventListener('change', state => {
if (state === 'active') {
track('APP_REFOCUS');
Expand All @@ -73,7 +84,7 @@ export function App(): React.ReactElement {
});
}, []);

return ready ? (
return ready && client ? (
<ErrorContextProvider>
<LocationContextProvider>
<ApolloProvider client={client}>
Expand Down
37 changes: 26 additions & 11 deletions App/Screens/ErrorScreen/ErrorScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with Sh**t! I Smoke. If not, see <http://www.gnu.org/licenses/>.

import React, { useContext, useEffect } from 'react';
import { Ionicons } from '@expo/vector-icons';
import React, { useContext, useEffect, useState } from 'react';
import { Image, StyleSheet, Text, View } from 'react-native';
import { scale } from 'react-native-size-matters';
import { ScrollView, TouchableOpacity } from 'react-native-gesture-handler';
import { NavigationInjectedProps } from 'react-navigation';

import errorPicture from '../../../assets/images/error.png';
Expand All @@ -40,9 +41,11 @@ const styles = StyleSheet.create({
flexDirection: 'column'
},
errorMessage: {
...theme.text,
fontSize: scale(10),
marginTop: theme.spacing.small
...theme.text
},
errorScrollView: {
flex: 1,
marginVertical: theme.spacing.small
},
errorText: {
...theme.shitText,
Expand All @@ -55,12 +58,13 @@ const styles = StyleSheet.create({

export function ErrorScreen(props: ErrorScreenProps): React.ReactElement {
const { error } = useContext(ErrorContext);
const [showDetails, setShowDetails] = useState(false);

trackScreen('ERROR');

useEffect(() => {
if (error) {
sentryError(error);
sentryError('ErrorScreen')(error);
}
}, [error]);

Expand All @@ -86,11 +90,22 @@ export function ErrorScreen(props: ErrorScreenProps): React.ReactElement {
{i18n.t('error_screen_choose_other_location').toUpperCase()}
</Button>
<Text style={theme.text}>{i18n.t('error_screen_error_description')}</Text>
<Text style={styles.errorMessage}>
{i18n.t('error_screen_error_message', {
errorText: error && error.message
})}
</Text>
<ScrollView style={styles.errorScrollView}>
<TouchableOpacity onPress={(): void => setShowDetails(!showDetails)}>
{showDetails ? (
<Text style={styles.errorMessage}>
{i18n.t('error_screen_error_message', {
errorText: error && error.message
})}
</Text>
) : (
<Text style={styles.errorMessage}>
{i18n.t('error_screen_show_details')}{' '}
<Ionicons name="ios-arrow-forward" />
</Text>
)}
</TouchableOpacity>
</ScrollView>
</View>
);
}
133 changes: 80 additions & 53 deletions App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,44 @@
// You should have received a copy of the GNU General Public License
// along with Sh**t! I Smoke. If not, see <http://www.gnu.org/licenses/>.

import { useMutation, useQuery } from '@apollo/react-hooks';
import { FontAwesome } from '@expo/vector-icons';
import { Frequency } from '@shootismoke/graphql';
import {
Frequency,
MutationUpdateUserArgs,
QueryGetUserArgs,
User
} from '@shootismoke/graphql';
import { Notifications } from 'expo';
import Constants from 'expo-constants';
import * as Localization from 'expo-localization';
import * as Permissions from 'expo-permissions';
import { pipe } from 'fp-ts/lib/pipeable';
import * as T from 'fp-ts/lib/Task';
import * as TE from 'fp-ts/lib/TaskEither';
import React, { useContext, useEffect, useState } from 'react';
import { AsyncStorage, StyleSheet, Text, View, ViewProps } from 'react-native';
import React, { useContext } from 'react';
import { StyleSheet, Text, View, ViewProps } from 'react-native';
import { scale } from 'react-native-size-matters';
import Switch from 'react-native-switch-pro';

import { ActionPicker } from '../../../../components';
import { i18n } from '../../../../localization';
import { ApiContext } from '../../../../stores';
import { updateNotifications } from '../../../../stores/util';
import { createUser, GET_USER, UPDATE_USER } from '../../../../stores/graphql';
import { AmplitudeEvent, track } from '../../../../util/amplitude';
import { logFpError, promiseToTE } from '../../../../util/fp';
import { promiseToTE } from '../../../../util/fp';
import { sentryError } from '../../../../util/sentry';
import * as theme from '../../../../util/theme';

const STORAGE_KEY = 'NOTIFICATIONS';
/**
* https://gist.github.com/navix/6c25c15e0a2d3cd0e5bce999e0086fc9
*/
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Array<infer U>
? Array<DeepPartial<U>>
: T[P] extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: DeepPartial<T[P]>;
};

const notificationsValues = ['never', 'daily', 'weekly', 'monthly'] as const;

Expand Down Expand Up @@ -90,29 +106,30 @@ export function SelectNotifications(
props: SelectNotificationsProps
): React.ReactElement {
const { style, ...rest } = props;
const [notif, setNotif] = useState<Frequency>('never');
const { api } = useContext(ApiContext);

useEffect(() => {
async function getNotifications(): Promise<void> {
const value = await AsyncStorage.getItem(STORAGE_KEY);

if (value && notificationsValues.includes(value as Frequency)) {
setNotif(value as Frequency);
const { data: queryData } = useQuery<{ getUser: User }, QueryGetUserArgs>(
GET_USER,
{
variables: {
expoInstallationId: Constants.installationId
}
}
);
const [updateUser, { data: mutationData }] = useMutation<
{ __typename: 'Mutation'; updateUser: DeepPartial<User> },
MutationUpdateUserArgs
>(UPDATE_USER);

getNotifications();
}, []);
const notif =
(mutationData?.updateUser || queryData?.getUser)?.notifications
?.frequency || 'never';

/**
* Handler for changing notification frequency
*
* @param buttonIndex - The button index in the ActionSheet
*/
function handleChangeNotif(frequency: Frequency): void {
setNotif(frequency);

track(
`HOME_SCREEN_NOTIFICATIONS_${frequency.toUpperCase()}` as AmplitudeEvent
);
Expand All @@ -124,41 +141,51 @@ export function SelectNotifications(
}

pipe(
promiseToTE(async () => {
const { status } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);

if (status !== 'granted') {
throw new Error('Permission to access notifications was denied');
}

return await Notifications.getExpoPushTokenAsync();
}, 'SelectNotifications'),
TE.chain(expoPushToken =>
updateNotifications({
expoPushToken,
frequency,
timezone: Localization.timezone,
universalId: api.pm25.location
})
),
TE.chain(() =>
promiseToTE(
() => AsyncStorage.setItem(STORAGE_KEY, frequency),
'SelectNotifications'
)
),
TE.fold(
() => {
setNotif('never');
AsyncStorage.setItem(STORAGE_KEY, 'never');

return T.of(void undefined);
},
() => T.of(void undefined)
createUser(),
TE.chain(expoInstallationId =>
promiseToTE(async () => {
const { status } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);

if (status !== 'granted') {
throw new Error('Permission to access notifications was denied');
}

const expoPushToken = await Notifications.getExpoPushTokenAsync();
const notifications = {
expoPushToken,
frequency,
timezone: Localization.timezone,
universalId: api.pm25.location
};
console.log(
`<SelectNotifications> - Update user ${JSON.stringify(
notifications
)}`
);

await updateUser({
optimisticResponse: {
__typename: 'Mutation',
updateUser: {
__typename: 'User',
_id: queryData?.getUser._id,
notifications: {
__typename: 'Notifications',
_id: queryData?.getUser.notifications?._id,
frequency
}
}
},
variables: {
expoInstallationId,
input: { notifications }
}
});
}, 'SelectNotifications')
)
)().catch(logFpError('SelectNotifications'));
)().catch(sentryError('SelectNotifications'));
}

// Is the switch on or off?
Expand Down
2 changes: 1 addition & 1 deletion App/Screens/Home/Footer/ShareButton/ShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function ShareButton(props: ShareButtonProps): React.ReactElement {
// https://github.com/amaurymartiny/shoot-i-smoke/issues/250
await Share.share({ message, title, url: imageUrl });
} catch (error) {
sentryError(error);
sentryError('ShareButton')(error);
}
}

Expand Down
1 change: 0 additions & 1 deletion App/Screens/Loading/Loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ export function Loading(): React.ReactElement {
// Set a 2s timer that will set `longWaiting` to true. Used to show an
// additional "cough" message on the loading screen
longWaitingTimeout = setTimeout(() => {
console.log('<Loading> - Long waiting');
setLongWaiting(true);
}, 2000);

Expand Down
4 changes: 2 additions & 2 deletions App/Screens/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from '../../stores';
import { Location } from '../../stores/util/fetchGpsPosition';
import { track, trackScreen } from '../../util/amplitude';
import { logFpError } from '../../util/fp';
import { sentryError } from '../../util/sentry';
import * as theme from '../../util/theme';
import { AlgoliaItem } from './AlgoliaItem';
import { AlgoliaHit, fetchAlgolia } from './fetchAlgolia';
Expand Down Expand Up @@ -112,7 +112,7 @@ export function Search(props: SearchProps): React.ReactElement {
return T.of(void undefined);
}
)
)().catch(logFpError('Search'));
)().catch(sentryError('Search'));
}, 500);
}

Expand Down
1 change: 1 addition & 0 deletions App/localization/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"error_screen_choose_other_location": "Choose other location",
"error_screen_error_description": "There's either a problem with our databases, or you don't have any Air Monitoring Stations near you. Try again later!",
"error_screen_error_message": "Error: {{errorText}}",
"error_screen_show_details": "Show details",
"home_station_too_far_message": "We couldn’t find a closer station to you.\nResults may be inaccurate at this distance.",
"home_beta_not_accurate": "Numbers may not be 100% accurate.",
"home_share_title": "Did you know that you may be smoking up to 20 cigarettes per day, just for living in a big city?",
Expand Down

0 comments on commit 32387f5

Please sign in to comment.