You may want to have your own custom Link widget to match your app’s UI. This is useful for customers who don’t want to use Vital link but still want to use the Vital API. It’s worthwhile ensuring that you have read the Auth Types documentation before proceeding. A full code example in React Native can be seen here.

An overview of the process is as follows:

  1. Get a list of all available providers “/v2/providers”
  2. Build your UI using this list of providers
  3. When selecting a provider, generate a link token for that provider “/v2/link/token”
  4. Use the link token to generate an oauth link, or authorize an email, or email and password.

1. List of all available providers

The first step to creating a custom widget is to make a call to the providers endpoint. This will return a list of all available providers. You can then use this list to build your UI.

import { VitalClient, VitalEnvironment } from "@tryvital/vital-node";
import {
  LinkTokenExchange,
  LinkGenerateOauthLinkRequest,
  OAuthProviders,
} from "@tryvital/vital-node/api";

const client = new VitalClient({
  apiKey: "<my_api_key>",
  environment: VitalEnvironment.Sandbox,
});

const allProviders = await client.providers.get_all();

// allProviders
// [
//   {
//     "name": "Oura",
//     "slug": "oura",
//     "logo": "https://logo_url.com",
//     "auth_type": "oauth",
//     "supported_resources": ["workouts", "sleep"]
//   }
// ]

2. Build your UI in your app

Once you have a list of all available providers, you can build your UI. You can use the logo field to display the provider’s logo. You can use the name field to display the provider’s name. You can use the supported_resources field to display the resources that the provider supports.

Custom UI

Building a custom UI

Once a user has selected a provider, you can generate a link token. This link token is used to generate an oauth link, or authorize an email, or email and password. You can read more about the link token here.

import { VitalClient, VitalEnvironment } from "@tryvital/vital-node";
import {
  LinkTokenExchange,
  LinkGenerateOauthLinkRequest,
  OAuthProviders,
} from "@tryvital/vital-node/api";

const client = new VitalClient({
  apiKey: "<my_api_key>",
  environment: VitalEnvironment.Sandbox,
});

const request: LinkTokenExchange = {
  userId: "<user_id>",
};
const tokenResponse = await client.link.token(request);

Once you have a link token, you can use it to generate an oauth link, or authorize an email, or email and password. You can read more about the link token here.

Connect an oauth provider

import { VitalClient, VitalEnvironment } from '@tryvital/vital-node';
import { LinkTokenExchange, LinkGenerateOauthLinkRequest, OAuthProviders } from '@tryvital/vital-node/api';


const client = new VitalClient({
    apiKey: '<my_api_key>',
    environment: VitalEnvironment.Sandbox,
});

const request: LinkTokenExchange = {
    userId: "<user_id>",
}
const tokenResponse = await client.link.token(request)

const linkRequest: LinkGenerateOauthLinkRequest = {
    vitalLinkToken: tokenResponse.linkToken,
}

const auth = await client.link.generateOauthLink(
    OAuthProviders.<provider>,
    linkRequest
)

Connact an Email/Password provider

import { VitalClient, VitalEnvironment } from '@tryvital/vital-node';
import { IndividualProviderData, PasswordProviders } from '@tryvital/vital-node/api';

const request: IndividualProviderData = {
    username: '<username>',
    password: '<password>',
}
const data = await client.link.connectPasswordProvider(PasswordProviders.<provider>, request)

Bringing this all together

Below is an example of how you can bring all of this together to build your own custom widget. This example is in React Native, but the same principles apply to any language.

React Native
import React, {useEffect, useState} from 'react';
import {
  SafeAreaView,
  StatusBar,
  Linking,
  View,
  useColorScheme,
  Platform,
} from 'react-native';
import {VitalHealth, VitalResource} from '@tryvital/vital-health-react-native';

const handleOAuth = async (userId: string, item: Provider) => {
  const linkToken = await Client.Link.getLinkToken(
    userId,
    `${AppConfig.slug}://link`,
  );
  const link = await Client.Providers.getOauthUrl(item.slug, linkToken.link_token);

  Linking.openURL(link.oauth_url);
};

