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