Building a Custom Widget
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:
- Get a list of all available providers “/v2/providers”
- Build your UI using this list of providers
- When selecting a provider, generate a link token for that provider “/v2/link/token”
- 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.
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.
Building a custom UI
3. Generate a link token
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.
4. Link the user to the provider
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
Connact an Email/Password provider
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.
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>
);
};