From 32387f5c41f110ca5212ed92832350daace87210 Mon Sep 17 00:00:00 2001 From: Amaury Martiny Date: Sun, 16 Feb 2020 13:14:36 +0100 Subject: [PATCH] feat: Add Offix to handle offline-first apollo (#454) * 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 --- App/App.tsx | 19 +- App/Screens/ErrorScreen/ErrorScreen.tsx | 37 +++- .../SelectNotifications.tsx | 133 +++++++----- .../Home/Footer/ShareButton/ShareButton.tsx | 2 +- App/Screens/Loading/Loading.tsx | 1 - App/Screens/Search/Search.tsx | 4 +- App/localization/languages/en.json | 1 + App/stores/api.tsx | 27 ++- .../createUser.ts} | 12 +- App/stores/graphql/getUser.ts | 29 +++ App/stores/graphql/index.ts | 19 ++ App/stores/graphql/updateUser.ts | 29 +++ App/stores/location.tsx | 7 +- App/stores/util/index.ts | 2 - App/stores/util/updateUser.ts | 66 ------ App/util/apollo.ts | 134 ++++++++++-- App/util/fp.ts | 14 +- App/util/sentry.ts | 24 ++- App/util/station.ts | 2 +- package.json | 14 +- yarn.lock | 199 ++++++++++++------ 21 files changed, 510 insertions(+), 265 deletions(-) rename App/stores/{util/getOrCreateUser.ts => graphql/createUser.ts} (87%) create mode 100644 App/stores/graphql/getUser.ts create mode 100644 App/stores/graphql/index.ts create mode 100644 App/stores/graphql/updateUser.ts delete mode 100644 App/stores/util/updateUser.ts diff --git a/App/App.tsx b/App/App.tsx index 3bf6699d..6a213de3 100644 --- a/App/App.tsx +++ b/App/App.tsx @@ -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'; @@ -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) { @@ -49,6 +51,8 @@ if (IS_SENTRY_SET_UP) { export function App(): React.ReactElement { const [ready, setReady] = useState(false); + const [client, setClient] = useState(); + useEffect(() => { Promise.all([ Font.loadAsync({ @@ -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'); @@ -73,7 +84,7 @@ export function App(): React.ReactElement { }); }, []); - return ready ? ( + return ready && client ? ( diff --git a/App/Screens/ErrorScreen/ErrorScreen.tsx b/App/Screens/ErrorScreen/ErrorScreen.tsx index 2a8f891c..62e57364 100644 --- a/App/Screens/ErrorScreen/ErrorScreen.tsx +++ b/App/Screens/ErrorScreen/ErrorScreen.tsx @@ -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 . -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'; @@ -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, @@ -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]); @@ -86,11 +90,22 @@ export function ErrorScreen(props: ErrorScreenProps): React.ReactElement { {i18n.t('error_screen_choose_other_location').toUpperCase()} {i18n.t('error_screen_error_description')} - - {i18n.t('error_screen_error_message', { - errorText: error && error.message - })} - + + setShowDetails(!showDetails)}> + {showDetails ? ( + + {i18n.t('error_screen_error_message', { + errorText: error && error.message + })} + + ) : ( + + {i18n.t('error_screen_show_details')}{' '} + + + )} + + ); } diff --git a/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx b/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx index ee1e565e..4dd0c135 100644 --- a/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx +++ b/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx @@ -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 . +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 = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : DeepPartial; +}; const notificationsValues = ['never', 'daily', 'weekly', 'monthly'] as const; @@ -90,20 +106,23 @@ export function SelectNotifications( props: SelectNotificationsProps ): React.ReactElement { const { style, ...rest } = props; - const [notif, setNotif] = useState('never'); const { api } = useContext(ApiContext); - - useEffect(() => { - async function getNotifications(): Promise { - 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 }, + MutationUpdateUserArgs + >(UPDATE_USER); - getNotifications(); - }, []); + const notif = + (mutationData?.updateUser || queryData?.getUser)?.notifications + ?.frequency || 'never'; /** * Handler for changing notification frequency @@ -111,8 +130,6 @@ export function SelectNotifications( * @param buttonIndex - The button index in the ActionSheet */ function handleChangeNotif(frequency: Frequency): void { - setNotif(frequency); - track( `HOME_SCREEN_NOTIFICATIONS_${frequency.toUpperCase()}` as AmplitudeEvent ); @@ -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( + ` - 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? diff --git a/App/Screens/Home/Footer/ShareButton/ShareButton.tsx b/App/Screens/Home/Footer/ShareButton/ShareButton.tsx index 71587c79..9b8b3800 100644 --- a/App/Screens/Home/Footer/ShareButton/ShareButton.tsx +++ b/App/Screens/Home/Footer/ShareButton/ShareButton.tsx @@ -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); } } diff --git a/App/Screens/Loading/Loading.tsx b/App/Screens/Loading/Loading.tsx index f4def41d..f9a75a3d 100644 --- a/App/Screens/Loading/Loading.tsx +++ b/App/Screens/Loading/Loading.tsx @@ -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(' - Long waiting'); setLongWaiting(true); }, 2000); diff --git a/App/Screens/Search/Search.tsx b/App/Screens/Search/Search.tsx index 9b0ff541..d65d009e 100644 --- a/App/Screens/Search/Search.tsx +++ b/App/Screens/Search/Search.tsx @@ -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'; @@ -112,7 +112,7 @@ export function Search(props: SearchProps): React.ReactElement { return T.of(void undefined); } ) - )().catch(logFpError('Search')); + )().catch(sentryError('Search')); }, 500); } diff --git a/App/localization/languages/en.json b/App/localization/languages/en.json index 3c67f843..86535e02 100644 --- a/App/localization/languages/en.json +++ b/App/localization/languages/en.json @@ -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?", diff --git a/App/stores/api.tsx b/App/stores/api.tsx index 87638793..244c9b77 100644 --- a/App/stores/api.tsx +++ b/App/stores/api.tsx @@ -20,18 +20,19 @@ import { OpenAQFormat, ProviderPromise } from '@shootismoke/dataproviders'; -import { aqicn, openaq, waqi } from '@shootismoke/dataproviders/lib/promise'; +import { aqicn, openaq } from '@shootismoke/dataproviders/lib/promise'; import Constants from 'expo-constants'; import { pipe } from 'fp-ts/lib/pipeable'; import * as T from 'fp-ts/lib/Task'; import * as TE from 'fp-ts/lib/TaskEither'; -import promiseAny from 'p-any'; +import promiseAny, { AggregateError } from 'p-any'; import React, { createContext, useContext, useEffect, useState } from 'react'; import { track } from '../util/amplitude'; -import { logFpError, promiseToTE } from '../util/fp'; +import { promiseToTE } from '../util/fp'; import { noop } from '../util/noop'; import { pm25ToCigarettes } from '../util/secretSauce'; +import { sentryError } from '../util/sentry'; import { ErrorContext } from './error'; import { CurrentLocationContext } from './location'; @@ -67,7 +68,7 @@ function filterPm25(normalized: Normalized): Api { }; } else { throw new Error( - `PM2.5 has not been measured by station ${normalized[0].location} right now` + `Station ${normalized[0].location} does not have PM2.5 measurings right now` ); } } @@ -101,11 +102,21 @@ function raceApi(gps: LatLng): TE.TaskEither { fetchForProvider(openaq, { limit: 1, parameter: ['pm25'] - }), - fetchForProvider(waqi) + }) ]; - return promiseToTE(() => promiseAny(tasks), 'raceApi'); + return promiseToTE( + () => + promiseAny(tasks).catch((errors: AggregateError) => { + // Transform an AggregateError into a JS Error + const aggregateMessage = [...errors] + .map(({ message }, index) => `${index + 1}. ${message}`) + .join('. '); + + throw new Error(aggregateMessage); + }), + 'ApiContext' + ); } interface Context { @@ -155,7 +166,7 @@ export function ApiContextProvider({ return T.of(void undefined); } ) - )().catch(logFpError('ApiContextProvider')); + )().catch(sentryError('ApiContextProvider')); }, [latitude, longitude]); // eslint-disable-line react-hooks/exhaustive-deps return ( diff --git a/App/stores/util/getOrCreateUser.ts b/App/stores/graphql/createUser.ts similarity index 87% rename from App/stores/util/getOrCreateUser.ts rename to App/stores/graphql/createUser.ts index 28f52c8a..2e7499cb 100644 --- a/App/stores/util/getOrCreateUser.ts +++ b/App/stores/graphql/createUser.ts @@ -15,12 +15,12 @@ // along with Sh**t! I Smoke. If not, see . import { CreateUserInput } from '@shootismoke/graphql'; -import { gql } from 'apollo-boost'; import Constants from 'expo-constants'; import * as TE from 'fp-ts/lib/TaskEither'; +import gql from 'graphql-tag'; import { AsyncStorage } from 'react-native'; -import { client } from '../../util/apollo'; +import { getApolloClient } from '../../util/apollo'; import { promiseToTE } from '../../util/fp'; const STORAGE_KEY = 'MONGO_ID'; @@ -34,13 +34,14 @@ const CREATE_USER = gql` `; // The mongo id of the user, stored here in memory, but also in AsyncStorage. -// If this string is set, it means that we created our user on the backend +// If this string is set, it means that we created our user on the backend. We +// don't actually use the mongodb id for now, but we store it, just in case. let cachedMongoId: string | undefined; /** * Get or create a user */ -export function getOrCreateUser(): TE.TaskEither { +export function createUser(): TE.TaskEither { return promiseToTE(async () => { if (!cachedMongoId) { let mongoId = await AsyncStorage.getItem(STORAGE_KEY); @@ -54,6 +55,7 @@ export function getOrCreateUser(): TE.TaskEither { )}` ); + const client = await getApolloClient(); const res = await client.mutate({ mutation: CREATE_USER, variables: { input } @@ -68,5 +70,5 @@ export function getOrCreateUser(): TE.TaskEither { } return Constants.installationId; - }, 'getOrCreateUser'); + }, 'createUser'); } diff --git a/App/stores/graphql/getUser.ts b/App/stores/graphql/getUser.ts new file mode 100644 index 00000000..eb3f8a04 --- /dev/null +++ b/App/stores/graphql/getUser.ts @@ -0,0 +1,29 @@ +// Sh**t! I Smoke +// Copyright (C) 2018-2020 Marcelo S. Coelho, Amaury Martiny + +// Sh**t! I Smoke is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Sh**t! I Smoke is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Sh**t! I Smoke. If not, see . + +import gql from 'graphql-tag'; + +export const GET_USER = gql` + query getUser($expoInstallationId: ID!) { + getUser(expoInstallationId: $expoInstallationId) { + _id + notifications { + _id + frequency + } + } + } +`; diff --git a/App/stores/graphql/index.ts b/App/stores/graphql/index.ts new file mode 100644 index 00000000..5ff51875 --- /dev/null +++ b/App/stores/graphql/index.ts @@ -0,0 +1,19 @@ +// Sh**t! I Smoke +// Copyright (C) 2018-2020 Marcelo S. Coelho, Amaury Martiny + +// Sh**t! I Smoke is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Sh**t! I Smoke is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Sh**t! I Smoke. If not, see . + +export * from './getUser'; +export * from './createUser'; +export * from './updateUser'; diff --git a/App/stores/graphql/updateUser.ts b/App/stores/graphql/updateUser.ts new file mode 100644 index 00000000..f3c71e06 --- /dev/null +++ b/App/stores/graphql/updateUser.ts @@ -0,0 +1,29 @@ +// Sh**t! I Smoke +// Copyright (C) 2018-2020 Marcelo S. Coelho, Amaury Martiny + +// Sh**t! I Smoke is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Sh**t! I Smoke is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Sh**t! I Smoke. If not, see . + +import gql from 'graphql-tag'; + +export const UPDATE_USER = gql` + mutation updateUser($expoInstallationId: ID!, $input: UpdateUserInput!) { + updateUser(expoInstallationId: $expoInstallationId, input: $input) { + _id + notifications { + _id + frequency + } + } + } +`; diff --git a/App/stores/location.tsx b/App/stores/location.tsx index b8b3ea68..2cfc532c 100644 --- a/App/stores/location.tsx +++ b/App/stores/location.tsx @@ -20,8 +20,9 @@ import * as T from 'fp-ts/lib/Task'; import * as TE from 'fp-ts/lib/TaskEither'; import React, { createContext, useContext, useEffect, useState } from 'react'; -import { logFpError, sideEffect } from '../util/fp'; +import { sideEffect } from '../util/fp'; import { noop } from '../util/noop'; +import { sentryError } from '../util/sentry'; import { ErrorContext } from './error'; import { fetchGpsPosition, @@ -95,7 +96,7 @@ export function LocationContextProvider({ }, location => { console.log( - ` - fetchGpsPosition - Got reverse location ${JSON.stringify( + ` - Got reverse location ${JSON.stringify( location )}` ); @@ -105,7 +106,7 @@ export function LocationContextProvider({ return T.of(undefined); } ) - )().catch(logFpError('LocationContextProvider')); + )().catch(sentryError('LocationContextProvider')); }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( diff --git a/App/stores/util/index.ts b/App/stores/util/index.ts index 7d3dac0b..8bfa28f4 100644 --- a/App/stores/util/index.ts +++ b/App/stores/util/index.ts @@ -15,5 +15,3 @@ // along with Sh**t! I Smoke. If not, see . export * from './fetchGpsPosition'; -export * from './getOrCreateUser'; -export * from './updateUser'; diff --git a/App/stores/util/updateUser.ts b/App/stores/util/updateUser.ts deleted file mode 100644 index e009921a..00000000 --- a/App/stores/util/updateUser.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Sh**t! I Smoke -// Copyright (C) 2018-2020 Marcelo S. Coelho, Amaury Martiny - -// Sh**t! I Smoke is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Sh**t! I Smoke is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Sh**t! I Smoke. If not, see . - -import { NotificationsInput } from '@shootismoke/graphql'; -import { gql } from 'apollo-boost'; -import * as C from 'fp-ts/lib/Console'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as TE from 'fp-ts/lib/TaskEither'; - -import { client } from '../../util/apollo'; -import { promiseToTE, sideEffect } from '../../util/fp'; -import { getOrCreateUser } from './getOrCreateUser'; - -const UPDATE_USER = gql` - mutation updateUser($expoInstallationId: ID!, $input: UpdateUserInput!) { - updateUser(expoInstallationId: $expoInstallationId, input: $input) { - _id - } - } -`; - -/** - * Update notification setting - */ -export function updateNotifications( - notifications: NotificationsInput -): TE.TaskEither { - return pipe( - getOrCreateUser(), - TE.map(expoInstallationId => ({ - expoInstallationId, - input: { - notifications - } - })), - TE.chain( - sideEffect(({ input }) => - TE.rightIO(C.log(` - ${JSON.stringify(input)}`)) - ) - ), - TE.chain(data => - promiseToTE( - async () => - client.mutate({ - mutation: UPDATE_USER, - variables: data - }), - 'updateNotifications' - ) - ), - TE.map(() => true) - ); -} diff --git a/App/util/apollo.ts b/App/util/apollo.ts index a6302ee5..cc731d0a 100644 --- a/App/util/apollo.ts +++ b/App/util/apollo.ts @@ -15,10 +15,21 @@ // along with Sh**t! I Smoke. If not, see . import Hawk from '@hapi/hawk/lib/browser'; +import NetInfo from '@react-native-community/netinfo'; import { userSchema } from '@shootismoke/graphql'; -import ApolloClient from 'apollo-boost'; -import { ErrorResponse } from 'apollo-link-error'; +import { InMemoryCache } from 'apollo-cache-inmemory'; +import { persistCache } from 'apollo-cache-persist'; +import { ApolloLink } from 'apollo-link'; +import { setContext } from 'apollo-link-context'; +import { ErrorResponse, onError } from 'apollo-link-error'; +import { createHttpLink } from 'apollo-link-http'; import Constants from 'expo-constants'; +import { + ApolloOfflineClient, + NetworkStatus, + PersistedData +} from 'offix-client'; +import { AsyncStorage } from 'react-native'; import { IS_PROD, RELEASE_CHANNEL } from '../util/constants'; import { sentryError } from './sentry'; @@ -27,6 +38,7 @@ const BACKEND_URI = IS_PROD ? 'https://shootismoke.now.sh/api/graphql' : 'https://staging.shootismoke.now.sh/api/graphql'; +// Hawk credentials const credentials = { id: `${Constants.manifest.slug}-${RELEASE_CHANNEL}`, key: Constants.manifest.extra.hawkKey, @@ -34,29 +46,105 @@ const credentials = { }; /** - * The Apollo client + * Create cache wrapper. + * + * @see https://offix.dev/docs/react-native */ -export const client = new ApolloClient({ - onError: ({ graphQLErrors, networkError }: ErrorResponse): void => { - // Send errors to Sentry - if (networkError) { - sentryError(networkError); +const cacheStorage = { + async getItem(key: string): Promise { + const data = await AsyncStorage.getItem(key); + if (typeof data === 'string') { + return JSON.parse(data); } - if (graphQLErrors) { - graphQLErrors.forEach(sentryError); - } + return data; + }, + async removeItem(key: string): Promise { + return AsyncStorage.removeItem(key); }, - request: (operation): void => { - // Set Hawk authorization header on each request - const { header } = Hawk.client.header(BACKEND_URI, 'POST', { credentials }); - - operation.setContext({ - headers: { - authorization: header - } - }); + async setItem(key: string, value: PersistedData): Promise { + const valueStr = typeof value === 'object' ? JSON.stringify(value) : value; + + return AsyncStorage.setItem(key, valueStr); + } +}; + +/** + * Create network interface. + * + * @see https://offix.dev/docs/react-native + */ +const networkStatus: NetworkStatus = { + onStatusChangeListener(callback) { + NetInfo.addEventListener(state => + callback.onStatusChange({ online: state.isConnected }) + ); }, - typeDefs: [userSchema], - uri: BACKEND_URI -}); + async isOffline() { + const state = await NetInfo.fetch(); + + return !state.isConnected; + } +}; + +// Cache Apollo client +let _client: ApolloOfflineClient; + +/** + * Create an Apollo client (via Offix). + */ +export async function getApolloClient(): Promise { + if (_client) { + return _client; + } + + const cache = new InMemoryCache(); + + // await before instantiating ApolloClient, else queries might run before the cache is persisted + await persistCache({ + cache, + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore FIXME, I don't know how to fix here + storage: AsyncStorage + }); + + const client = new ApolloOfflineClient({ + cache, + cacheStorage, + link: ApolloLink.from([ + // Add Hawk authentication in header + setContext(() => { + // Set Hawk authorization header on each request + const { header } = Hawk.client.header(BACKEND_URI, 'POST', { + credentials + }); + + return { + headers: { + authorization: header + } + }; + }), + // Error handling + onError(({ graphQLErrors, networkError }: ErrorResponse): void => { + // Send errors to Sentry + if (networkError) { + sentryError('Apollo')(networkError); + } + + if (graphQLErrors) { + graphQLErrors.forEach(sentryError('Apollo')); + } + }), + // Classic HTTP link + createHttpLink({ uri: BACKEND_URI }) + ]), + offlineStorage: cacheStorage, + networkStatus, + typeDefs: [userSchema] + }); + + _client = client; + + return client; +} diff --git a/App/util/fp.ts b/App/util/fp.ts index 5c974744..451af68e 100644 --- a/App/util/fp.ts +++ b/App/util/fp.ts @@ -83,18 +83,6 @@ export function retry( ); } -/** - * Tasks and IOs can sometimes throw unexpectedly, so we catch and log here. - * This should realistically never happen. - */ -export function logFpError(namespace: string) { - return function(error: Error): void { - console.log(`<${namespace}> - ${error.message}`); - - sentryError(error); - }; -} - /** * Convert a Promise into a TaskEither * @param fn - Function returning a Promise @@ -119,7 +107,7 @@ export function promiseToTE( error = reason instanceof Error ? reason : new Error(String(reason)); } - logFpError(namespace)(error); + sentryError(namespace)(error); return error; }); diff --git a/App/util/sentry.ts b/App/util/sentry.ts index 8f01590e..38d58d30 100644 --- a/App/util/sentry.ts +++ b/App/util/sentry.ts @@ -20,10 +20,14 @@ import { IS_SENTRY_SET_UP } from './constants'; // We don't send the following errors to Sentry const UNTRACKED_ERRORS = [ + // Location not allowed 'Permission to access location was denied', 'Location provider is unavailable. Make sure that location services are enabled', 'Location request timed out', - 'Location request failed due to unsatisfied device settings' + 'Location request failed due to unsatisfied device settings', + // No results from data providers + 'does not have PM2.5 measurings right now', + 'Cannot normalize, got 0 result' ]; /** @@ -32,11 +36,15 @@ const UNTRACKED_ERRORS = [ * @see https://sentry.io * @param error - The error to send */ -export function sentryError(error: Error): void { - if ( - IS_SENTRY_SET_UP && - !UNTRACKED_ERRORS.some(msg => error.message.includes(msg)) - ) { - Sentry.captureException(error); - } +export function sentryError(namespace: string) { + return function(error: Error): void { + if ( + IS_SENTRY_SET_UP && + !UNTRACKED_ERRORS.some(msg => error.message.includes(msg)) + ) { + Sentry.captureException(error); + } + + console.log(`[${namespace}] ${error.message}`); + }; } diff --git a/App/util/station.ts b/App/util/station.ts index 40bf3e3e..63a077fa 100644 --- a/App/util/station.ts +++ b/App/util/station.ts @@ -24,7 +24,7 @@ import { DistanceUnit } from '../stores/distanceUnit'; export const MAX_DISTANCE_TO_STATION = 10; /** - * Station given by the Waqi API is fucked up. Sometimes it's [lat, lng], + * Station given by the AQICN API is fucked up. Sometimes it's [lat, lng], * sometimes it's [lng, lat]. * We check here with the user's real currentLocation coordinates, and take the * "closest" one. diff --git a/package.json b/package.json index 521ab0ce..87651ca6 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,20 @@ "@expo/react-native-action-sheet": "^3.5.0", "@expo/vector-icons": "^10.0.0", "@hapi/hawk": "^8.0.0", + "@react-native-community/netinfo": "4.6.0", "@shootismoke/convert": "^0.2.8", - "@shootismoke/dataproviders": "^0.2.9", - "@shootismoke/graphql": "^0.2.13", + "@shootismoke/dataproviders": "^0.2.17", + "@shootismoke/graphql": "^0.2.16", "@types/haversine": "^1.1.4", "@types/i18n-js": "^3.0.1", "@types/p-any": "^1.1.3", "@types/react-native": "^0.61.12", - "apollo-boost": "^0.4.7", + "apollo-cache-inmemory": "^1.6.5", + "apollo-cache-persist": "^0.1.1", + "apollo-client": "^2.6.8", + "apollo-link-context": "^1.0.19", + "apollo-link-error": "^1.1.12", + "apollo-link-http": "^1.5.16", "date-fns": "^2.9.0", "expo": "^36.0.0", "expo-analytics-amplitude": "~8.0.0", @@ -36,10 +42,12 @@ "expo-permissions": "~8.0.0", "fp-ts": "^2.4.4", "graphql": "^14.6.0", + "graphql-tag": "^2.10.3", "haversine": "^1.1.1", "i18n-js": "^3.5.1", "io-ts": "^2.0.6", "lottie-react-native": "~2.6.1", + "offix-client": "^0.13.1", "p-any": "^2.1.0", "react": "16.9.0", "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.1.tar.gz", diff --git a/yarn.lock b/yarn.lock index 6ce0a98a..77a73ace 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1712,6 +1712,11 @@ wcwidth "^1.0.1" ws "^1.1.0" +"@react-native-community/netinfo@4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-4.6.0.tgz#fc0b79a226da78371158f885a2f798ff07c926be" + integrity sha512-wz39BUpExDU1kTpLlBkDwwb0Efg+uuwixToosTSarZgpzG/CmcRvWdD786TMiE5tLDd+Mpi2xh3w4FrVM8zjoA== + "@react-navigation/core@^3.5.1": version "3.5.1" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-3.5.1.tgz#7a2339fca3496979305fb3a8ab88c2ca8d8c214d" @@ -1837,10 +1842,10 @@ resolved "https://registry.yarnpkg.com/@shootismoke/convert/-/convert-0.2.8.tgz#328eb9d32aff370020f1ba17c1badc340c102300" integrity sha512-fa8PdUZOCbEjrRAgteXBIywMNMaXriFcaiy+2PTqBLZKBhi/EkHm4BsdTO1WyYEQwj61YgGzkMYRrlA+1yX4kw== -"@shootismoke/dataproviders@^0.2.9": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@shootismoke/dataproviders/-/dataproviders-0.2.9.tgz#e02ee0e7a23ba22300b4be2d683e18104b99810f" - integrity sha512-QIgV+xehT6L4V5/A7NXL7EnPXPfHIsYH13LMJ2uIE0dBRdpWVt1l4X+rwwUsJpOeLXLkNQ7ch4Z+GYnSDi4a+Q== +"@shootismoke/dataproviders@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@shootismoke/dataproviders/-/dataproviders-0.2.17.tgz#0f6581e62ca44060a6849aaefcde3d9c91ff091c" + integrity sha512-t5a6J/x8tCIVIaFOCiOtrllrq6M7QiIdgTxoZWThFu52FaL8huVLD0dN9a5y6OJ0J3bkYsXx74sJTGYBCBB3aA== dependencies: "@shootismoke/convert" "^0.2.8" axios "^0.19.0" @@ -1849,10 +1854,10 @@ fp-ts "^2.1.1" io-ts "^2.0.1" -"@shootismoke/graphql@^0.2.13": - version "0.2.13" - resolved "https://registry.yarnpkg.com/@shootismoke/graphql/-/graphql-0.2.13.tgz#1e2feaefa40a30bd8efa99c363f67bef97c97fd9" - integrity sha512-M9xZudnAlclnXfTcyh2ExhFSRT5n7cXF5T0mgDnDUGT8cij8uee6hrNOI42TLv28dV2mrhMJQZGPpTG8L/xd5Q== +"@shootismoke/graphql@^0.2.16": + version "0.2.16" + resolved "https://registry.yarnpkg.com/@shootismoke/graphql/-/graphql-0.2.16.tgz#e257a3ac7ded16a4885e0aa096f1951b318fb054" + integrity sha512-bbxkL1qo8+FHXnz4UWEXhwFJbXvlMueGhChbIwImKQ+AJjhHd6h1+9RfDc+rtBFM6a7YDwfXxVGDIgN2VWfI4A== "@types/babel__core@^7.1.0": version "7.1.2" @@ -1971,9 +1976,9 @@ integrity sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA== "@types/node@>=6": - version "12.12.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.12.tgz#529bc3e73dbb35dd9e90b0a1c83606a9d3264bdb" - integrity sha512-MGuvYJrPU0HUwqF7LqvIj50RZUX23Z+m583KBygKYUZLlZ88n6w28XRNJRJgsHukLEnLz6w6SvxZoLgbr5wLqQ== + version "13.7.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.1.tgz#238eb34a66431b71d2aaddeaa7db166f25971a0d" + integrity sha512-Zq8gcQGmn4txQEJeiXo/KiLpon8TzAl0kmKH4zdWctPj05nWwp1ClMdAVEloqrQKfaC48PNLdgN/aVaLqUrluA== "@types/p-any@^1.1.3": version "1.1.3" @@ -2041,7 +2046,7 @@ dependencies: "@types/yargs-parser" "*" -"@types/zen-observable@^0.8.0": +"@types/zen-observable@0.8.0", "@types/zen-observable@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== @@ -2305,22 +2310,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-boost@^0.4.7: - version "0.4.7" - resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.4.7.tgz#b0680ab0893e3f8b1ab1058dcfa2b00cb6440d79" - integrity sha512-jfc3aqO0vpCV+W662EOG5gq4AH94yIsvSgAUuDvS3o/Z+8Joqn4zGC9CgLCDHusK30mFgtsEgwEe0pZoedohsQ== - dependencies: - apollo-cache "^1.3.4" - apollo-cache-inmemory "^1.6.5" - apollo-client "^2.6.7" - apollo-link "^1.0.6" - apollo-link-error "^1.0.3" - apollo-link-http "^1.3.1" - graphql-tag "^2.4.2" - ts-invariant "^0.4.0" - tslib "^1.10.0" - -apollo-cache-inmemory@^1.6.5: +apollo-cache-inmemory@1.6.5, apollo-cache-inmemory@^1.6.5: version "1.6.5" resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.5.tgz#2ccaa3827686f6ed7fb634203dbf2b8d7015856a" integrity sha512-koB76JUDJaycfejHmrXBbWIN9pRKM0Z9CJGQcBzIOtmte1JhEBSuzsOUu7NQgiXKYI4iGoMREcnaWffsosZynA== @@ -2331,6 +2321,11 @@ apollo-cache-inmemory@^1.6.5: ts-invariant "^0.4.0" tslib "^1.10.0" +apollo-cache-persist@0.1.1, apollo-cache-persist@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/apollo-cache-persist/-/apollo-cache-persist-0.1.1.tgz#e6cfe1983b998982a679aaf05241d3ed395edb1e" + integrity sha512-/7GAyblPR169ryW3ugbtHqiU0UGkhIt10NeaO2gn2ClxjLHF/nIkJD5mx/0OCF2vLNbbnzLZVDeIO1pf72TrEA== + apollo-cache@1.3.4, apollo-cache@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.4.tgz#0c9f63c793e1cd6e34c450f7668e77aff58c9a42" @@ -2339,7 +2334,7 @@ apollo-cache@1.3.4, apollo-cache@^1.3.4: apollo-utilities "^1.3.3" tslib "^1.10.0" -apollo-client@^2.6.7: +apollo-client@2.6.8, apollo-client@^2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.8.tgz#01cebc18692abf90c6b3806414e081696b0fa537" integrity sha512-0zvJtAcONiozpa5z5zgou83iEKkBaXhhSSXJebFHRXs100SecDojyUWKjwTtBPn9HbM6o5xrvC5mo9VQ5fgAjw== @@ -2353,7 +2348,15 @@ apollo-client@^2.6.7: tslib "^1.10.0" zen-observable "^0.8.0" -apollo-link-error@^1.0.3: +apollo-link-context@1.0.19, apollo-link-context@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.19.tgz#3c9ba5bf75ed5428567ce057b8837ef874a58987" + integrity sha512-TUi5TyufU84hEiGkpt+5gdH5HkB3Gx46npNfoxR4of3DKBCMuItGERt36RCaryGcU/C3u2zsICU3tJ+Z9LjFoQ== + dependencies: + apollo-link "^1.2.13" + tslib "^1.9.3" + +apollo-link-error@1.1.12, apollo-link-error@^1.1.12: version "1.1.12" resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.12.tgz#e24487bb3c30af0654047611cda87038afbacbf9" integrity sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA== @@ -2371,7 +2374,7 @@ apollo-link-http-common@^0.2.15: ts-invariant "^0.4.0" tslib "^1.9.3" -apollo-link-http@^1.3.1: +apollo-link-http@1.5.16, apollo-link-http@^1.5.16: version "1.5.16" resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.16.tgz#44fe760bcc2803b8a7f57fc9269173afb00f3814" integrity sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw== @@ -2380,7 +2383,16 @@ apollo-link-http@^1.3.1: apollo-link-http-common "^0.2.15" tslib "^1.9.3" -apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.13: +apollo-link-retry@2.2.15: + version "2.2.15" + resolved "https://registry.yarnpkg.com/apollo-link-retry/-/apollo-link-retry-2.2.15.tgz#4cc3202fcb6251fed6f6b57ade99b4b1ad05c619" + integrity sha512-ltwXGxm+2NXzskrk+GTofj66LQtcc9OGCjIxAPbjlvtHanpKJn8CviWq8dIsMiYGS9T9rGG/kPPx/VdJfcFb6w== + dependencies: + "@types/zen-observable" "0.8.0" + apollo-link "^1.2.13" + tslib "^1.9.3" + +apollo-link@1.2.13, apollo-link@^1.0.0, apollo-link@^1.2.13: version "1.2.13" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" integrity sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw== @@ -2390,7 +2402,7 @@ apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.13: tslib "^1.9.3" zen-observable-ts "^0.8.20" -apollo-utilities@1.3.3, apollo-utilities@^1.3.3: +apollo-utilities@1.3.3, apollo-utilities@^1.3.0, apollo-utilities@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.3.tgz#f1854715a7be80cd810bc3ac95df085815c0787c" integrity sha512-F14aX2R/fKNYMvhuP2t9GD9fggID7zp5I96MF5QeKYWDWTrkRdHRp4+SVfXUVN+cXOaB/IebfvRtzPf25CM0zw== @@ -2400,16 +2412,6 @@ apollo-utilities@1.3.3, apollo-utilities@^1.3.3: ts-invariant "^0.4.0" tslib "^1.10.0" -apollo-utilities@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" - integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== - dependencies: - "@wry/equality" "^0.1.2" - fast-json-stable-stringify "^2.0.0" - ts-invariant "^0.4.0" - tslib "^1.9.3" - aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -3353,6 +3355,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" +debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -3367,13 +3376,6 @@ debug@^3.1.0, debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -4514,10 +4516,10 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== -graphql-tag@^2.4.2: - version "2.10.1" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" - integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg== +graphql-tag@^2.10.3: + version "2.10.3" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03" + integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA== graphql@^14.6.0: version "14.6.0" @@ -4697,6 +4699,11 @@ iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, ic dependencies: safer-buffer ">= 2.1.2 < 3" +idb-localstorage@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/idb-localstorage/-/idb-localstorage-0.2.0.tgz#227c41798283ce24e983d3ad033249e8a118f384" + integrity sha512-urH2OHkqzRlfUwtNj2/YuQyqRsOxYzHevf+0TABYBocifo6gf/02eMU+ffpyrB0qlVQwQjE+1zUly1G2BQQRXQ== + ignore-walk@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" @@ -4865,6 +4872,11 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4978,6 +4990,11 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -5141,9 +5158,9 @@ istanbul-reports@^2.2.6: handlebars "^4.1.2" iterall@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" - integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" + integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== jest-changed-files@^24.9.0: version "24.9.0" @@ -6594,7 +6611,7 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.entries@^1.1.1: +object.entries@^1.1.0, object.entries@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b" integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ== @@ -6639,6 +6656,55 @@ object.values@^1.1.1: function-bind "^1.1.1" has "^1.0.3" +offix-cache@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/offix-cache/-/offix-cache-0.13.1.tgz#130f74b899421dacd9b26bbc17b988364ff23616" + integrity sha512-RRH6CHyscFQnPEyLr4dY9pqq+qDHQ0SzhWeyqFvHoLYx2vGYWT6543cXABiBEl5FcoTN+tcPVDPsmUDsa7iu5w== + dependencies: + apollo-client "2.6.8" + apollo-link "1.2.13" + apollo-utilities "1.3.3" + util "0.12.1" + +offix-client@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/offix-client/-/offix-client-0.13.1.tgz#f6f43213228da2f7477fcc7e68f495a5da366081" + integrity sha512-kF5jeCWpnMqdX23h/ZL+B6vNj6DfInoKAt+Ng9raLXGnzz25m4oZyaUXvvlVkqQIc6tG4e6sqK01LVXK506JfA== + dependencies: + apollo-cache-inmemory "1.6.5" + apollo-cache-persist "0.1.1" + apollo-client "2.6.8" + apollo-link "1.2.13" + apollo-link-context "1.0.19" + apollo-link-error "1.1.12" + apollo-link-http "1.5.16" + apollo-link-retry "2.2.15" + debug "4.1.1" + idb-localstorage "0.2.0" + offix-cache "0.13.1" + offix-conflicts-client "0.13.1" + offix-offline "0.13.1" + offix-scheduler "0.13.1" + +offix-conflicts-client@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/offix-conflicts-client/-/offix-conflicts-client-0.13.1.tgz#aeece93f8db3e24e3258b07de9c4f0bdc2c2e8a3" + integrity sha512-sCS9gZzgrg7wZkOKVQPWHW3ky+ed7Ta0Ea3GdDJh8ELOcloTL3VOBBRm7jy8OlM4/vzLHxt3sWHcvNuRZ+7p0g== + +offix-offline@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/offix-offline/-/offix-offline-0.13.1.tgz#086d793395520fbdf936c0c9ab888b22e624f543" + integrity sha512-E5NRUoOzJATwKFVR59meeSW/MlDfYXPqq647WvbOGtmHLglsGzPrvvzJoeegRIZbcdxodFbmyGLGumTC4ghQsg== + +offix-scheduler@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/offix-scheduler/-/offix-scheduler-0.13.1.tgz#9fae13c377ae5260b512bf1315da41fe3a887e64" + integrity sha512-5vK7QKL9yaPssiuFY/8gB308wR9xXAlPxyMByIjScgmfCEMLPk0/UKG8etIB1dCzcainKLLjkJI2rKWzW7xTNA== + dependencies: + idb-localstorage "0.2.0" + offix-cache "0.13.1" + offix-offline "0.13.1" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -8685,6 +8751,17 @@ util.promisify@^1.0.0: define-properties "^1.1.2" object.getownpropertydescriptors "^2.0.3" +util@0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.1.tgz#f908e7b633e7396c764e694dd14e716256ce8ade" + integrity sha512-MREAtYOp+GTt9/+kwf00IYoHZyjM8VU4aVrkzUlejyqaIjd2GztVl5V9hGXKlvBKE3gENn/FMfHE5v6hElXGcQ== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + object.entries "^1.1.0" + safe-buffer "^5.1.2" + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -9050,6 +9127,6 @@ zen-observable-ts@^0.8.20: zen-observable "^0.8.0" zen-observable@^0.8.0: - version "0.8.14" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.14.tgz#d33058359d335bc0db1f0af66158b32872af3bf7" - integrity sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g== + version "0.8.15" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" + integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==