From c6392fdc96e451ad076bbac971ebdedeac849156 Mon Sep 17 00:00:00 2001 From: Amaury Martiny Date: Mon, 9 Mar 2020 08:34:35 +0100 Subject: [PATCH] fix: Retry fetching expo push token (#473) * fix: Retry fetching expo push token * Change switch component * Use dooboo * Add useQuery too * Don't use getOrCreate, just create * Don't log useless data * Small tweaks * Small tweaks --- .../SelectNotifications.tsx | 228 +++++++++++------- App/Screens/Search/Search.tsx | 4 +- App/Screens/Search/fetchAlgolia.ts | 116 ++++----- App/ambient.d.ts | 1 - App/components/ActionPicker/ActionPicker.tsx | 4 +- App/localization/languages/en.json | 1 + App/stores/api.tsx | 6 +- App/stores/distanceUnit.tsx | 27 ++- App/stores/location.tsx | 2 +- App/stores/util/fetchGpsPosition.ts | 75 +++--- App/stores/util/gql.ts | 27 ++- App/util/fp.ts | 37 ++- App/util/sentry.ts | 4 +- package.json | 2 +- yarn.lock | 19 +- 15 files changed, 341 insertions(+), 212 deletions(-) diff --git a/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx b/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx index 4266204e..8883245e 100644 --- a/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx +++ b/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx @@ -14,28 +14,34 @@ // You should have received a copy of the GNU General Public License // along with Sh**t! I Smoke. If not, see . -import { useMutation } from '@apollo/client'; +import { useMutation, useQuery } from '@apollo/client'; +import Switch from '@dooboo-ui/native-switch-toggle'; import { FontAwesome } from '@expo/vector-icons'; import { Frequency, - MutationGetOrCreateUserArgs, + MutationCreateUserArgs, 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 * as C from 'fp-ts/lib/Console'; +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 { 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 { GET_OR_CREATE_USER, UPDATE_USER } from '../../../../stores/util'; +import { CREATE_USER, GET_USER, UPDATE_USER } from '../../../../stores/util'; import { AmplitudeEvent, track } from '../../../../util/amplitude'; +import { promiseToTE, retry, sideEffect } from '../../../../util/fp'; import { sentryError } from '../../../../util/sentry'; import * as theme from '../../../../util/theme'; @@ -94,8 +100,17 @@ const styles = StyleSheet.create({ fontWeight: '900', textTransform: 'uppercase' }, - switch: { - marginRight: theme.spacing.small + switchCircle: { + borderRadius: scale(11), + height: scale(22), + width: scale(22) + }, + switchContainer: { + borderRadius: scale(14), + height: scale(28), + marginRight: theme.spacing.small, + padding: scale(3), + width: scale(48) } }); @@ -104,17 +119,26 @@ export function SelectNotifications( ): React.ReactElement { const { style, ...rest } = props; const { api } = useContext(ApiContext); - const [getOrCreateUser, { data: queryData }] = useMutation< - { getOrCreateUser: User }, - MutationGetOrCreateUserArgs - >(GET_OR_CREATE_USER, { + const { data: getUserData, error: queryError } = useQuery< + { getUser: DeepPartial }, + QueryGetUserArgs + >(GET_USER, { + fetchPolicy: 'cache-and-network', + variables: { + expoInstallationId: Constants.installationId + } + }); + const [createUser, { data: createUserData }] = useMutation< + { createUser: DeepPartial }, + MutationCreateUserArgs + >(CREATE_USER, { variables: { input: { expoInstallationId: Constants.installationId } } }); - const [updateUser, { data: mutationData }] = useMutation< - { __typename: 'Mutation'; updateUser: DeepPartial }, + const [updateUser, { data: updateUserData }] = useMutation< + { updateUser: DeepPartial }, MutationUpdateUserArgs >(UPDATE_USER); - // This state is used of optimistic UI: right after the user clicks, we set + // This state is used for optimistic UI: right after the user clicks, we set // this state to what the user clicked. When the actual mutation resolves, we // populate with the real data. const [optimisticNotif, setOptimisticNotif] = useState(); @@ -123,24 +147,27 @@ export function SelectNotifications( // If we have optimistic UI, show it optimisticNotif || // If we have up-to-date data from backend, take that - mutationData?.updateUser.notifications?.frequency || + updateUserData?.updateUser.notifications?.frequency || + createUserData?.createUser.notifications?.frequency || // At the beginning, before anything happens, query from backend - queryData?.getOrCreateUser.notifications?.frequency || - // If the queryData is still loading, just show `never` + getUserData?.getUser.notifications?.frequency || + // If the getUserData is still loading, just show `never` 'never'; useEffect(() => { - getOrCreateUser({ - variables: { input: { expoInstallationId: Constants.installationId } } - }).catch(sentryError('SelectNotifications')); - }, [getOrCreateUser]); + if (queryError?.message.includes('No user with expoInstallationId')) { + createUser({ + variables: { input: { expoInstallationId: Constants.installationId } } + }).catch(sentryError('SelectNotifications')); + } + }, [createUser, queryError]); useEffect(() => { - // If we receive new mutationData, then our optimistic UI is obsolete - if (mutationData) { + // If we receive new updateUserData, then our optimistic UI is obsolete + if (updateUserData) { setOptimisticNotif(undefined); } - }, [mutationData]); + }, [updateUserData]); /** * Handler for changing notification frequency @@ -154,39 +181,75 @@ export function SelectNotifications( `HOME_SCREEN_NOTIFICATIONS_${frequency.toUpperCase()}` as AmplitudeEvent ); - async function updateNotification(): Promise { - const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS); - - if (status !== 'granted') { - throw new Error('Permission to access notifications was denied'); - } - - if (!api) { - throw new Error( - 'Home/SelectNotifications/SelectNotifications.tsx only gets displayed when `api` is defined.' - ); - } + if (!api) { + throw new Error( + 'Home/SelectNotifications/SelectNotifications.tsx only gets displayed when `api` is defined.' + ); + } - const expoPushToken = await Notifications.getExpoPushTokenAsync(); - const notifications = { + pipe( + promiseToTE( + () => Permissions.askAsync(Permissions.NOTIFICATIONS), + 'SelectNotifications' + ), + TE.chain(({ status }) => + status === 'granted' + ? TE.right(undefined) + : TE.left(new Error('Permission to access notifications was denied')) + ), + TE.chain(() => + // Retry 3 times to get the Expo push token, sometimes we get an Error + // "Couldn't get GCM token for device" on 1st try + retry( + () => + promiseToTE( + () => Notifications.getExpoPushTokenAsync(), + 'SelectNotifications' + ), + { + retries: 3 + } + ) + ), + TE.map(expoPushToken => ({ expoPushToken, frequency, timezone: Localization.timezone, universalId: api.pm25.location - }; - console.log( - ` - Update user ${JSON.stringify(notifications)}` - ); - - await updateUser({ - variables: { - expoInstallationId: Constants.installationId, - input: { notifications } - } - }); - } + })), + TE.chain( + sideEffect(notifications => + TE.rightIO( + C.log( + ` - Update user ${JSON.stringify( + notifications + )}` + ) + ) + ) + ), + TE.chain(notifications => + promiseToTE( + () => + updateUser({ + variables: { + expoInstallationId: Constants.installationId, + input: { notifications } + } + }), + 'SelectNotifications' + ) + ), + TE.fold( + error => { + sentryError('SelectNotifications')(error); + setOptimisticNotif('never'); - updateNotification().catch(sentryError('SelectNotifications')); + return T.of(undefined); + }, + () => T.of(undefined) + ) + )().catch(sentryError('SelectNotifications')); } // Is the switch on or off? @@ -199,6 +262,7 @@ export function SelectNotifications( options: notificationsValues .map(f => i18n.t(`home_frequency_${f}`)) // Translate .map(capitalize) + .concat(i18n.t('home_frequency_notifications_cancel')) }} callback={(buttonIndex): void => { if (buttonIndex === 4) { @@ -209,40 +273,40 @@ export function SelectNotifications( handleChangeNotif(notificationsValues[buttonIndex]); // +1 because we skipped neve }} > - - - - {isSwitchOn ? ( - + {(open): React.ReactElement => ( + + + + {isSwitchOn ? ( + + + {i18n.t('home_frequency_notify_me')} + + + {i18n.t(`home_frequency_${notif}`)}{' '} + + + + ) : ( - {i18n.t('home_frequency_notify_me')} + {i18n.t('home_frequency_allow_notifications')} - - {i18n.t(`home_frequency_${notif}`)}{' '} - - - - ) : ( - - {i18n.t('home_frequency_allow_notifications')} - - )} - + )} + + )} ); } diff --git a/App/Screens/Search/Search.tsx b/App/Screens/Search/Search.tsx index 826e8d40..23fc392e 100644 --- a/App/Screens/Search/Search.tsx +++ b/App/Screens/Search/Search.tsx @@ -101,7 +101,7 @@ export function Search(props: SearchProps): React.ReactElement { setLoading(false); setAlgoliaError(err); - return T.of(void undefined); + return T.of(undefined); }, hits => { setLoading(false); @@ -109,7 +109,7 @@ export function Search(props: SearchProps): React.ReactElement { setHits(hits); setFrequency('daily'); - return T.of(void undefined); + return T.of(undefined); } ) )().catch(sentryError('Search')); diff --git a/App/Screens/Search/fetchAlgolia.ts b/App/Screens/Search/fetchAlgolia.ts index 18e44731..3d77cbbd 100644 --- a/App/Screens/Search/fetchAlgolia.ts +++ b/App/Screens/Search/fetchAlgolia.ts @@ -66,66 +66,70 @@ export function fetchAlgolia( search: string, gps?: LatLng ): TE.TaskEither { - return retry(algoliaUrls.length, status => - pipe( - TE.rightIO( - C.log( - ` - fetchAlgolia - Attempt #${status.iterNumber}: ${ - algoliaUrls[(status.iterNumber - 1) % algoliaUrls.length] - }/1/places/query` - ) - ), - TE.chain(() => - promiseToTE( - () => - axios.post( - `${ - algoliaUrls[(status.iterNumber - 1) % algoliaUrls.length] - }/1/places/query`, - { - aroundLatLng: gps - ? `${gps.latitude},${gps.longitude}` - : undefined, - hitsPerPage: 10, - language: 'en', - query: search - }, - { - headers: - Constants.manifest.extra.algoliaApplicationId && - Constants.manifest.extra.algoliaApiKey - ? { - 'X-Algolia-Application-Id': - Constants.manifest.extra.algoliaApplicationId, - 'X-Algolia-API-Key': - Constants.manifest.extra.algoliaApiKey - } + return retry( + status => + pipe( + TE.rightIO( + C.log( + ` - fetchAlgolia - Attempt #${status.iterNumber}: ${ + algoliaUrls[(status.iterNumber - 1) % algoliaUrls.length] + }/1/places/query` + ) + ), + TE.chain(() => + promiseToTE( + () => + axios.post( + `${ + algoliaUrls[(status.iterNumber - 1) % algoliaUrls.length] + }/1/places/query`, + { + aroundLatLng: gps + ? `${gps.latitude},${gps.longitude}` : undefined, + hitsPerPage: 10, + language: 'en', + query: search + }, + { + headers: + Constants.manifest.extra.algoliaApplicationId && + Constants.manifest.extra.algoliaApiKey + ? { + 'X-Algolia-Application-Id': + Constants.manifest.extra.algoliaApplicationId, + 'X-Algolia-API-Key': + Constants.manifest.extra.algoliaApiKey + } + : undefined, - timeout: 3000 - } - ), - 'fetchAlgolia' - ) - ), - TE.chain(response => - T.of( - pipe( - AxiosResponseT.decode(response), - E.mapLeft(failure), - E.mapLeft(errs => errs[0]), // Only show 1st error - E.mapLeft(Error) + timeout: 3000 + } + ), + 'fetchAlgolia' ) - ) - ), - TE.map(response => response.data.hits), - TE.chain( - sideEffect((hits: AlgoliaHit[]) => - TE.rightIO( - C.log(` - fetchAlgolia - Got ${hits.length} results`) + ), + TE.chain(response => + T.of( + pipe( + AxiosResponseT.decode(response), + E.mapLeft(failure), + E.mapLeft(errs => errs[0]), // Only show 1st error + E.mapLeft(Error) + ) + ) + ), + TE.map(response => response.data.hits), + TE.chain( + sideEffect((hits: AlgoliaHit[]) => + TE.rightIO( + C.log(` - fetchAlgolia - Got ${hits.length} results`) + ) ) ) - ) - ) + ), + { + retries: algoliaUrls.length + } ); } diff --git a/App/ambient.d.ts b/App/ambient.d.ts index 3f94017a..5ad97c7a 100644 --- a/App/ambient.d.ts +++ b/App/ambient.d.ts @@ -18,4 +18,3 @@ declare module '*.json'; declare module '*.mp4'; declare module '*.png'; declare module '@hapi/hawk/lib/browser'; -declare module 'react-native-switch-pro'; diff --git a/App/components/ActionPicker/ActionPicker.tsx b/App/components/ActionPicker/ActionPicker.tsx index 5e899a84..cb1f71c4 100644 --- a/App/components/ActionPicker/ActionPicker.tsx +++ b/App/components/ActionPicker/ActionPicker.tsx @@ -24,7 +24,7 @@ import { TouchableOpacity, TouchableOpacityProps } from 'react-native'; interface ActionPickerProps extends TouchableOpacityProps { actionSheetOptions: ActionSheetOptions; callback: (i: number) => void; - children: React.ReactElement; + children: (open: () => void) => React.ReactElement; } export function ActionPicker(props: ActionPickerProps): React.ReactElement { @@ -37,7 +37,7 @@ export function ActionPicker(props: ActionPickerProps): React.ReactElement { return ( - {children} + {children(handleActionSheet)} ); } diff --git a/App/localization/languages/en.json b/App/localization/languages/en.json index 73755c8a..f7400f86 100644 --- a/App/localization/languages/en.json +++ b/App/localization/languages/en.json @@ -65,6 +65,7 @@ "home_frequency_weekly": "weekly", "home_frequency_monthly": "monthly", "home_frequency_allow_notifications": "Allow\nnotifications?", + "home_frequency_notifications_cancel": "Cancel", "home_frequency_notify_me": "Notify me", "loading_title_cough": "Cough", "loading_title_loading": "Loading", diff --git a/App/stores/api.tsx b/App/stores/api.tsx index 244c9b77..ade895d8 100644 --- a/App/stores/api.tsx +++ b/App/stores/api.tsx @@ -108,7 +108,7 @@ function raceApi(gps: LatLng): TE.TaskEither { return promiseToTE( () => promiseAny(tasks).catch((errors: AggregateError) => { - // Transform an AggregateError into a JS Error + // Transform an AggregateError into a JS native Error const aggregateMessage = [...errors] .map(({ message }, index) => `${index + 1}. ${message}`) .join('. '); @@ -157,13 +157,13 @@ export function ApiContextProvider({ setError(error); track('API_DAILY_ERROR'); - return T.of(void undefined); + return T.of(undefined); }, newApi => { setApi(newApi); track('API_DAILY_RESPONSE'); - return T.of(void undefined); + return T.of(undefined); } ) )().catch(sentryError('ApiContextProvider')); diff --git a/App/stores/distanceUnit.tsx b/App/stores/distanceUnit.tsx index 77e44d99..980fa37a 100644 --- a/App/stores/distanceUnit.tsx +++ b/App/stores/distanceUnit.tsx @@ -19,6 +19,7 @@ import { AsyncStorage } from 'react-native'; import { i18n } from '../localization'; import { noop } from '../util/noop'; +import { sentryError } from '../util/sentry'; export type DistanceUnit = 'km' | 'mile'; type DistanceUnitFormat = 'short' | 'long'; @@ -46,24 +47,28 @@ export function DistanceUnitProvider({ i18n.locale === 'en-US' ? 'mile' : 'km' ); - const getDistanceUnit = async (): Promise => { - const unit = await AsyncStorage.getItem(STORAGE_KEY); - if (unit === 'km' || unit === 'mile') { - setDistanceUnit(unit); - } - }; - - const localizedDistanceUnit = (format: 'short' | 'long'): string => - distanceUnit === 'km' + function localizedDistanceUnit(format: 'short' | 'long'): string { + return distanceUnit === 'km' ? i18n.t(`distance_unit_${format}_km`) : i18n.t(`distance_unit_${format}_mi`); + } useEffect(() => { - getDistanceUnit(); + async function getDistanceUnit(): Promise { + const unit = await AsyncStorage.getItem(STORAGE_KEY); + + if (unit === 'km' || unit === 'mile') { + setDistanceUnit(unit); + } + } + + getDistanceUnit().catch(sentryError('DistanceUnitProvider')); }, []); useEffect(() => { - AsyncStorage.setItem(STORAGE_KEY, distanceUnit); + AsyncStorage.setItem(STORAGE_KEY, distanceUnit).catch( + sentryError('DistanceUnitProvider') + ); }, [distanceUnit]); return ( diff --git a/App/stores/location.tsx b/App/stores/location.tsx index 2cfc532c..8340df9e 100644 --- a/App/stores/location.tsx +++ b/App/stores/location.tsx @@ -77,7 +77,7 @@ export function LocationContextProvider({ setGpsLocation(gps); setCurrentLocation(gps); - return TE.right(void undefined); + return TE.right(undefined); }) ), TE.chain(gps => diff --git a/App/stores/util/fetchGpsPosition.ts b/App/stores/util/fetchGpsPosition.ts index 1431cf5d..f1e2abe3 100644 --- a/App/stores/util/fetchGpsPosition.ts +++ b/App/stores/util/fetchGpsPosition.ts @@ -32,15 +32,15 @@ export function fetchReverseGeocode( currentLocation: LatLng ): TE.TaskEither { return pipe( - promiseToTE(async () => { - const reverse = await ExpoLocation.reverseGeocodeAsync(currentLocation); - - if (!reverse.length) { - throw new Error('Reverse geocoding returned no results'); - } - - return reverse[0]; - }, 'fetchReverseGeocode'), + promiseToTE( + () => ExpoLocation.reverseGeocodeAsync(currentLocation), + 'fetchReverseGeocode' + ), + TE.chain(reverse => + reverse.length + ? TE.right(reverse[0]) + : TE.left(new Error('Reverse geocoding returned no results')) + ), TE.map(reverse => ({ ...currentLocation, city: reverse.city, @@ -59,28 +59,37 @@ export function fetchGpsPosition(): TE.TaskEither< Error, ExpoLocation.LocationData > { - return promiseToTE(async () => { - const { status } = await Permissions.askAsync(Permissions.LOCATION); - - if (status !== 'granted') { - throw new Error('Permission to access location was denied'); - } - - return ExpoLocation.getCurrentPositionAsync({ - timeout: 5000 - }); - // Uncomment to get other locations - // return { - // coords: { - // latitude: Math.random() * 90, - // longitude: Math.random() * 90 - // } - // }; - // return { - // coords: { - // latitude: 48.4, - // longitude: 2.34 - // } - // }; - }, 'fetchGpsPosition'); + return pipe( + promiseToTE( + () => Permissions.askAsync(Permissions.LOCATION), + 'fetchGpsPosition' + ), + TE.chain(({ status }) => + status === 'granted' + ? TE.right(undefined) + : TE.left(new Error('Permission to access location was denied')) + ), + TE.chain(() => + promiseToTE( + () => + ExpoLocation.getCurrentPositionAsync({ + timeout: 5000 + }), + // Uncomment to get other locations + // Promise.resolve({ + // coords: { + // latitude: Math.random() * 90, + // longitude: Math.random() * 90 + // } + // }); + // Promise.resolve({ + // coords: { + // latitude: 48.4, + // longitude: 2.34 + // } + // }); + 'fetchGpsPosition' + ) + ) + ); } diff --git a/App/stores/util/gql.ts b/App/stores/util/gql.ts index 5aaf5b51..2ebf47e9 100644 --- a/App/stores/util/gql.ts +++ b/App/stores/util/gql.ts @@ -16,11 +16,29 @@ import { gql } from '@apollo/client'; -export const GET_OR_CREATE_USER = gql` - mutation getOrCreateUser($input: GetOrCreateUserInput!) { - getOrCreateUser(input: $input) { +export const GET_USER = gql` + query getUser($expoInstallationId: ID!) { + __typename + getUser(expoInstallationId: $expoInstallationId) { + __typename _id notifications { + __typename + _id + frequency + } + } + } +`; + +export const CREATE_USER = gql` + mutation createUser($input: CreateUserInput!) { + __typename + createUser(input: $input) { + __typename + _id + notifications { + __typename _id frequency } @@ -30,9 +48,12 @@ export const GET_OR_CREATE_USER = gql` export const UPDATE_USER = gql` mutation updateUser($expoInstallationId: ID!, $input: UpdateUserInput!) { + __typename updateUser(expoInstallationId: $expoInstallationId, input: $input) { + __typename _id notifications { + __typename _id frequency } diff --git a/App/util/fp.ts b/App/util/fp.ts index 451af68e..b238dfaf 100644 --- a/App/util/fp.ts +++ b/App/util/fp.ts @@ -21,14 +21,20 @@ import * as O from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; import * as T from 'fp-ts/lib/Task'; import * as TE from 'fp-ts/lib/TaskEither'; -import { capDelay, limitRetries, RetryStatus } from 'retry-ts'; +import { + capDelay, + exponentialBackoff, + limitRetries, + monoidRetryPolicy, + RetryStatus +} from 'retry-ts'; import { retrying } from 'retry-ts/lib/Task'; import { sentryError } from './sentry'; /** * A side-effect in a TaskEither chain: if the TaskEither fails, still return - * a TaskEither.RightTask + * a TaskEither.Right * * @example * ``` @@ -57,29 +63,42 @@ export function sideEffect(fn: (input: A) => TE.TaskEither) { ); } -// This Error is always thrown at the beginning of a retry, we ignore it -const EMPTY_OPTION_ERROR = new Error('Empty Option'); +interface RetryOptions { + capDelay?: number; + exponentialBackoff?: number; + retries?: number; +} /** + * Retry a TaskEither * * @param retries - The number of time to retry * @param teFn - A function returning a TE */ export function retry( - retries: number, - teFn: (status: RetryStatus, delay: number) => TE.TaskEither + teFn: (status: RetryStatus, delay: number) => TE.TaskEither, + options: RetryOptions = {} ): TE.TaskEither { + // Set our retry policy + const policy = capDelay( + options.capDelay || 2000, + monoidRetryPolicy.concat( + exponentialBackoff(options.exponentialBackoff || 200), + limitRetries(options.retries || 3) + ) + ); + return retrying( - capDelay(2000, limitRetries(retries)), // Do `retries` times max, and set limit to 2s + policy, status => pipe( status.previousDelay, O.fold( - () => TE.left(EMPTY_OPTION_ERROR), + () => TE.left(new Error('Empty Option')), delay => teFn(status, delay) ) ), - either => E.isLeft(either) + E.isLeft ); } diff --git a/App/util/sentry.ts b/App/util/sentry.ts index 3996a7ff..7bcaebca 100644 --- a/App/util/sentry.ts +++ b/App/util/sentry.ts @@ -28,7 +28,9 @@ const UNTRACKED_ERRORS = [ 'Reverse geocoding returned no results', // No results from data providers 'does not have PM2.5 measurings right now', - 'Cannot normalize, got 0 result' + 'Cannot normalize, got 0 result', + // User not created yet on backend + 'No user with expoInstallationId' ]; /** diff --git a/package.json b/package.json index df2b8229..f054a3d4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@apollo/link-error": "^2.0.0-beta.3", "@apollo/link-retry": "^2.0.0-beta.3", "@apollo/react-hooks": "^3.1.3", + "@dooboo-ui/native-switch-toggle": "^0.4.0", "@expo/react-native-action-sheet": "^3.5.0", "@expo/vector-icons": "^10.0.0", "@hapi/hawk": "^8.0.0", @@ -49,7 +50,6 @@ "react-native-reanimated": "~1.4.0", "react-native-scroll-into-view": "^1.0.3", "react-native-size-matters": "^0.3.0", - "react-native-switch-pro": "^1.0.4", "react-native-view-shot": "3.0.2", "react-navigation": "^4.0.10", "react-navigation-stack": "^1.8.1", diff --git a/yarn.lock b/yarn.lock index a840c29a..30e966c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1353,6 +1353,13 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@dooboo-ui/native-switch-toggle@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@dooboo-ui/native-switch-toggle/-/native-switch-toggle-0.4.0.tgz#147b552cc96daeefb473382857da8efa5c402033" + integrity sha512-LlFqVEp5FOedHOmjztnQWLcZa42i6Aj6Xu+71K0LVbkeWui2CeW3zgr8VPXAIaye7zBhkWei4QTGR/10/2v8Kg== + dependencies: + dooboolab-welcome "^1.1.1" + "@expo/react-native-action-sheet@^3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@expo/react-native-action-sheet/-/react-native-action-sheet-3.5.0.tgz#243ef101b47d9d438ba17883ff816252c5d5b7dc" @@ -3431,6 +3438,11 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" +dooboolab-welcome@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/dooboolab-welcome/-/dooboolab-welcome-1.1.1.tgz#ca2184f6e1f568865d707a6ce3bda7e003550f54" + integrity sha512-tQ/9NBCGnalHwKOBjDqTD4t4wQpDfSSbxQGctzkzaLHR3AhN0wCBkIs05JciVtvl/jfLZ+DgAndktAMk3Bu7Vw== + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -7209,13 +7221,6 @@ react-native-size-matters@^0.3.0: resolved "https://registry.yarnpkg.com/react-native-size-matters/-/react-native-size-matters-0.3.0.tgz#0692e324a189884819b570c4cab24d27ecd4311d" integrity sha512-PuwA07mr3woQPFJXo3FwlFwpe6bb2LCb20sDZ4JWMpGBtmppq4sKH4GLgS6Sw+GkN0sSOEXGZLaOsHZYFY9hJg== -react-native-switch-pro@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/react-native-switch-pro/-/react-native-switch-pro-1.0.4.tgz#9007628e4b8721b44fa0d6ecd07013f6afd0031a" - integrity sha512-HCuvKVrCbN/wO6rFfwCS9ds4CbqldUMdtINosbVVrR72Zf9YiG/rLrsDC/8ezJSc1Q2P03vCF89odaS81bJyow== - dependencies: - prop-types "^15.5.10" - react-native-view-shot@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/react-native-view-shot/-/react-native-view-shot-3.0.2.tgz#daccaec5b8038a680b17533ff7e72876e68c7d0d"