Integrate Quick Scan
The API below is used for Quick Scan.
Make sure the integration setup done before this implementation
/**
* Begin food detection using the device's camera.
* @param options - An object to determine which types of scanning should be performed.
* @param callback - A callback to repeatedly receive food detection events as they occur.
* @returns A `Subscription` that should be retained by the caller while food detection is running. Call `remove` on the subscription to terminate food detection.
*/
startFoodDetection(
options: FoodDetectionConfig,
callback: (detection: FoodDetectionEvent) => void
): Subscription
/**
* Look up the food item result for a given Passio ID.
* @param passioID - The Passio ID for the query.
* @returns A `Promise` resolving to a `PassioFoodItem` object if the record exists in the database or `null` if not.
*/
fetchFoodItemForPassioID(passioID: PassioID): Promise<PassioFoodItem | null>
/**
* Look up the food item result for a given by barcode or packagedFoodCode.
* @param barcode - barcode for the query.
* or
* @param packageFoodCode - packageFoodCode for the query.
* @returns A `Promise` resolving to a `PassioFoodItem` object if the record exists in the database or `null` if not.
*/
fetchFoodItemForProductCode(
code: Barcode | PackagedFoodCode
): Promise<PassioFoodItem | null>
Example
useQuickScan
import { useEffect, useRef, useState, useCallback } from 'react'
import {
PassioSDK,
type FoodDetectionConfig,
type FoodDetectionEvent,
PassioFoodItem,
DetectedCandidate,
} from '@passiolife/nutritionai-react-native-sdk-v3'
/**
* Custom hook for handling quick food scanning using PassioSDK.
* It provides functions and state variables related to food detection and alternative food items.
*/
export const useQuickScan = () => {
// State variables
const [passioFoodItem, setPassioFoodItem] = useState<PassioFoodItem | null>(
null
)
const [alternative, setAlternativePassioIDAttributes] = useState<
DetectedCandidate[] | null | undefined
>(null)
const [loading, setLoading] = useState(true)
const passioFoodItemRef = useRef<PassioFoodItem | null>(null)
// Function to clear the scanning results
const onClearResultPress = () => {
setLoading(true)
passioFoodItemRef.current = null
setPassioFoodItem(null)
setAlternativePassioIDAttributes(null)
}
useEffect(() => {
// Function to handle food detection events
const handleFoodDetection = async (detection: FoodDetectionEvent) => {
const { candidates } = detection
// If no candidates available, return
if (!candidates) {
return
}
let attributes: PassioFoodItem | null = null
// Determine the type of food detection and fetch attributes accordingly
if (candidates.barcodeCandidates?.[0]) {
const barcode = candidates.barcodeCandidates[0].barcode
attributes = await PassioSDK.fetchFoodItemForProductCode(barcode)
} else if (candidates.packagedFoodCode?.[0]) {
const packagedFoodCode = candidates.packagedFoodCode[0]
attributes = await PassioSDK.fetchFoodItemForProductCode(
packagedFoodCode
)
} else if (candidates.detectedCandidates?.[0]) {
const passioID = candidates.detectedCandidates[0].passioID
attributes = await PassioSDK.fetchFoodItemForPassioID(passioID)
}
// If attributes are null, return
if (attributes === null) {
return
}
// Check if the detected food is different from the previous one
if (attributes?.id !== passioFoodItemRef.current?.id) {
passioFoodItemRef.current = attributes
// Update state variables and fetch alternative food items
setPassioFoodItem((prev) => {
if (attributes?.id === prev?.id) {
return prev
} else {
setAlternativePassioIDAttributes(
candidates.detectedCandidates[0]?.alternatives
)
return attributes
}
})
setLoading(false)
}
}
// Configuration for food detection
const config: FoodDetectionConfig = {
detectBarcodes: true,
detectPackagedFood: true,
}
// Start food detection and subscribe to events
const subscription = PassioSDK.startFoodDetection(
config,
handleFoodDetection
)
// Cleanup function to unsubscribe when the component unmounts
return () => subscription.remove()
}, []) // Empty dependency array to run the effect only once during component mount
// Function to handle changes in alternative food items
const onAlternativeFoodItemChange = useCallback(
async (attribute: DetectedCandidate) => {
const alternatePassioFoodItem = await PassioSDK.fetchFoodItemForPassioID(
attribute.passioID
)
if (alternatePassioFoodItem) {
passioFoodItemRef.current = alternatePassioFoodItem
setPassioFoodItem(alternatePassioFoodItem)
}
},
[]
)
// Return the hook's public API
return {
loading,
passioFoodItem,
onAlternativeFoodItemChange,
onClearResultPress,
alternative,
}
}
QuickScanningScreen
import {
ActivityIndicator,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import {
DetectionCameraView,
PassioFoodItem,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import { useQuickScan } from './useQuickScan'
import React from 'react'
import { QuickFoodResult } from './view/QuickFoodResult'
interface Props {
onClose: () => void
onFoodDetail: (passioFoodItem: PassioFoodItem) => void
}
export const QuickScanningScreen = ({ onClose, onFoodDetail }: Props) => {
const {
loading,
passioFoodItem,
onClearResultPress,
alternative,
onAlternativeFoodItemChange,
} = useQuickScan()
const styles = quickScanStyle()
return (
<View style={styles.blackBackgroundStyle}>
<DetectionCameraView style={styles.detectionCamera} />
{loading ? (
<View style={styles.loadingIndicator}>
<ActivityIndicator />
<Text>Scanning...</Text>
</View>
) : null}
{passioFoodItem !== null ? (
<QuickFoodResult
attribute={passioFoodItem}
onAlternativeFoodItemChange={onAlternativeFoodItemChange}
onClearResultPress={onClearResultPress}
alternativeAttributes={alternative ?? []}
onItemClick={onFoodDetail}
/>
) : null}
<View style={styles.closeButton}>
<TouchableOpacity onPress={onClose}>
<Text style={styles.text}>✕</Text>
</TouchableOpacity>
</View>
</View>
)
}
const quickScanStyle = () =>
StyleSheet.create({
detectionCamera: {
flex: 1,
width: '100%',
},
blackBackgroundStyle: {
backgroundColor: 'black',
width: '100%',
flex: 1,
flexDirection: 'column',
},
loadingIndicator: {
backgroundColor: 'white',
minHeight: 150,
borderTopRightRadius: 24,
alignItems: 'center',
justifyContent: 'center',
borderTopLeftRadius: 24,
position: 'absolute',
bottom: 0,
right: 0,
left: 0,
},
text: {
color: 'white',
fontSize: 30,
},
closeButton: {
position: 'absolute',
top: 45,
right: 25,
zIndex: 1000,
color: 'white',
},
})
QuickFoodResult
import React from 'react'
import { Pressable, StyleSheet, Text, View, Image } from 'react-native'
import {
IconSize,
PassioIconView,
type PassioFoodItem,
DetectedCandidate,
PassioSDK,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import { AlternativeFood } from '../../../src/views/AlternativeFood'
export interface QuickFoodResultProps {
attribute: PassioFoodItem
alternativeAttributes?: DetectedCandidate[]
onAlternativeFoodItemChange?: (item: DetectedCandidate) => void
onClearResultPress?: () => void
onItemClick?: (passioFoodItem: PassioFoodItem) => void
}
export const QuickFoodResult = ({
attribute,
alternativeAttributes,
onAlternativeFoodItemChange,
onClearResultPress,
onItemClick,
}: QuickFoodResultProps) => {
const styles = quickFoodResultStyle()
const nutrients =
PassioSDK.getNutrientsSelectedSizeOfPassioFoodItem(attribute)
return (
<Pressable
onPress={() => {
onItemClick?.(attribute)
}}
style={styles.itemContainer}
>
<View style={styles.foodResult}>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: attribute.iconId ?? attribute.id,
iconSize: IconSize.PX180,
}}
/>
</View>
<View>
<Text style={styles.itemFoodName}>{attribute.name}</Text>
<Text style={styles.itemFoodDetail}>
{Math.round(attribute.amount?.weight?.value ?? 0) +
' ' +
attribute.amount?.weight?.unit}
</Text>
<Text style={styles.itemFoodDetail}>
{Math.round(nutrients.calories?.value ?? 0) +
' ' +
nutrients?.calories?.unit}
</Text>
</View>
</View>
{alternativeAttributes && (
<AlternativeFood
detectedCandidates={alternativeAttributes}
onAlternativeFoodItemChange={onAlternativeFoodItemChange}
/>
)}
<Pressable style={styles.clearResult} onPress={onClearResultPress}>
<Image
source={require('../../assets/close.png')}
style={styles.close}
/>
</Pressable>
</Pressable>
)
}
const quickFoodResultStyle = () =>
StyleSheet.create({
itemContainer: {
position: 'absolute',
bottom: 0,
right: 0,
padding: 16,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
backgroundColor: 'white',
minHeight: 150,
flex: 1,
marginVertical: 0,
left: 0,
},
foodResult: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
close: {
width: 24,
height: 24,
tintColor: 'white',
},
itemFoodName: {
flex: 1,
textTransform: 'capitalize',
paddingHorizontal: 8,
fontSize: 16,
fontWeight: '500',
},
itemFoodDetail: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 12,
},
itemIcon: {
height: 60,
width: 60,
},
itemIconContainer: {
height: 60,
width: 60,
overflow: 'hidden',
borderRadius: 30,
},
clearResult: {
flexDirection: 'row',
backgroundColor: 'red',
borderRadius: 32,
alignItems: 'center',
alignSelf: 'center',
padding: 8,
marginVertical: 16,
},
})
Last updated