const ListItem = ({
  userId,
  item,
  navigation,
}: {
  userId: string;
  item: Provider;
  navigation: any;
}) => {
  const {colors} = useTheme();
  const isDarkMode = useColorScheme() === 'dark';
  const [isLoading, setLoading] = useState(false);

  const handleNativeHealthKit = async () => {
    const providerSlug =
      Platform.OS == 'ios'
        ? ManualProviderSlug.AppleHealthKit
        : ManualProviderSlug.HealthConnect;

    setLoading(true);

    const isHealthSDKAvailable = await VitalHealth.isAvailable();
    if (!isHealthSDKAvailable) {
      console.warn('Health Connect is not available on this device.');
      navigation.navigate('ConnectionCallback', {
        state: 'failed',
        provider: providerSlug,
      });
      return;
    }

    try {
      await VitalHealth.configure({
        logsEnabled: true,
        numberOfDaysToBackFill: 30,
        androidConfig: {syncOnAppStart: true},
        iOSConfig: {
          dataPushMode: 'automatic',
          backgroundDeliveryEnabled: true,
        },
      });
    } catch (e) {
      setLoading(false);
      console.warn(`Failed to configure ${providerSlug}`, e);
      navigation.navigate('ConnectionCallback', {
        state: 'failed',
        provider: providerSlug,
      });
    }
    await VitalHealth.setUserId(userId);
    try {
      await VitalHealth.askForResources([
        VitalResource.Steps,
        VitalResource.Activity,
        VitalResource.HeartRate,
        VitalResource.Sleep,
        VitalResource.Workout,
        VitalResource.BloodPressure,
        VitalResource.Glucose,
        VitalResource.Body,
        VitalResource.Profile,
        VitalResource.ActiveEnergyBurned,
        VitalResource.BasalEnergyBurned,
      ]);
      await VitalCore.createConnectedSourceIfNotExist(providerSlug);
      setLoading(false);
      navigation.navigate('ConnectionCallback', {
        state: 'success',
        provider: providerSlug,
      });
    } catch (e) {
      setLoading(false);
      navigation.navigate('ConnectionCallback', {
        state: 'failed',
        provider: providerSlug,
      });
    }
  };

  const onPress = async () => {
    if (item.auth_type === 'oauth') {
      await handleOAuth(userId, item);
    } else if (
      item.slug === 'apple_health_kit' ||
      item.slug === 'health_connect'
    ) {
      await handleNativeHealthKit();
    }
  };

  return (
    <Pressable onPress={() => onPress()}>
      {({isHovered, isFocused, isPressed}) => {
        return (
        <HStack space={'md'} justifyContent="flex-start">
            <VStack>
            <Text color={colors.text} fontType="medium">
                {item.name}
            </Text>
            <Text
                fontType="regular"
                color={colors.secondary}
                size="xs"
                flexShrink={1}
                flexWrap="wrap">
                {item.description}
            </Text>
            </VStack>
        </HStack>
        );
      }}
    </Pressable>
  );
};

export const LinkDeviceScreen = ({navigation}) => {
  const isDarkMode = useColorScheme() === 'dark';
  const {colors} = useTheme();
  const styles = makeStyles(colors);
  const [providers, setProviders] = React.useState<Provider[]>([]);
  const [devices, setDevices] = React.useState<Provider[]>([]);
  const [loading, setLoading] = React.useState<boolean>(false);
  const [searchText, setSearchText] = React.useState('');
  const [error, setError] = React.useState<string | null>(null);

  const [userId, setUserId] = React.useState('');

  const handleSearch = (text: string) => {
    setSearchText(text);
    if (text === '' && text.length <= 2) {
      setDevices(providers);
    } else {
      const filtered = providers.filter(item =>
        item.name.toLowerCase().includes(text.toLowerCase()),
      );
      setDevices(filtered);
    }
  };
  useEffect(() => {
    const getDevices = async () => {
      setLoading(true);
      setError(null);
      const user_id = await getData('user_id');
      if (user_id) {
        const providers = await Client.Providers.getProviders();
        setLoading(false);
        setUserId(user_id);
        setProviders(providers);
      } else {
        setUserId('');
        setLoading(false);
        console.warn('Failed to get all supported providers');
        setError('Failed to get devices');
      }
    };
  }, [navigation]);

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar
        barStyle={isDarkMode ? 'light-content' : 'dark-content'}
        backgroundColor={styles.container.backgroundColor}
      />
      <View
        style={{
          paddingVertical: 10,
          paddingHorizontal: 16,
          flex: 1,
          width: '100%',
        }}>
        <VStack pb={10}>
          <HStack
            justifyContent={'space-between'}
            py={'$3'}
            alignItems="center">
            <H1>Connect a Device</H1>
            <Button onPress={() => navigation.goBack()} variant="link">
              <Ionicons name="close-outline" size={25} color={colors.text} />
            </Button>
          </HStack>
          <Input
            variant="outline"
            size="lg"
            isDisabled={false}
            isInvalid={false}
            isReadOnly={false}>
            <InputField
              color={colors.text}
              fontFamily={AppConfig.fonts.regular}
              onChangeText={handleSearch}
              value={searchText}
              placeholder="Search"
            />
          </Input>
        </VStack>

        {!loading && error ? (
          <VStack>
            <Text fontType="light">Failed to get supported Providers</Text>
          </VStack>
        ) : (
          <FlatList
            data={devices}
            renderItem={({item}) => (
              <ListItem userId={userId} item={item} navigation={navigation} />
            )}
            keyExtractor={item => item.slug}
          />
        )}
      </View>
    </SafeAreaView>
  );
};