5 hours ago
5 hours ago
It works up to the point when users can select which permissions they want to grant. After clicking next. I get the error, No token in response. What's wrong?
My code
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as AuthSession from "expo-auth-session";
import { StatusBar } from "expo-status-bar";
import * as WebBrowser from "expo-web-browser";
import React, { JSX, useEffect, useState } from "react";
import {
ActivityIndicator,
Alert,
RefreshControl,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
WebBrowser.maybeCompleteAuthSession();
// Fitbit OAuth endpoints
const discovery = {
authorizationEndpoint: "https://www.fitbit.com/oauth2/authorize",
tokenEndpoint: "https://api.fitbit.com/oauth2/token",
};
// Client ID
const CLIENT_ID = "23QF62";
// const redirectUri = "https://auth.expo.dev/@gbemiga/predict_health";
const redirectUri = AuthSession.makeRedirectUri({
// For development (using Expo's proxy)
scheme: __DEV__ ? "exp" : "predicthealth",
path: "redirect",
// For production/standalone apps
native: "predicthealth://redirect",
});
console.log("Using redirect URI:", redirectUri);
type ZoneName = "out of range" | "fat burn" | "cardio" | "peak";
interface StepsDataPoint {
dateTime: string;
value: string;
}
interface StepsData {
"activities-steps": StepsDataPoint[];
}
interface HeartRateZone {
caloriesOut: number;
max: number;
min: number;
minutes: number;
name: string;
}
interface HeartRateValue {
customHeartRateZones: HeartRateZone[];
heartRateZones: HeartRateZone[];
restingHeartRate?: number;
}
interface HeartRateDataPoint {
dateTime: string;
value: HeartRateValue;
}
interface HeartRateData {
"activities-heart": HeartRateDataPoint[];
}
interface UserProfileData {
user: {
displayName: string;
encodedId: string;
fullName: string;
};
}
export default function HomeScreen() {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [stepsData, setStepsData] = useState<StepsData | null>(null);
const [heartRateData, setHeartRateData] = useState<HeartRateData | null>(
null
);
const [userProfile, setUserProfile] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: CLIENT_ID,
scopes: ["activity", "heartrate", "profile"],
redirectUri,
usePKCE: true,
responseType: "code",
extraParams: {
prompt: "consent",
},
},
discovery
);
useEffect(() => {
loadAccessToken();
}, []);
useEffect(() => {
if (response?.type === "success") {
const code = response.params.code;
if (code) exchangeCodeForToken(code);
}
}, [response]);
const loadAccessToken = async () => {
const token = await AsyncStorage.getItem("fitbit_access_token");
if (token) {
setAccessToken(token);
fetchHealthData(token);
}
};
const saveAccessToken = async (token: string) => {
await AsyncStorage.setItem("fitbit_access_token", token);
};
const exchangeCodeForToken = async (code: string) => {
setIsLoading(true);
try {
console.log("Exchanging code for token...");
console.log("Using redirect URI:", redirectUri);
const tokenResponse = await AuthSession.exchangeCodeAsync(
{
clientId: CLIENT_ID,
code,
redirectUri,
},
discovery
);
console.log("Token response:", tokenResponse);
if (tokenResponse.accessToken) {
setAccessToken(tokenResponse.accessToken);
await saveAccessToken(tokenResponse.accessToken);
fetchHealthData(tokenResponse.accessToken);
} else {
console.error("No access token in response");
Alert.alert("Error", "Failed to get access token. Please try again.");
}
} catch (err: unknown) {
if (err instanceof Error) {
console.error("Token exchange failed:", err);
Alert.alert(
"Authentication Failed",
err.message || "Could not authenticate with Fitbit"
);
} else {
console.error("Unknown error:", err);
}
} finally {
setIsLoading(false);
}
};
const fetchHealthData = async (token: string) => {
if (!token) return;
setIsLoading(true);
try {
await Promise.all([
fetchStepsData(token),
fetchHeartRateData(token),
fetchUserProfile(token),
]);
} catch (e) {
Alert.alert("Error", "Failed to fetch health data");
} finally {
setIsLoading(false);
}
};
const fetchStepsData = async (token: string) => {
const today = new Date().toISOString().split("T")[0];
const res = await fetch(
`https://api.fitbit.com/1/user/-/activities/steps/date/${today}/1d.json`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (res.ok) {
const data: StepsData = await res.json();
setStepsData(data);
} else if (res.status === 401) {
logout();
Alert.alert("Session expired", "Please log in again");
} else throw new Error("Steps fetch failed");
};
const authenticateWithFitbit = async () => {
try {
// First try with the standard promptAsync
const authResult = await promptAsync();
if (authResult?.type === "success") {
await exchangeCodeForToken(authResult.params.code);
} else if (authResult?.type === "error") {
console.error("Auth error:", authResult?.error);
Alert.alert(
"Error",
authResult?.error?.description || "Authentication failed"
);
}
} catch (error) {
console.error("Auth session error:", error);
// Fallback to direct WebBrowser approach if promptAsync fails
try {
const authUrl =
`https://www.fitbit.com/oauth2/authorize?` +
`response_type=code&` +
`client_id=${CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`scope=activity%20heartrate%20profile&` +
`prompt=consent`;
const result = await WebBrowser.openAuthSessionAsync(
authUrl,
redirectUri
);
if (result.type === "success") {
const url = result.url;
const params = new URLSearchParams(url.split("?")[1]);
const code = params.get("code");
if (code) await exchangeCodeForToken(code);
}
} catch (fallbackError) {
console.error("Fallback auth failed:", fallbackError);
Alert.alert(
"Error",
"Could not connect to Fitbit. Please try again later."
);
}
}
};
const fetchHeartRateData = async (token: string) => {
const today = new Date().toISOString().split("T")[0];
const res = await fetch(
`https://api.fitbit.com/1/user/-/activities/heart/date/${today}/1d.json`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (res.ok) {
const data: HeartRateData = await res.json();
setHeartRateData(data);
} else if (res.status === 401) {
logout();
Alert.alert("Session expired", "Please log in again");
} else throw new Error("Heart rate fetch failed");
};
const fetchUserProfile = async (token: string) => {
const res = await fetch("https://api.fitbit.com/1/user/-/profile.json", {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data: UserProfileData = await res.json();
setUserProfile(data.user.displayName);
} else if (res.status === 401) {
logout();
Alert.alert("Session expired", "Please log in again");
}
};
const onRefresh = async () => {
setRefreshing(true);
if (accessToken) await fetchHealthData(accessToken);
setRefreshing(false);
};
const logout = async () => {
await AsyncStorage.removeItem("fitbit_access_token");
setAccessToken(null);
setStepsData(null);
setHeartRateData(null);
setUserProfile(null);
};
const getZoneColor = (zoneName: string): string => {
const name = zoneName.toLowerCase() as ZoneName;
switch (name) {
case "out of range":
return "#4CAF50";
case "fat burn":
return "#FF9800";
case "cardio":
return "#FF5722";
case "peak":
return "#E91E63";
default:
return "#ccc";
}
};
const formatNumber = (num: string | number): string =>
typeof num === "string"
? parseInt(num).toLocaleString()
: num.toLocaleString();
5 hours ago
5 hours ago
Here's my log
Android Bundled 6328ms node_modules\expo-router\entry.js (1 module)
LOG Using redirect URI: predicthealth://redirect
LOG Using redirect URI: predicthealth://redirect
LOG Exchanging code for token...
LOG Using redirect URI: predicthealth://redirect
LOG Exchanging code for token...
LOG Using redirect URI: predicthealth://redirect
LOG Token response: {"accessToken": undefined, "expiresIn": undefined, "idToken": undefined, "issuedAt": 1750167189, "rawResponse": {"errors": [[Object]], "success": false}, "refreshToken": undefined, "scope": undefined, "state": undefined, "tokenType": "bearer"}
ERROR No access token in response
LOG Token response: {"accessToken": undefined, "expiresIn": undefined, "idToken": undefined, "issuedAt": 1750167190, "rawResponse": {"errors": [[Object]], "success": false}, "refreshToken": undefined, "scope": undefined, "state": undefined, "tokenType": "bearer"}
ERROR No access token in response