From 3f3cca0a8382973b56c98d09131e980c40f6f1e9 Mon Sep 17 00:00:00 2001 From: Young-Ho Kim Date: Fri, 27 Mar 2020 20:27:16 -0400 Subject: [PATCH] #45 Show quota limit of the service in settings page. --- .../pages/settings/SettingsScreen.tsx | 52 ++++++++++++++++--- src/measure/service/DataService.ts | 9 ++++ src/measure/service/fitbit/FitbitService.ts | 13 +++++ .../fitbit/core/FitbitExampleServiceCore.ts | 10 ++++ .../fitbit/core/FitbitOfficialServiceCore.ts | 38 ++++++++++---- src/measure/service/fitbit/types.ts | 5 ++ src/utils/errors.ts | 6 +++ 7 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 src/utils/errors.ts diff --git a/src/components/pages/settings/SettingsScreen.tsx b/src/components/pages/settings/SettingsScreen.tsx index 62ddae75..700c539b 100644 --- a/src/components/pages/settings/SettingsScreen.tsx +++ b/src/components/pages/settings/SettingsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState, useMemo, useEffect } from "react"; import { View, Text, StyleSheet, TouchableHighlight, Alert, ScrollView, Switch, ViewStyle, Platform, SafeAreaView, ActionSheetIOS, UIManager, findNodeHandle } from "react-native"; import { MeasureUnitType } from "@measure/DataSourceSpec"; import { Dispatch } from "redux"; @@ -13,10 +13,10 @@ import { InitialLoadingIndicator } from "@components/pages/exploration/parts/mai import { StackNavigationProp } from "@react-navigation/stack"; import { SettingsSteckParamList } from "@components/Routes"; import { SystemLogger } from "@core/logging/SystemLogger"; -import { Logo } from "@components/Logo"; import { StyleTemplates } from "@style/Styles"; import { SafeAreaConsumer } from "react-native-safe-area-context"; import { AboutPanel } from "./AbountPanel"; +import { format } from "date-fns"; const unitTypes = [{ @@ -72,7 +72,11 @@ const styles = StyleSheet.create({ color: Colors.textGray, fontWeight: '500', backgroundColor: Colors.backPanelColor - } + }, + + smallTextStyle: { + color: Colors.textColorLight + } }) const SettingsRow = React.forwardRef((props: { title: string, subtitle?: string, value?: string, showArrow?: boolean, onClick: () => void }, ref: any) => { @@ -107,9 +111,44 @@ const BooleanSettingsRow = (props: { title: string, value: boolean, onChange?: ( } -const Subheader = (props: { title: string }) => { +const Subheader = React.memo((props: { title: string }) => { return {props.title} -} +}) + + +const ServiceQuotaMeter = React.memo((props: { serviceKey: string }) => { + + const [isLoading, setIsLoading] = useState(false) + const [leftQuota, setLeftQuota] = useState(Number.MAX_SAFE_INTEGER) + const [quotaResetAt, setQuotaResetAt] = useState(Number.NaN) + + const service = useMemo(() => DataServiceManager.instance.getServiceByKey(props.serviceKey), [props.serviceKey]) + + const quotaLimited = useMemo(() => service.isQuotaLimited, [service]) + + const reloadQuotaInfo = useCallback(async () => { + if (quotaLimited === true) { + setIsLoading(true) + setLeftQuota(await service.getLeftQuota()) + setQuotaResetAt(await service.getQuotaResetEpoch()) + setIsLoading(false) + } + }, [quotaLimited, service]) + + useEffect(() => { + reloadQuotaInfo() + }, []) + + if (quotaLimited === true) { + return + Service Quota + { + quotaResetAt >= Date.now() ? {leftQuota} Calls left (refilled at {format(quotaResetAt, "h:mm a")}). + : Full quota left. + } + + } else return null +}) interface Props { @@ -159,7 +198,7 @@ class SettingsScreen extends React.PureComponent{ } }) } else if (Platform.OS === 'android') { - UIManager.showPopupMenu(findNodeHandle(this.unitRowRef.current as any), selections, ()=>{}, (item, buttonIndex) => { + UIManager.showPopupMenu(findNodeHandle(this.unitRowRef.current as any), selections, () => { }, (item, buttonIndex) => { this.props.setUnitType(buttonIndex) }) } @@ -296,6 +335,7 @@ class SettingsScreen extends React.PureComponent{ + { __DEV__ === true ? diff --git a/src/measure/service/DataService.ts b/src/measure/service/DataService.ts index 5e6016c3..fe9e5453 100644 --- a/src/measure/service/DataService.ts +++ b/src/measure/service/DataService.ts @@ -14,6 +14,11 @@ export interface ServiceActivationResult { error?: any } +export enum ServiceApiErrorType{ + CredentialError="credential", + QuotaLimitReached="quota-limit", +} + export abstract class DataService { static readonly STORAGE_PREFIX = "@source_service:" @@ -136,6 +141,10 @@ export abstract class DataService { abstract onSystemExit(): Promise + abstract isQuotaLimited: boolean + abstract getLeftQuota(): Promise + abstract getQuotaResetEpoch(): Promise + protected abstract exportToCsv(): Promise> async exportData(): Promise { diff --git a/src/measure/service/fitbit/FitbitService.ts b/src/measure/service/fitbit/FitbitService.ts index ad55bc77..3f72aa3e 100644 --- a/src/measure/service/fitbit/FitbitService.ts +++ b/src/measure/service/fitbit/FitbitService.ts @@ -372,4 +372,17 @@ export class FitbitService extends DataService { get getToday(){ return this.core.getToday } + + get isQuotaLimited(): boolean{ + return this.core.isQuotaLimited + } + + getLeftQuota(): Promise{ + return this.core.getLeftQuota() + } + + getQuotaResetEpoch(): Promise{ + return this.core.getQuotaResetEpoch() + } + } diff --git a/src/measure/service/fitbit/core/FitbitExampleServiceCore.ts b/src/measure/service/fitbit/core/FitbitExampleServiceCore.ts index 6c5a31e3..c7a5c6aa 100644 --- a/src/measure/service/fitbit/core/FitbitExampleServiceCore.ts +++ b/src/measure/service/fitbit/core/FitbitExampleServiceCore.ts @@ -26,6 +26,8 @@ interface ExampleDayRow { } export class FitbitExampleServiceCore implements FitbitServiceCore { + descriptionOverride?: string; + thumbnailOverride?: any; nameOverride = "Fitbit (Example Data)" keyOverride = "fitbit_example" @@ -204,4 +206,12 @@ export class FitbitExampleServiceCore implements FitbitServiceCore { scale: this.latestDate } } + + isQuotaLimited: boolean = false + getLeftQuota(): Promise { + return Promise.resolve(Number.MAX_SAFE_INTEGER) + } + getQuotaResetEpoch(): Promise { + return Promise.resolve(Number.NaN) + } } \ No newline at end of file diff --git a/src/measure/service/fitbit/core/FitbitOfficialServiceCore.ts b/src/measure/service/fitbit/core/FitbitOfficialServiceCore.ts index 560edf10..565e0102 100644 --- a/src/measure/service/fitbit/core/FitbitOfficialServiceCore.ts +++ b/src/measure/service/fitbit/core/FitbitOfficialServiceCore.ts @@ -2,11 +2,12 @@ import { refresh, authorize, revoke, AuthConfiguration } from 'react-native-app- import { FitbitServiceCore, FitbitDailyActivityHeartRateQueryResult, FitbitDailyActivityStepsQueryResult, FitbitWeightTrendQueryResult, FitbitWeightQueryResult, FitbitSleepQueryResult, FitbitIntradayStepDayQueryResult, FitbitHeartRateIntraDayQueryResult, FitbitUserProfile, FitbitDeviceListQueryResult } from "../types"; import { makeFitbitIntradayActivityApiUrl, makeFitbitHeartRateIntraDayLogApiUrl, makeFitbitWeightTrendApiUrl, makeFitbitWeightLogApiUrl, makeFitbitDayLevelActivityLogsUrl, makeFitbitSleepApiUrl, FITBIT_PROFILE_URL, FITBIT_DEVICES_URL } from "../api"; import { LocalAsyncStorageHelper } from "@utils/AsyncStorageHelper"; -import { DataService, UnSupportedReason } from "../../DataService"; +import { DataService, UnSupportedReason, ServiceApiErrorType } from "../../DataService"; import { DateTimeHelper } from "@utils/time"; import { FitbitLocalDbManager } from '../sqlite/database'; import { DatabaseParams } from 'react-native-sqlite-storage'; import { parseISO, max } from 'date-fns'; +import { SystemError } from '@utils/errors'; interface FitbitCredential { @@ -22,7 +23,24 @@ const STORAGE_KEY_USER_TIMEZONE = const STORAGE_KEY_USER_MEMBER_SINCE = DataService.STORAGE_PREFIX + 'fitbit:user_memberSince'; +const STORAGE_KEY_LEFT_QUOTA = DataService.STORAGE_PREFIX + 'fitbit:left_quota'; +const STORAGE_KEY_QUOTA_RESET_AT = DataService.STORAGE_PREFIX + 'fitbit:quota_reset_at'; + + export class FitbitOfficialServiceCore implements FitbitServiceCore { + keyOverride?: string; + nameOverride?: string; + descriptionOverride?: string; + thumbnailOverride?: any; + + readonly isQuotaLimited: boolean = true + + getLeftQuota(): Promise { + return this.localAsyncStorage.getInt(STORAGE_KEY_LEFT_QUOTA) + } + getQuotaResetEpoch(): Promise { + return this.localAsyncStorage.getLong(STORAGE_KEY_QUOTA_RESET_AT) + } private _credential: FitbitCredential = null; private _authConfig: AuthConfiguration = null; @@ -172,10 +190,13 @@ export class FitbitOfficialServiceCore implements FitbitServiceCore { }, }).then(async result => { const quota = result.headers.get('Fitbit-Rate-Limit-Limit'); - const remainedCalls = result.headers.get('Fitbit-Rate-Limit-Remaining'); - const secondsLeftToNextReset = result.headers.get( + const remainedCalls = Number.parseInt(result.headers.get('Fitbit-Rate-Limit-Remaining')); + const secondsLeftToNextReset = Number.parseInt(result.headers.get( 'Fitbit-Rate-Limit-Reset', - ); + )); + + await this.localAsyncStorage.set(STORAGE_KEY_LEFT_QUOTA, remainedCalls) + await this.localAsyncStorage.set(STORAGE_KEY_QUOTA_RESET_AT, secondsLeftToNextReset * 1000 + Date.now()) if (result.ok === false) { const json = await result.json(); @@ -186,15 +207,10 @@ export class FitbitOfficialServiceCore implements FitbitServiceCore { 'Fitbit token is expired. refresh token and try once again.', ); return this.authenticate().then(() => this.fetchFitbitQuery(url)); - } else throw { error: 'Access token invalid.' }; + } else throw new SystemError(ServiceApiErrorType.CredentialError, "Access token invalid."); case 429: - throw { - error: - 'Fitbit quota limit reached. Next reset: ' + - secondsLeftToNextReset + - ' secs.', - }; + throw new SystemError(ServiceApiErrorType.QuotaLimitReached, "Quota limit reached.") default: throw { error: result.status }; } diff --git a/src/measure/service/fitbit/types.ts b/src/measure/service/fitbit/types.ts index 315af053..5c96d771 100644 --- a/src/measure/service/fitbit/types.ts +++ b/src/measure/service/fitbit/types.ts @@ -196,4 +196,9 @@ export interface FitbitServiceCore { localAsyncStorage: LocalAsyncStorageHelper getToday: () => Date + + isQuotaLimited: boolean + getLeftQuota(): Promise + getQuotaResetEpoch(): Promise + } \ No newline at end of file diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 00000000..97d8c1ce --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,6 @@ +export class SystemError{ + constructor( + readonly type: string, + readonly message: string | undefined = undefined, + readonly payload: any | undefined = undefined){} +} \ No newline at end of file