Skip to main content
Step-by-step implementations for common user journeys with the SmartMenu API.

Flow 1: First-Time User Setup

A new user sets up their dietary preferences for the first time.

Implementation

async function onboardNewUser(chainId, guestId) {
  // 1. Start session - all context bound here
  const { sessionId } = await startSession({
    chainId,
    guestId,
    platform: 'IOS',
    appVersion: '3.2.1'
  });

  // Store session ID for subsequent API calls
  setSessionId(sessionId);

  // 2. Get available options (uses X-Session-ID header)
  const options = await getFilterOptions();

  // 3. Show preference UI and wait for selection
  const preferences = await showPreferenceSetupUI(options);

  // 4. Save preferences locally
  saveUserPreferences(preferences);

  // 5. Fetch personalized menu (uses X-Session-ID header)
  const menu = await searchWithPreferences(preferences);

  return menu;
}

Flow 2: Returning User Quick Order

A returning user with saved preferences browses the menu.
async function loadMenuForReturningUser(chainId, guestId) {
  // 1. Start session - all context bound here
  const { sessionId } = await startSession({
    chainId,
    guestId,
    platform: 'IOS',
    appVersion: '3.2.1'
  });

  setSessionId(sessionId);

  // 2. Load saved preferences
  const preferences = loadUserPreferences();

  // 3. Immediately fetch personalized menu (uses X-Session-ID header)
  const menu = await searchWithPreferences(preferences);

  // 4. Prefetch filter options in background
  prefetchFilterOptions();

  return menu;
}

Flow 3: Browsing with Dynamic Filters

User adjusts filters while browsing the menu.
function useMenuWithFilters() {
  const [preferences, setPreferences] = useState(loadUserPreferences());
  const [menu, setMenu] = useState(null);
  const sessionId = useSession(); // Session already started with chain context

  // Debounced search when preferences change
  useEffect(() => {
    const search = debounce(async () => {
      // Uses X-Session-ID header automatically
      const results = await searchWithPreferences(preferences);
      setMenu(results);
    }, 300);

    search();
    return () => search.cancel();
  }, [preferences, sessionId]);

  const updateDiet = (diet) => {
    setPreferences(prev => ({
      ...prev,
      diets: prev.diets.includes(diet)
        ? prev.diets.filter(d => d !== diet)
        : [...prev.diets, diet]
    }));
  };

  const updateAllergen = (allergen) => {
    setPreferences(prev => ({
      ...prev,
      excludeAllergens: prev.excludeAllergens.includes(allergen)
        ? prev.excludeAllergens.filter(a => a !== allergen)
        : [...prev.excludeAllergens, allergen]
    }));
  };

  return { menu, preferences, updateDiet, updateAllergen };
}

Flow 4: Viewing Dish Details

User taps on a dish to see full details.
async function viewDishDetails(dishId) {
  // Fetch complete dish information (uses X-Session-ID header)
  const dish = await getDishDetails(dishId);

  // Track the view for analytics
  trackEvent('dish_viewed', {
    dishId,
    matchStatus: dish.matchStatus,
    category: dish.category
  });

  return dish;
}

Dish Detail UI Considerations

function DishDetailScreen({ dish }) {
  return (
    <View>
      <Image source={{ uri: dish.imageUrl }} />
      <Text style={styles.name}>{dish.name}</Text>
      <Text style={styles.description}>{dish.description}</Text>

      {/* Allergen warnings */}
      {dish.allergens.length > 0 && (
        <AllergenWarning allergens={dish.allergens} />
      )}

      {/* Nutrition facts */}
      <NutritionLabel nutrition={dish.nutrition} />

      {/* Diet badges */}
      <DietBadges diets={dish.diets} />

      {/* Ingredient list with allergen highlighting */}
      <IngredientList
        ingredients={dish.ingredients}
        highlightAllergens={userPreferences.excludeAllergens}
      />
    </View>
  );
}

Flow 5: Handling “Almost Match” Scenarios

User views a dish that almost matches their preferences.
function AlmostMatchCard({ dish, matchReasons, onViewAnyway }) {
  return (
    <Card style={styles.almostMatch}>
      <DishPreview dish={dish} />

      <WarningBanner>
        <Text>This dish almost matches your preferences:</Text>
        {matchReasons.map(reason => (
          <Text key={reason}>{reason}</Text>
        ))}
      </WarningBanner>

      <Button onPress={() => onViewAnyway(dish.id)}>
        View Anyway
      </Button>
    </Card>
  );
}

Flow 6: Empty Results Handling

No dishes match the user’s strict preferences.
function EmptyResultsScreen({ preferences, onRelaxFilters }) {
  const suggestions = getSuggestions(preferences);

  return (
    <View style={styles.emptyState}>
      <Icon name="filter-off" size={64} />
      <Text style={styles.title}>No exact matches found</Text>
      <Text style={styles.subtitle}>
        Try adjusting your filters to see more options
      </Text>

      {suggestions.map(suggestion => (
        <SuggestionChip
          key={suggestion.id}
          label={suggestion.label}
          onPress={() => onRelaxFilters(suggestion.change)}
        />
      ))}

      <Button variant="secondary" onPress={() => onRelaxFilters('all')}>
        Clear All Filters
      </Button>
    </View>
  );
}

Complete Example: React Native

See the GitHub repository for a complete React Native implementation of these flows.

Download Sample App

Clone the sample app to see these flows in action