Installation & Setup
Get started with CUSS2 React in your project.
1. Install the Package
npm install cuss2-react
2. Import Hooks
import {
Cuss2Connector,
useCuss2,
useBarcodeReader,
useCardReader,
useCamera,
useAnnouncement
} from '@elevationai/cuss2-react';
3. Basic Component Setup
import { Cuss2Connector, Cuss2ConnectionOptions, useCuss2 } from '@elevationai/cuss2-react';
const options: Cuss2ConnectionOptions = {
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
cussUrl: 'cuss-url'
};
function App() {
return (
<Cuss2Connector
options={options}
onConnect={() => console.log('Connected to CUSS2')}
onDisconnect={() => console.log('Disconnected from CUSS2')}
onError={(error) => console.error('Connection error:', error)}
>
<KioskApp />
</Cuss2Connector>
);
}
function KioskApp() {
const { state, connected } = useCuss2();
if (!connected) {
return (
<div className="loading">
<i className="fas fa-spinner fa-spin"></i>
Connecting to CUSS2 platform...
</div>
);
}
return (
<div>
<h1>CUSS2 Platform Ready (State: {state})</h1>
{/* Your app content */}
</div>
);
}
Basic Connection Management
Learn how to manage CUSS2 platform connections effectively.
Connection with Error Handling
import { useEffect, useCallback } from 'react';
import { useCuss2 } from '@elevationai/cuss2-react';
function ConnectionManager() {
const { cuss2, connected, state } = useCuss2();
const handleConnectionError = useCallback((error: any) => {
// Implement retry logic or offline mode
setTimeout(() => {
console.log('Retrying connection...');
// Retry connection logic would go here
}, 5000);
}, []);
// Monitor connection status
useEffect(() => {
if (connected) {
console.log('Successfully connected to CUSS2 platform');
// enableKioskFeatures();
} else {
// disableKioskFeatures();
// showOfflineMessage();
}
}, [connected]);
// Monitor platform state changes
useEffect(() => {
console.log('CUSS2 Platform State:', state);
}, [state]);
const handleRequestState = async (targetState: 'UNAVAILABLE' | 'AVAILABLE' | 'ACTIVE') => {
if (!cuss2) return;
try {
switch (targetState) {
case 'UNAVAILABLE':
await cuss2.requestUnavailableState();
break;
case 'AVAILABLE':
await cuss2.requestAvailableState();
break;
case 'ACTIVE':
await cuss2.requestActiveState();
break;
}
} catch (error) {
console.error(`Failed to request ${targetState} state:`, error);
setErrors(prev => [`Failed to request ${targetState} state: ${error}`, ...prev].slice(0, 20));
}
};
return (
<div>
{connected ? (
<div>Kiosk State { state } </div>
<div className="btn-group mb-3" role="group">
<button
className="btn btn-outline-warning"
onClick={() => handleRequestState('UNAVAILABLE')}
disabled={!connected}
>
Request UNAVAILABLE
</button>
<button
className="btn btn-outline-success"
onClick={() => handleRequestState('AVAILABLE')}
disabled={!connected}
>
Request AVAILABLE
</button>
<button
className="btn btn-outline-primary"
onClick={() => handleRequestState('ACTIVE')}
disabled={!connected || state !== 'AVAILABLE'}
>
Request ACTIVE
</button>
</div>
) : (
<div>Connecting to platform...</div>
)}
</div>
);
}
Component Status Monitoring
Monitor and manage the status of CUSS2 components.
import { useEffect, useMemo } from 'react';
import {
useCuss2,
useBarcodeReader,
useCardReader,
useBoardingPassPrinter,
useScale
} from '@elevationai/cuss2-react';
function ComponentStatusMonitor() {
const { connected } = useCuss2();
const { device: barcodeReader, enabled: barcodeEnabled } = useBarcodeReader();
const { device: cardReader, enabled: cardEnabled } = useCardReader();
const { device: printer, enabled: printerEnabled } = useBoardingPassPrinter();
const { device: scale, enabled: scaleEnabled } = useScale();
const componentStatus = useMemo(() => ({
barcodeReader: { device: barcodeReader, enabled: barcodeEnabled },
cardReader: { device: cardReader, enabled: cardEnabled },
printer: { device: printer, enabled: printerEnabled },
scale: { device: scale, enabled: scaleEnabled }
}), [barcodeReader, barcodeEnabled, cardReader, cardEnabled, printer, printerEnabled, scale, scaleEnabled]);
useEffect(() => {
console.log('Component Status:', componentStatus);
// Enable features based on available components
if (componentStatus.barcodeReader.enabled) {
// enableBarcodeScanning();
}
if (componentStatus.cardReader.enabled) {
// enablePaymentProcessing();
}
if (!componentStatus.printer.enabled && componentStatus.printer.device) {
// showPrinterOfflineWarning();
}
}, [componentStatus]);
// Auto-enable disabled components
useEffect(() => {
const componentsToEnable = Object.values(componentStatus).filter(
c => c.device && !c.enabled
);
componentsToEnable.forEach(async ({ device }) => {
if (device) {
try {
await device.enable();
console.log(`Enabled ${(device as any).deviceType}`);
} catch (err) {
console.error(`Failed to enable ${(device as any).deviceType}:`, err);
}
}
});
}, [componentStatus]);
return (
<div>
<h3>Component Status</h3>
<ul>
<li>Barcode Reader: {barcodeEnabled ? 'Enabled' : 'Disabled'}</li>
<li>Card Reader: {cardEnabled ? 'Enabled' : 'Disabled'}</li>
<li>Printer: {printerEnabled ? 'Enabled' : 'Disabled'}</li>
<li>Scale: {scaleEnabled ? 'Enabled' : 'Disabled'}</li>
</ul>
</div>
);
}
Complete Check-In Flow
Build a complete passenger check-in flow with barcode scanning and boarding pass printing.
import { useState, useEffect, useCallback } from 'react';
import {
useBarcodeReader,
useDocumentReader,
useBoardingPassPrinter
} from '@elevationai/cuss2-react';
type CheckInStep = 'welcome' | 'scan' | 'processing' | 'print' | 'complete';
function CheckInFlow() {
const { device: barcodeReader, data: barcodeData } = useBarcodeReader();
const { device: documentReader, data: documentData } = useDocumentReader();
const { device: printer } = useBoardingPassPrinter();
const [currentStep, setCurrentStep] = useState<CheckInStep>('welcome');
const [passengerData, setPassengerData] = useState<any>(null);
const startCheckInFlow = useCallback(() => {
setCurrentStep('scan');
console.log('Starting check-in flow');
// Enable scanning devices
barcodeReader?.enable().catch(console.error);
documentReader?.enable().catch(console.error);
}, [barcodeReader, documentReader]);
const resetFlow = useCallback(() => {
setCurrentStep('welcome');
setPassengerData(null);
setTimeout(() => startCheckInFlow(), 3000);
}, [startCheckInFlow]);
const processBoardingPass = useCallback(async (barcode: string) => {
setCurrentStep('processing');
console.log('Processing boarding pass...');
try {
// Parse boarding pass data (implementation depends on format)
const passengerInfo = parseBoardingPass(barcode);
// Validate with airline systems
const validatedData = await validatePassenger(passengerInfo);
setPassengerData(validatedData);
setCurrentStep('print');
await printBoardingPass(validatedData);
} catch (error) {
console.error('Check-in processing failed:', error);
resetFlow();
}
}, [resetFlow]);
const processPassport = useCallback(async (documentLines: string[]) => {
setCurrentStep('processing');
console.log('Processing passport...');
try {
const mrzLines = documentLines.filter(line => isMRZLine(line));
const passportData = parseMRZ(mrzLines);
// Create check-in record
const passengerInfo = await createCheckInFromPassport(passportData);
setPassengerData(passengerInfo);
setCurrentStep('print');
await printBoardingPass(passengerInfo);
} catch (error) {
console.error('Passport processing failed:', error);
resetFlow();
}
}, [resetFlow]);
const printBoardingPass = async (passengerInfo: any) => {
try {
// Setup printer with airline assets
await printer?.setupITPS([
'PT##$S6A#@;#TICK#CHEC#BOAR#0101110112011301210122012301C#0201A34#03BRB061661#0430G25F'
]);
// Print boarding pass with passenger data
await printer?.sendITPS([
'CP#A#01S#CP#C01#02@@01#03M1THIS IS A BARCODE#04THIS IS A BOARDING PASS#'
]);
setCurrentStep('complete');
console.log('Boarding pass printed successfully');
// Auto-reset after 10 seconds
setTimeout(() => resetFlow(), 10000);
} catch (error) {
console.error('Printing failed:', error);
resetFlow();
}
};
// Handle barcode data
useEffect(() => {
if (barcodeData && currentStep === 'scan') {
const barcodes = barcodeData.payload?.dataRecords?.map((dr: any) => dr.data) || [];
if (barcodes[0]) {
processBoardingPass(barcodes[0]);
}
}
}, [barcodeData, currentStep, processBoardingPass]);
// Handle document data
useEffect(() => {
if (documentData && currentStep === 'scan') {
const documentLines = documentData.payload?.dataRecords?.map((dr: any) => dr.data) || [];
if (documentLines.length > 0) {
processPassport(documentLines);
}
}
}, [documentData, currentStep, processPassport]);
// Start flow on mount
useEffect(() => {
startCheckInFlow();
}, [startCheckInFlow]);
return (
<div>
<h2>Check-In: {currentStep}</h2>
{passengerData && <div>Passenger: {passengerData.name}</div>}
</div>
);
}
// Helper functions
function parseBoardingPass(barcode: string) {
// Implementation depends on barcode format (IATA BCBP, etc.)
// Return passenger information
return { name: 'Passenger', flight: 'AA123', gate: 'A1', departureTime: '14:30' };
}
function parseMRZ(mrzLines: string[]) {
// Parse Machine Readable Zone from passport
// Return passport data
return {};
}
function isMRZLine(line: string): boolean {
// Check if line is part of MRZ
return line.length > 0;
}
async function validatePassenger(data: any) {
// Validate with airline systems
return data;
}
async function createCheckInFromPassport(passportData: any) {
// Create check-in record
return { name: 'Passenger', flight: 'AA123', gate: 'A1', departureTime: '14:30' };
}
Payment Processing
Implement secure payment card processing with the CardReader.
import { useState, useEffect, useCallback } from 'react';
import { useCardReader } from '@elevationai/cuss2-react';
type PaymentState = 'idle' | 'waiting' | 'processing' | 'success' | 'failed';
function PaymentProcessor() {
const { device: cardReader, data: cardData } = useCardReader();
const [paymentState, setPaymentState] = useState<PaymentState>('idle');
const [amount, setAmount] = useState<number>(0);
const paymentFailed = useCallback((message: string) => {
setPaymentState('failed');
console.error('Payment failed:', message);
// Reset after error message
setTimeout(() => {
setPaymentState('idle');
}, 3000);
}, []);
const paymentSuccess = useCallback((result: any) => {
setPaymentState('success');
console.log('Payment approved:', result.transactionId);
// Auto-reset after showing success
setTimeout(() => {
setPaymentState('idle');
}, 5000);
}, []);
const processPaymentCard = useCallback(async (data: any[], paymentAmount: number) => {
setPaymentState('processing');
console.log('Processing payment...');
try {
// Extract card information
const cardInfo = parseCardData(data.map(d => d.data));
if (!cardInfo.isValid) {
throw new Error('Invalid card data');
}
// Process payment through payment gateway
const paymentResult = await submitPayment(cardInfo, paymentAmount);
if (paymentResult.approved) {
paymentSuccess(paymentResult);
} else {
paymentFailed(paymentResult.message || 'Payment declined');
}
} catch (error) {
console.error('Payment processing error:', error);
paymentFailed('Payment processing failed. Please try again.');
} finally {
await cardReader?.disable();
}
}, [cardReader, paymentSuccess, paymentFailed]);
const processPayment = useCallback(async (paymentAmount: number) => {
setAmount(paymentAmount);
setPaymentState('waiting');
console.log(`Initiating payment for $${paymentAmount}`);
try {
// Enable card reader
await cardReader?.enable();
// Start payment read with 30 second timeout
await cardReader?.read(30000);
} catch (error) {
console.error('Payment initiation failed:', error);
paymentFailed('Unable to initialize payment. Please try again.');
}
}, [cardReader, paymentFailed]);
// Handle card data
useEffect(() => {
if (cardData && paymentState === 'waiting') {
if (cardData.length) {
processPaymentCard(cardData, amount);
}
}
}, [cardData, paymentState, amount, processPaymentCard]);
return (
<div>
<h2>Payment: {paymentState}</h2>
{paymentState === 'idle' && (
<button onClick={() => processPayment(100)}>Process $100 Payment</button>
)}
</div>
);
}
// Helper functions
function parseCardData(data: string[]) {
// parse and return card information
}
async function submitPaymen(cardInfo: any, amount: number) {
// Implement payment api call
}
Baggage Handling
Implement baggage weighing and tag printing with weight validation.
import { useState, useEffect, useCallback, useRef } from 'react';
import { useScale, useBagTagPrinter } from '@elevationai/cuss2-react';
type BaggageState = 'idle' | 'weighing' | 'overweight' | 'printing' | 'complete';
function BaggageHandler() {
const { device: scale, data: scaleData } = useScale();
const { device: printer } = useBagTagPrinter();
const [baggageState, setBaggageState] = useState<BaggageState>('idle');
const [currentWeight, setCurrentWeight] = useState<number>(0);
const [passenger, setPassenger] = useState<any>(null);
const maxWeight = 50; // kg
const timeoutRef = useRef<NodeJS.Timeout>();
const resetBaggageProcess = useCallback(() => {
setBaggageState('idle');
setCurrentWeight(0);
setPassenger(null);
}, []);
const saveBaggageInfo = async (tagNumber: string, weight: number) => {
// Save baggage information to backend
};
const printBagTag = useCallback(async (weight: number) => {
setBaggageState('printing');
console.log(`Printing bag tag for weight: ${weight}kg`);
try {
// Setup bag tag printer with assets
const companyLogo = 'LT0146940A020101000000001D01630064006400000000000000000000' +
'000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF...';
const assets = 'BTT0801~J 500262=#01C0M5493450304#02C0M5493450304#03B1MA020250541=06' +
'#04B1MK200464141=06#05L0 A258250000#\n' + companyLogo;
await device.setupITPS([assets]);
// Print bag tag with passenger and weight information
const coupon = 'BTP080101#01THIS IS A#02BAG TAG#03123#04456#0501#';
await printer?.sendITPS([coupon]);
setBaggageState('complete');
console.log('Bag tag printed successfully:', bagTagNumber);
// Save baggage information
await saveBaggageInfo(bagTagNumber, weight);
// Auto-reset after success
setTimeout(() => resetBaggageProcess(), 8000);
} catch (error) {
console.error('Bag tag printing failed:', error);
resetBaggageProcess();
}
}, [printer, passenger, generateBagTagNumber, resetBaggageProcess]);
const handleOverweight = useCallback((weight: number) => {
setBaggageState('overweight');
const excessWeight = weight - maxWeight;
console.log(`Bag is overweight: ${weight}kg (${excessWeight.toFixed(1)}kg over limit)`);
// Allow passenger to adjust and try again
setTimeout(() => {
console.log('Waiting for adjusted bag weight');
setBaggageState('weighing');
stableCountRef.current = 0;
lastWeightRef.current = 0;
}, 10000);
}, []);
const startBaggageProcess = useCallback(async (passengerData: any) => {
setPassenger(passengerData);
setBaggageState('weighing');
console.log('Starting baggage process for:', passengerData.name);
// Enable scale
await scale?.enable();
// Set timeout for stable weight
timeoutRef.current = setTimeout(() => {
if (baggageState === 'weighing') {
console.error('Unable to get stable weight reading');
resetBaggageProcess();
}
}, 30000);
}, [scale, baggageState, resetBaggageProcess]);
// Monitor scale data and process weight
useEffect(() => {
if (scaleData && baggageState === 'weighing') {
const weight = scaleData.data;
if (weight > 0) {
setCurrentWeight(weight);
if (weight > maxWeight) {
handleOverweight(weight);
} else {
await printBagTag(weight);
}
}
}
}, [scaleData, baggageState]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
return (
<div>
<h2>Baggage Handling: {baggageState}</h2>
<div className={`weight-display ${
currentWeight > maxWeight ? 'overweight' :
currentWeight > maxWeight * 0.9 ? 'warning' : 'normal'
}`}>
{currentWeight.toFixed(1)} kg
</div>
{baggageState === 'idle' && (
<button onClick={() => startBaggageProcess({ name: 'John Doe', airline: 'AA' })}>
Start Baggage Process
</button>
)}
</div>
);
}
Camera Capture
Implement passenger photo capture for documentation and verification purposes.
import { useState, useEffect, useCallback } from 'react';
import { useCamera } from '@elevationai/cuss2-react';
type CaptureState = 'idle' | 'preparing' | 'ready' | 'capturing' | 'complete';
function PhotoCapture() {
const { device: camera, ready, data: cameraData } = useCamera();
const [captureState, setCaptureState] = useState<CaptureState>('idle');
const [capturedPhoto, setCapturedPhoto] = useState<string | null>(null);
// Monitor camera ready status
useEffect(() => {
if (ready && captureState === 'preparing') {
setCaptureState('ready');
console.log('Camera is ready');
}
}, [ready, captureState]);
// Monitor captured photos
useEffect(() => {
if (cameraData && captureState === 'capturing') {
const photos = cameraData.payload?.dataRecords?.map((dr: any) => dr.data) || [];
if (photos.length > 0 && photos[0]) {
setCapturedPhoto(photos[0]);
setCaptureState('complete');
console.log('Photo captured successfully');
savePhoto(photos[0]);
}
}
}, [cameraData, captureState]);
const countdownToCapture = async () => {
const countdown = [3, 2, 1];
for (const count of countdown) {
console.log(`Countdown: ${count}`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log('Capturing photo...');
};
const savePhoto = async (photoData: any) => {
// save photo to backend
};
const startPhotoCapture = useCallback(async () => {
setCaptureState('preparing');
console.log('Preparing camera...');
try {
// Enable camera
await camera?.enable();
// Wait for camera to be ready
setCaptureState('ready');
console.log('Camera ready, starting countdown');
// Countdown before capture
await countdownToCapture();
// Capture photo with 10 second timeout
setCaptureState('capturing');
await camera?.read(10000);
} catch (error) {
console.error('Photo capture failed:', error);
setCaptureState('idle');
}
}, [camera]);
const retakePhoto = useCallback(() => {
setCaptureState('idle');
setCapturedPhoto(null);
startPhotoCapture();
}, [startPhotoCapture]);
return (
<div>
<h2>Photo Capture: {captureState}</h2>
{captureState === 'idle' && (
<button onClick={startPhotoCapture}>Start Photo Capture</button>
)}
{captureState === 'complete' && capturedPhoto && (
<div>
<p>Photo captured successfully!</p>
<button onClick={retakePhoto}>Retake Photo</button>
</div>
)}
</div>
);
}
Accessibility Support
Implement comprehensive accessibility features with headset and keypad navigation.
import { useState, useEffect, useCallback, useMemo } from 'react';
import {
useHeadset,
useKeypad,
useAnnouncement,
useBarcodeReader,
useCardReader,
useBoardingPassPrinter
} from '@elevationai/cuss2-react';
interface MenuItem {
id: string;
label: string;
action: () => void;
}
function AccessibleKiosk() {
const { device: headset, enabled: headsetEnabled } = useHeadset();
const { device: keypad, keypadData } = useKeypad();
const { device: announcement } = useAnnouncement();
const { enabled: barcodeEnabled } = useBarcodeReader();
const { enabled: cardEnabled } = useCardReader();
const { enabled: printerEnabled } = useBoardingPassPrinter();
const [accessibilityMode, setAccessibilityMode] = useState(false);
const [currentFocus, setCurrentFocus] = useState(0);
const startCheckIn = useCallback(() => {
announcement?.say(
'Starting check-in process. You can scan your boarding pass, ' +
'scan a QR code from your phone, or place your passport on the document reader.'
);
// Navigate to check-in component
}, [announcement]);
const showFlightInfo = useCallback(() => {
announcement?.say('Loading flight information');
// Navigate to flight info component
}, [announcement]);
const getHelp = useCallback(() => {
announcement?.say(
'For additional assistance, please approach the service desk ' +
'or press the call button to speak with an agent.'
);
}, [announcement]);
const changeLanguage = useCallback(() => {
announcement?.say('Language options: English, Spanish, French. Use arrow keys to select.');
}, [announcement]);
const menuItems: MenuItem[] = useMemo(() => [
{ id: 'check-in', label: 'Check In', action: startCheckIn },
{ id: 'flight-info', label: 'Flight Information', action: showFlightInfo },
{ id: 'help', label: 'Help and Assistance', action: getHelp },
{ id: 'language', label: 'Change Language', action: changeLanguage }
], [startCheckIn, showFlightInfo, getHelp, changeLanguage]);
const announceCurrentOption = useCallback(() => {
const currentItem = menuItems[currentFocus];
if (currentItem) {
const position = `Option ${currentFocus + 1} of ${menuItems.length}`;
announcement?.say(`${position}: ${currentItem.label}`);
}
}, [currentFocus, menuItems, announcement]);
const enableAccessibilityMode = useCallback(async () => {
console.log('Enabling accessibility mode');
try {
// Enable headset
await headset?.enable();
// Enable keypad for navigation
await keypad?.enable();
setAccessibilityMode(true);
// Provide welcome message
await announcement?.say(
'Accessibility mode enabled. Audio guidance is now available. ' +
'Use the up and down arrow keys to navigate options. ' +
'Press enter to select. Press the help key at any time for assistance.'
);
// Start with first menu item
setCurrentFocus(0);
announceCurrentOption();
} catch (error) {
console.error('Failed to enable accessibility mode:', error);
}
}, [headset, keypad, announcement, announceCurrentOption]);
const disableAccessibilityMode = useCallback(() => {
console.log('Disabling accessibility mode');
setAccessibilityMode(false);
// Switch to visual-only interface
}, []);
// Monitor headset connection
useEffect(() => {
if (headsetEnabled) {
enableAccessibilityMode();
} else {
disableAccessibilityMode();
}
}, [headsetEnabled, enableAccessibilityMode, disableAccessibilityMode]);
const navigateUp = useCallback(() => {
setCurrentFocus(prev => {
const newFocus = prev > 0 ? prev - 1 : menuItems.length - 1;
return newFocus;
});
}, [menuItems.length]);
const navigateDown = useCallback(() => {
setCurrentFocus(prev => {
const newFocus = prev < menuItems.length - 1 ? prev + 1 : 0;
return newFocus;
});
}, [menuItems.length]);
const selectCurrentOption = useCallback(async () => {
const selectedItem = menuItems[currentFocus];
if (selectedItem) {
await announcement?.say(`Selected: ${selectedItem.label}`);
selectedItem.action();
}
}, [currentFocus, menuItems, announcement]);
const provideHelp = useCallback(() => {
const helpText =
'You are using the accessible kiosk interface. ' +
'Current options: Check in, Flight information, Help, and Language settings. ' +
'Use up and down arrows to navigate between options. ' +
'Press enter to select an option. ' +
'Press home to return to the main menu. ' +
'Press volume up or down to adjust audio volume. ' +
'Press help at any time to hear this message again.';
announcement?.say(helpText);
}, [announcement]);
const goToMainMenu = useCallback(async () => {
setCurrentFocus(0);
await announcement?.say('Returning to main menu');
setTimeout(() => announceCurrentOption(), 500);
}, [announcement, announceCurrentOption]);
const adjustVolume = useCallback((direction: number) => {
const action = direction > 0 ? 'increased' : 'decreased';
announcement?.say(`Volume ${action}`);
}, [announcement]);
// Handle keypad input
useEffect(() => {
if (keypadData && accessibilityMode) {
if (keypadData.UP) {
navigateUp();
} else if (keypadData.DOWN) {
navigateDown();
} else if (keypadData.ENTER) {
selectCurrentOption();
} else if (keypadData.HOME) {
goToMainMenu();
} else if (keypadData.HELP) {
provideHelp();
} else if (keypadData.VOLUMEUP) {
adjustVolume(1);
} else if (keypadData.VOLUMEDOWN) {
adjustVolume(-1);
}
}
}, [keypadData, accessibilityMode, navigateUp, navigateDown, selectCurrentOption,
goToMainMenu, provideHelp, adjustVolume]);
// Announce current option when focus changes
useEffect(() => {
if (accessibilityMode) {
announceCurrentOption();
}
}, [currentFocus, accessibilityMode, announceCurrentOption]);
// Announce available services
useEffect(() => {
if (accessibilityMode) {
const availables = [];
if (barcodeEnabled) availables.push('barcode scanning');
if (cardEnabled) availables.push('payment processing');
if (printerEnabled) availables.push('boarding pass printing');
if (availables.length > 0) {
const servicesText = availables.join(', ');
announcement?.say(`Available services: ${servicesText}`);
}
}
}, [accessibilityMode, barcodeEnabled, cardEnabled, printerEnabled, announcement]);
return (
<div>
<h2>Accessible Kiosk</h2>
<p>Accessibility Mode: {accessibilityMode ? 'Enabled' : 'Disabled'}</p>
{accessibilityMode && (
<ul>
{menuItems.map((item, index) => (
<li key={item.id} className={index === currentFocus ? 'focused' : ''}>
{item.label}
</li>
))}
</ul>
)}
</div>
);
}
Error Handling Best Practices
Implement robust error handling for production CUSS2 applications.
import { useCallback } from 'react';
import { useCuss2 } from '@elevationai/cuss2-react';
function useErrorHandling() {
const { connected } = useCuss2();
// Centralized error handling with retry logic
const handleError = useCallback(async <T,>(
operation: () => Promise<T>,
serviceName: string,
maxRetries: number = 3,
retryDelay: number = 2000
): Promise<T> => {
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
console.warn(`${serviceName} failed (attempt ${attempt}/${maxRetries}):`, error);
if (attempt < maxRetries) {
// Exponential backoff
const delay = retryDelay * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// All retries failed
console.error(`${serviceName} failed after ${maxRetries} retries:`, lastError);
handleFinalError(lastError, serviceName);
throw lastError;
}, []);
const handleFinalError = useCallback((error: any, serviceName: string) => {
let userMessage = 'A technical error occurred. Please try again.';
// Provide specific error messages based on error type
if (error.code === 'TIMEOUT') {
userMessage = `${serviceName} timeout. Please ensure the device is ready and try again.`;
} else if (error.code === 'DEVICE_NOT_FOUND') {
userMessage = `${serviceName} is not available. Please see an agent for assistance.`;
} else if (error.code === 'CONNECTION_LOST') {
userMessage = 'Connection to the platform was lost. Reconnecting...';
attemptReconnection();
}
// Display error to user
console.error(userMessage);
}, []);
const attemptReconnection = useCallback(async () => {
if (!connected) {
// Wait 3 seconds before reconnecting
await new Promise(resolve => setTimeout(resolve, 3000));
try {
// In a real implementation, reconnection would be handled by the Cuss2Connector
console.log('Connection restored');
} catch (error) {
console.error('Unable to reconnect. Please see an agent.');
}
}
}, [connected]);
// Device-specific error handling
const handleBarcodeReaderError = useCallback((error: any) => {
if (error.message?.includes('timeout')) {
console.error('Barcode scanning timed out');
} else if (error.message?.includes('invalid')) {
console.error('Unable to read barcode');
} else {
console.error('Barcode scanner error');
}
}, []);
const handleCardReaderError = useCallback((error: any) => {
if (error.message?.includes('payment')) {
console.error('Payment card could not be processed');
} else if (error.message?.includes('track')) {
console.error('Card could not be read');
} else {
console.error('Card reader error');
}
}, []);
const handlePrinterError = useCallback((error: any) => {
if (error.message?.includes('paper')) {
console.error('Printer is out of paper');
} else if (error.message?.includes('jam')) {
console.error('Printer jam detected');
} else {
console.error('Printing failed');
}
}, []);
return {
handleError,
handleBarcodeReaderError,
handleCardReaderError,
handlePrinterError
};
}
// Example usage in a component
function ExampleComponent() {
const { handleError, handleBarcodeReaderError } = useErrorHandling();
const { device: barcodeReader } = useBarcodeReader();
const scanBarcode = async () => {
try {
await handleError(
() => barcodeReader!.read(10000),
'Barcode Reader',
3,
2000
);
} catch (error) {
handleBarcodeReaderError(error);
}
};
return <button onClick={scanBarcode}>Scan Barcode</button>;
}
Performance Optimization
Tips for optimizing performance in CUSS2 React applications.
1. Lazy Load Components
Use React.lazy and Suspense to reduce initial bundle size.
import { lazy, Suspense } from 'react';
// Lazy load device components
const BarcodeScanner = lazy(() => import('./components/BarcodeScanner'));
const PaymentProcessor = lazy(() => import('./components/PaymentProcessor'));
function KioskApp() {
return (
<Suspense fallback={<div>Loading...</div>}>
<BarcodeScanner />
<PaymentProcessor />
</Suspense>
);
}
2. Memoize Expensive Computations
Use useMemo to optimize expensive calculations.
import { useMemo } from 'react';
import { useBarcodeReader } from '@elevationai/cuss2-react';
function ScannerComponent() {
const { data: barcodeData } = useBarcodeReader();
const scanResult = useMemo(() => {
if (!barcodeData?.payload?.dataRecords) return null;
return barcodeData.payload.dataRecords.map(dr => dr.data)[0];
}, [barcodeData]);
return <div>{scanResult}</div>;
}
3. Cleanup Effects Properly
Always cleanup effects to prevent memory leaks.
import { useEffect } from 'react';
import { useBarcodeReader } from '@elevationai/cuss2-react';
function BaseKioskComponent() {
const { device: barcodeReader, data } = useBarcodeReader();
useEffect(() => {
// Subscribe to data
console.log('Barcode data:', data);
// Cleanup when component unmounts
return () => {
barcodeReader?.disable();
};
}, [barcodeReader, data]);
return <div>Scanner Component</div>;
}
4. Debounce Rapid Events
Use debouncing to prevent UI flooding from rapid device events.
import { useState, useEffect } from 'react';
import { useScale } from '@elevationai/cuss2-react';
function WeightDisplay() {
const { data: scaleData } = useScale();
const [displayWeight, setDisplayWeight] = useState(0);
useEffect(() => {
// Debounce weight updates
const timer = setTimeout(() => {
if (scaleData?.payload?.weight) {
const weight = scaleData.payload.weight.grossWeight.weight;
// Only update if weight changed significantly
if (Math.abs(weight - displayWeight) > 0.1) {
setDisplayWeight(weight);
}
}
}, 500);
return () => clearTimeout(timer);
}, [scaleData, displayWeight]);
return <div>{displayWeight.toFixed(1)} kg</div>;
}
Testing Strategies
Comprehensive testing approaches for CUSS2 React applications.
Unit Testing with React Testing Library
import { render, screen, waitFor } from '@testing-library/react';
import { useBarcodeReader } from '@elevationai/cuss2-react';
import BarcodeScanner from './BarcodeScanner';
// Mock the hook
jest.mock('@elevationai/cuss2-react');
describe('BarcodeScanner Component', () => {
const mockUseBarcodeReader = useBarcodeReader as jest.MockedFunction<typeof useBarcodeReader>;
beforeEach(() => {
mockUseBarcodeReader.mockReturnValue({
device: {
enable: jest.fn().mockResolvedValue(undefined),
disable: jest.fn().mockResolvedValue(undefined),
read: jest.fn().mockResolvedValue(undefined)
} as any,
ready: true,
enabled: true,
data: null,
status: 'OK',
pending: false
});
});
it('should render scanner component', () => {
render(<BarcodeScanner />);
expect(screen.getByText(/barcode/i)).toBeInTheDocument();
});
it('should display barcode data when scanned', async () => {
const mockData = {
meta: { messageCode: 'DATA_PRESENT' },
payload: {
dataRecords: [{ data: 'TEST123456789' }]
}
};
mockUseBarcodeReader.mockReturnValue({
device: {} as any,
ready: true,
enabled: true,
data: mockData,
status: 'OK',
pending: false
});
render(<BarcodeScanner />);
await waitFor(() => {
expect(screen.getByText(/TEST123456789/)).toBeInTheDocument();
});
});
});
Integration Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Cuss2Connector, Cuss2ConnectionOptions } from '@elevationai/cuss2-react';
import CheckInFlow from './CheckInFlow';
const testOptions: Cuss2ConnectionOptions = {
clientId: 'test-client',
clientSecret: 'test-secret',
cussUrl: 'ws://localhost:8080'
};
describe('CheckInFlow Integration', () => {
it('should complete check-in flow when barcode is scanned', async () => {
render(
<Cuss2Connector options={testOptions}>
<CheckInFlow />
</Cuss2Connector>
);
// Wait for component to load
await waitFor(() => {
expect(screen.getByText(/check-in/i)).toBeInTheDocument();
});
// Start check-in flow
const startButton = screen.getByRole('button', { name: /start/i });
fireEvent.click(startButton);
// Simulate barcode scan by updating the hook's data
// In a real test, you would mock the platform response
await waitFor(() => {
expect(screen.getByText(/processing/i)).toBeInTheDocument();
});
});
});
E2E Testing with Playwright
import { test, expect } from '@playwright/test';
test.describe('Kiosk E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000');
});
test('should complete passenger check-in', async ({ page }) => {
// Wait for connection
await expect(page.locator('[data-testid="connection-status"]')).toHaveText('Connected');
// Start check-in
await page.click('button:has-text("Start Check-In")');
// Simulate barcode scan via mock platform
await page.evaluate(() => {
// Trigger mock barcode scan event
window.postMessage({
type: 'MOCK_BARCODE_SCAN',
data: 'M1DOE/JOHN UA123 JFKLAX 001F003A0025'
}, '*');
});
// Verify UI updates
await expect(page.locator('[data-testid="check-in-status"]')).toHaveText('Processing');
// Wait for completion
await expect(page.locator('[data-testid="check-in-status"]')).toHaveText('Complete', {
timeout: 10000
});
await expect(page.locator('[data-testid="completion-message"]')).toContainText('Check-in complete');
});
test('should handle barcode scanner errors', async ({ page }) => {
await page.click('button:has-text("Start Check-In")');
// Simulate error
await page.evaluate(() => {
window.postMessage({
type: 'MOCK_BARCODE_ERROR',
error: 'Scanner timeout'
}, '*');
});
await expect(page.locator('[data-testid="error-message"]')).toContainText('timeout');
});
});