Cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Failed to get access token in Expo development build app

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();



Best Answer
0 Votes
1 REPLY 1

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
Best Answer
0 Votes