Skip to content

Commit

Permalink
#45 Show quota limit of the service in settings page.
Browse files Browse the repository at this point in the history
  • Loading branch information
yghokim committed Mar 28, 2020
1 parent d53c044 commit 3f3cca0
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 17 deletions.
52 changes: 46 additions & 6 deletions src/components/pages/settings/SettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 = [{
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -107,9 +111,44 @@ const BooleanSettingsRow = (props: { title: string, value: boolean, onChange?: (
</View>
}

const Subheader = (props: { title: string }) => {
const Subheader = React.memo((props: { title: string }) => {
return <Text style={styles.subheaderStyle}>{props.title}</Text>
}
})


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 <View style={styles.rowContainerStyleNormalPadding}>
<Text style={styles.rowTitleStyle}>Service Quota</Text>
{
quotaResetAt >= Date.now() ? <Text style={styles.smallTextStyle}>{leftQuota} Calls left (refilled at {format(quotaResetAt, "h:mm a")}).</Text>
: <Text style={styles.smallTextStyle}>Full quota left.</Text>
}
</View>
} else return null
})


interface Props {
Expand Down Expand Up @@ -159,7 +198,7 @@ class SettingsScreen extends React.PureComponent<Props, State>{
}
})
} 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)
})
}
Expand Down Expand Up @@ -296,6 +335,7 @@ class SettingsScreen extends React.PureComponent<Props, State>{
<Subheader title={"Measure Data Source"} />
<SettingsRow title="Service" value={DataServiceManager.instance.getServiceByKey(this.props.selectedServiceKey).name}
onClick={this.onPressServiceButton} />
<ServiceQuotaMeter serviceKey={this.props.selectedServiceKey} />
<SettingsRow title="Refresh all data cache" onClick={this.onPressRefreshAllCache} showArrow={false} />
{
__DEV__ === true ?
Expand Down
9 changes: 9 additions & 0 deletions src/measure/service/DataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:"

Expand Down Expand Up @@ -136,6 +141,10 @@ export abstract class DataService {

abstract onSystemExit(): Promise<void>

abstract isQuotaLimited: boolean
abstract getLeftQuota(): Promise<number>
abstract getQuotaResetEpoch(): Promise<number>

protected abstract exportToCsv(): Promise<Array<{ name: string, csv: string }>>

async exportData(): Promise<boolean> {
Expand Down
13 changes: 13 additions & 0 deletions src/measure/service/fitbit/FitbitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,4 +372,17 @@ export class FitbitService extends DataService {
get getToday(){
return this.core.getToday
}

get isQuotaLimited(): boolean{
return this.core.isQuotaLimited
}

getLeftQuota(): Promise<number>{
return this.core.getLeftQuota()
}

getQuotaResetEpoch(): Promise<number>{
return this.core.getQuotaResetEpoch()
}

}
10 changes: 10 additions & 0 deletions src/measure/service/fitbit/core/FitbitExampleServiceCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ interface ExampleDayRow {
}

export class FitbitExampleServiceCore implements FitbitServiceCore {
descriptionOverride?: string;
thumbnailOverride?: any;

nameOverride = "Fitbit (Example Data)"
keyOverride = "fitbit_example"
Expand Down Expand Up @@ -204,4 +206,12 @@ export class FitbitExampleServiceCore implements FitbitServiceCore {
scale: this.latestDate
}
}

isQuotaLimited: boolean = false
getLeftQuota(): Promise<number> {
return Promise.resolve(Number.MAX_SAFE_INTEGER)
}
getQuotaResetEpoch(): Promise<number> {
return Promise.resolve(Number.NaN)
}
}
38 changes: 27 additions & 11 deletions src/measure/service/fitbit/core/FitbitOfficialServiceCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<number> {
return this.localAsyncStorage.getInt(STORAGE_KEY_LEFT_QUOTA)
}
getQuotaResetEpoch(): Promise<number> {
return this.localAsyncStorage.getLong(STORAGE_KEY_QUOTA_RESET_AT)
}

private _credential: FitbitCredential = null;
private _authConfig: AuthConfiguration = null;
Expand Down Expand Up @@ -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();
Expand All @@ -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 };
}
Expand Down
5 changes: 5 additions & 0 deletions src/measure/service/fitbit/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,9 @@ export interface FitbitServiceCore {
localAsyncStorage: LocalAsyncStorageHelper

getToday: () => Date

isQuotaLimited: boolean
getLeftQuota(): Promise<number>
getQuotaResetEpoch(): Promise<number>

}
6 changes: 6 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class SystemError{
constructor(
readonly type: string,
readonly message: string | undefined = undefined,
readonly payload: any | undefined = undefined){}
}

0 comments on commit 3f3cca0

Please sign in to comment.