BTDeviceInstruction
A specialized PDF viewer component for displaying medical device instruction manuals with automatic device-specific PDF loading and error handling.
Overview
The BTDeviceInstruction component provides a seamless way to display device-specific instruction manuals directly within your healthcare application. It automatically maps device types to their corresponding PDF instruction files and handles loading, error states, and PDF rendering.
Props Interface
export type BTDeviceInstructionProps = Omit<PDFViewerProps, "sourceURI"> & {
deviceType: IntegratedDevices;
};
The component accepts all props from PDFViewer except sourceURI, which is automatically determined based on the deviceType.
Features
📋 Automatic PDF Mapping
- Device-Specific: Automatically loads the correct instruction PDF for each device type
- Pre-configured: Includes instruction PDFs for 15+ medical device types
- URL Management: Centralized PDF URL management with CDN hosting
🔍 Error Handling
- Not Found Handling: Graceful handling when instruction PDF is not available
- Load Error Recovery: Comprehensive error handling for PDF loading failures
- User Feedback: Clear error messages and recovery options
🎯 Integration Ready
- Type Safety: Full TypeScript support with device type validation
- Theme Compatible: Inherits styling from PDF viewer with theme integration
- Performance Optimized: Efficient PDF loading and caching
Basic Usage
Simple Instruction Display
import React from 'react';
import { BTDeviceInstruction } from '@modules/bt-device';
import { IntegratedDevices } from '@modules/bt-management';
const InstructionScreen = ({ deviceType }: { deviceType: IntegratedDevices }) => {
return (
<BTDeviceInstruction
deviceType={deviceType}
style={{ flex: 1 }}
/>
);
};
// Usage examples
<InstructionScreen deviceType={IntegratedDevices.BP2} />
<InstructionScreen deviceType={IntegratedDevices.SPO2} />
<InstructionScreen deviceType={IntegratedDevices.TAIDOC_GLUCOSE} />
With Error Handling
import React, { useState } from "react";
import { Alert, View, Text, StyleSheet } from "react-native";
import { Button } from "react-native-paper";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
const InstructionWithErrorHandling = ({
deviceType,
}: {
deviceType: IntegratedDevices;
}) => {
const [error, setError] = useState<string | null>(null);
const handleError = (error: Error) => {
console.error("PDF Load Error:", error);
setError(error.message);
Alert.alert(
"Instruction Not Available",
"Unable to load device instructions. Please check your internet connection or contact support.",
[
{ text: "Retry", onPress: () => setError(null) },
{ text: "OK", style: "cancel" },
],
);
};
if (error) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>
Failed to load instructions: {error}
</Text>
<Button mode="contained" onPress={() => setError(null)}>
Retry
</Button>
</View>
);
}
return (
<BTDeviceInstruction
deviceType={deviceType}
onError={handleError}
style={{ flex: 1 }}
/>
);
};
const styles = StyleSheet.create({
errorContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
errorText: {
textAlign: "center",
marginBottom: 16,
color: "#d32f2f",
},
});
Advanced Usage
Modal Instruction Viewer
import React from "react";
import {
Modal,
SafeAreaView,
View,
Text,
StyleSheet,
Alert,
} from "react-native";
import { Button } from "react-native-paper";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
interface InstructionModalProps {
visible: boolean;
deviceType: IntegratedDevices;
onClose: () => void;
}
const InstructionModal = ({
visible,
deviceType,
onClose,
}: InstructionModalProps) => {
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Device Instructions</Text>
<Button mode="outlined" onPress={onClose}>
Close
</Button>
</View>
<BTDeviceInstruction
deviceType={deviceType}
style={{ flex: 1 }}
onError={(error) => {
console.error("Instruction error:", error);
Alert.alert("Error", "Failed to load instructions");
}}
/>
</SafeAreaView>
</Modal>
);
};
const styles = StyleSheet.create({
modalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#e0e0e0",
},
modalTitle: {
fontSize: 18,
fontWeight: "bold",
},
});
Tabbed Instructions
import React, { useState } from "react";
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
const Tab = createMaterialTopTabNavigator();
const MultiDeviceInstructions = ({
deviceTypes,
}: {
deviceTypes: IntegratedDevices[];
}) => {
return (
<Tab.Navigator>
{deviceTypes.map((deviceType) => (
<Tab.Screen
key={deviceType}
name={deviceType}
options={{ title: getDeviceDisplayName(deviceType) }}
>
{() => (
<BTDeviceInstruction deviceType={deviceType} style={{ flex: 1 }} />
)}
</Tab.Screen>
))}
</Tab.Navigator>
);
};
// Helper function to get display names
const getDeviceDisplayName = (deviceType: IntegratedDevices): string => {
const displayNames = {
[IntegratedDevices.BP2]: "Blood Pressure",
[IntegratedDevices.SPO2]: "Pulse Oximeter",
[IntegratedDevices.TAIDOC_GLUCOSE]: "Glucose Meter",
// ... add more as needed
};
return displayNames[deviceType] || deviceType;
};
Instruction with Device Info
import React from "react";
import { ScrollView, View, Text, StyleSheet } from "react-native";
import { BTDeviceInstruction, BTDeviceItem } from "@modules/bt-device";
import { IntegratedDevices, DeviceStatus } from "@modules/bt-management";
interface DeviceInstructionScreenProps {
deviceType: IntegratedDevices;
deviceData?: {
title: string;
subtitle: string;
imageUri: string;
status: DeviceStatus;
};
}
const DeviceInstructionScreen = ({
deviceType,
deviceData,
}: DeviceInstructionScreenProps) => {
return (
<ScrollView style={{ flex: 1 }}>
{deviceData && (
<View style={styles.deviceInfo}>
<BTDeviceItem>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title>{deviceData.title}</BTDeviceItem.Title>
<BTDeviceItem.SubTitle>
{deviceData.subtitle}
</BTDeviceItem.SubTitle>
<BTDeviceItem.Status status={deviceData.status} />
</BTDeviceItem.Content>
<BTDeviceItem.Image uri={deviceData.imageUri} />
</BTDeviceItem.TopContainer>
</BTDeviceItem>
</View>
)}
<View style={styles.instructionSection}>
<Text style={styles.sectionTitle}>Operating Instructions</Text>
<BTDeviceInstruction
deviceType={deviceType}
style={{ height: 600 }} // Fixed height for ScrollView
/>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
deviceInfo: {
padding: 16,
backgroundColor: "#f5f5f5",
},
instructionSection: {
flex: 1,
padding: 16,
},
sectionTitle: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 16,
},
});
Supported Device Types
The component includes pre-configured instruction PDFs for the following device types:
Blood Pressure & ECG
- BP2 (
IntegratedDevices.BP2): Wellue BP2 Armfit+ blood pressure monitor - ALIVE_COR (
IntegratedDevices.ALIVE_COR): AliveCor KardiaMobile ECG device
Pulse Oximeters
- SPO2 (
IntegratedDevices.SPO2): Wellue Oxyfit pulse oximeter - PC60WF (
IntegratedDevices.PC60WF): OxySmart PC60WF pulse oximeter - COSINUSS (
IntegratedDevices.COSINUSS): Cosinuss Two multi-sensor
Thermometers
- AOJ20A (
IntegratedDevices.AOJ20A): AOJ-20A infrared thermometer - CLEVERTEMP (
IntegratedDevices.CLEVERTEMP): TAIDOC TD1107 CleverTemp - FORATEMP (
IntegratedDevices.FORATEMP): FORA IR20 thermometer - ANDESFITTEMP (
IntegratedDevices.ANDESFITTEMP): Andesfit B33A thermometer
Glucose Meters
- TAIDOC_GLUCOSE (
IntegratedDevices.TAIDOC_GLUCOSE): TAIDOC TD4216 glucose meter - GLUCOCHECK (
IntegratedDevices.GLUCOCHECK): GlucoCheck Gold glucose meter
Scales
- F4 (
IntegratedDevices.F4): Viatom F4 smart scale - FORA_SCALE (
IntegratedDevices.FORA_SCALE): FORA W550 scale - RPM_SCALE (
IntegratedDevices.RPM_SCALE): GBS-2012-B Transtek Tele-RPM
Urinalysis
- BC01 (
IntegratedDevices.BC01): BC401 urine analyzer
Devices Without Instructions
Some device types don't have instruction PDFs configured:
CMED,SP80B,S5,BGM
For these devices, the onError callback will be triggered with "Device instruction pdf not found" message.
PDF URL Configuration
Instruction PDFs are hosted on Google Cloud Storage with the following structure:
const pdfBaseUrl =
"https://storage.googleapis.com/public-assets-com-expo-app/instruction-pdf";
export const DEVICE_INSTRUCTION_PDFS: Record<IntegratedDevices, string> = {
[IntegratedDevices.BP2]: `${pdfBaseUrl}/instruction_armfit.pdf`,
[IntegratedDevices.F4]: `${pdfBaseUrl}/instruction_scales.pdf`,
[IntegratedDevices.SPO2]: `${pdfBaseUrl}/instruction_oxyfit.pdf`,
// ... more mappings
};
Adding New Device Instructions
To add instructions for a new device type:
- Upload the PDF to the CDN
- Add the mapping to
DEVICE_INSTRUCTION_PDFS - Ensure the device type exists in
IntegratedDevicesenum
// Example: Adding a new device
[IntegratedDevices.NEW_DEVICE]: `${pdfBaseUrl}/new_device_manual.pdf`,
Error Handling
Error Types
The component handles several error scenarios:
- PDF Not Found: When no PDF is configured for the device type
- Network Errors: When the PDF URL is unreachable
- PDF Load Errors: When the PDF file is corrupted or invalid
- Rendering Errors: When the PDF cannot be displayed
Error Handling Pattern
import { Alert } from "react-native";
const handleInstructionError = (error: Error) => {
// Log error for debugging
console.error("Instruction PDF Error:", {
message: error.message,
timestamp: new Date().toISOString(),
});
// Categorize error type and show appropriate messages
if (error.message.includes("not found")) {
// Handle missing PDF
Alert.alert(
"Instructions Not Available",
"No instruction manual is available for this device type.",
[{ text: "OK" }],
);
} else if (error.message.includes("network")) {
// Handle network issues
Alert.alert(
"Network Error",
"Unable to load instructions. Please check your internet connection.",
[
{
text: "Retry",
onPress: () => {
/* retry logic */
},
},
{ text: "Cancel", style: "cancel" },
],
);
} else {
// Handle generic errors
Alert.alert(
"Error Loading Instructions",
"An error occurred while loading the instruction manual.",
[{ text: "OK" }],
);
}
};
Graceful Degradation
import React, { useState } from "react";
import { View, Text, Linking, StyleSheet } from "react-native";
import { Button } from "react-native-paper";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
const InstructionWithFallback = ({
deviceType,
}: {
deviceType: IntegratedDevices;
}) => {
const [hasError, setHasError] = useState(false);
if (hasError) {
return (
<View style={styles.fallbackContainer}>
<Text style={styles.fallbackTitle}>Instructions Not Available</Text>
<Text style={styles.fallbackMessage}>
Please visit our website or contact support for device instructions.
</Text>
<Button
mode="contained"
onPress={() => Linking.openURL("mailto:support@company.com")}
>
Contact Support
</Button>
</View>
);
}
return (
<BTDeviceInstruction
deviceType={deviceType}
onError={() => setHasError(true)}
/>
);
};
const styles = StyleSheet.create({
fallbackContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
fallbackTitle: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 8,
},
fallbackMessage: {
textAlign: "center",
marginBottom: 16,
color: "#666",
},
});
Performance Considerations
PDF Caching
The underlying PDFViewer handles caching automatically, but you can optimize further:
import React, { useEffect } from "react";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
import { DEVICE_INSTRUCTION_PDFS } from "@modules/bt-device";
const PreloadedInstructions = ({
deviceTypes,
}: {
deviceTypes: IntegratedDevices[];
}) => {
useEffect(() => {
// Preload common device instructions
deviceTypes.forEach((deviceType) => {
const pdfUrl = DEVICE_INSTRUCTION_PDFS[deviceType];
if (pdfUrl) {
// Prefetch PDF (implementation depends on PDF library)
// Example: Image.prefetch(pdfUrl) or custom prefetch implementation
console.log(`Prefetching PDF: ${pdfUrl}`);
}
});
}, [deviceTypes]);
return <BTDeviceInstruction deviceType={deviceTypes[0]} />;
};
Memory Management
import React, { useState } from "react";
import { View } from "react-native";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
// Mock DeviceSelector component for this example
const DeviceSelector = ({
onSelect,
}: {
onSelect: (device: IntegratedDevices) => void;
}) => {
// Implementation would go here
return null;
};
const OptimizedInstructions = () => {
const [currentDevice, setCurrentDevice] = useState<IntegratedDevices | null>(
null,
);
// Only render instruction when needed
return (
<View style={{ flex: 1 }}>
<DeviceSelector onSelect={setCurrentDevice} />
{currentDevice && (
<BTDeviceInstruction deviceType={currentDevice} style={{ flex: 1 }} />
)}
</View>
);
};
Testing
Component Testing
import React from "react";
import { render, waitFor } from "@testing-library/react-native";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
// Mock the PDFViewer component
jest.mock("@modules/pdf", () => ({
PDFViewer: ({ sourceURI, onError, ...props }) => {
if (sourceURI) {
return <Text testID="pdf-viewer">PDF Loaded: {sourceURI}</Text>;
} else {
onError?.(new Error("No PDF URL provided"));
return <Text testID="pdf-error">PDF Error</Text>;
}
},
}));
describe("BTDeviceInstruction", () => {
it("renders PDF viewer with correct URL", async () => {
const { getByTestId } = render(
<BTDeviceInstruction deviceType={IntegratedDevices.BP2} />,
);
await waitFor(() => {
const pdfViewer = getByTestId("pdf-viewer");
expect(pdfViewer).toBeTruthy();
expect(pdfViewer.props.children).toContain("instruction_armfit.pdf");
});
});
it("calls onError for unsupported device types", async () => {
const mockOnError = jest.fn();
render(
<BTDeviceInstruction
deviceType={IntegratedDevices.BGM} // No PDF configured
onError={mockOnError}
/>,
);
await waitFor(() => {
expect(mockOnError).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("Device instruction pdf not found"),
}),
);
});
});
it("passes through PDF viewer props correctly", () => {
const mockProps = {
style: { backgroundColor: "red" },
testID: "custom-pdf",
};
const { getByTestId } = render(
<BTDeviceInstruction
deviceType={IntegratedDevices.SPO2}
{...mockProps}
/>,
);
const component = getByTestId("custom-pdf");
expect(component.props.style).toContain(mockProps.style);
});
});
Integration Testing
import React, { useState } from "react";
import { render, fireEvent, waitFor } from "@testing-library/react-native";
import { View, Button } from "react-native";
describe("BTDeviceInstruction Integration", () => {
it("integrates with device selection flow", async () => {
const TestApp = () => {
const [selectedDevice, setSelectedDevice] = useState(null);
return (
<View>
<Button
testID="select-device"
title="Select BP Monitor"
onPress={() => setSelectedDevice(IntegratedDevices.BP2)}
/>
{selectedDevice && (
<BTDeviceInstruction deviceType={selectedDevice} />
)}
</View>
);
};
const { getByTestId, queryByTestId } = render(<TestApp />);
// Initially no instruction shown
expect(queryByTestId("pdf-viewer")).toBeNull();
// Select device
fireEvent.press(getByTestId("select-device"));
// Instruction should load
await waitFor(() => {
expect(getByTestId("pdf-viewer")).toBeTruthy();
});
});
});
Accessibility
Screen Reader Support
import React from "react";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
// Helper function to get device display name
const getDeviceName = (deviceType: IntegratedDevices): string => {
const deviceNames = {
[IntegratedDevices.BP2]: "Blood Pressure Monitor",
[IntegratedDevices.SPO2]: "Pulse Oximeter",
[IntegratedDevices.TAIDOC_GLUCOSE]: "Glucose Meter",
[IntegratedDevices.F4]: "Smart Scale",
[IntegratedDevices.ALIVE_COR]: "ECG Device",
// Add more as needed
};
return deviceNames[deviceType] || "Medical Device";
};
const InstructionScreen = ({
deviceType,
}: {
deviceType: IntegratedDevices;
}) => {
return (
<BTDeviceInstruction
deviceType={deviceType}
accessible={true}
accessibilityLabel={`Instruction manual for ${getDeviceName(deviceType)}`}
accessibilityHint="Scroll to read device operating instructions"
/>
);
};
Focus Management
import React, { useRef, useEffect } from "react";
import { BTDeviceInstruction } from "@modules/bt-device";
import { IntegratedDevices } from "@modules/bt-management";
const AccessibleInstructions = ({
deviceType,
}: {
deviceType: IntegratedDevices;
}) => {
const instructionRef = useRef(null);
useEffect(() => {
// Focus instruction when loaded
if (instructionRef.current) {
instructionRef.current.focus();
}
}, [deviceType]);
return (
<BTDeviceInstruction
ref={instructionRef}
deviceType={deviceType}
accessible={true}
accessibilityRole="document"
/>
);
};
Best Practices
Component Usage
- Error Handling: Always implement
onErrorcallback for production apps - Loading States: Show loading indicators while PDF loads
- Fallback Content: Provide alternative content when PDFs are unavailable
- Type Safety: Use TypeScript to ensure valid device types
Performance
- Lazy Loading: Only load instructions when needed
- Caching: Leverage built-in PDF caching mechanisms
- Memory: Unmount components when not in use
- Network: Consider offline instruction storage for critical devices
User Experience
- Navigation: Provide clear navigation to and from instructions
- Search: Implement search functionality for long instruction documents
- Bookmarks: Allow users to bookmark frequently accessed sections
- Feedback: Collect user feedback on instruction clarity and usefulness
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
deviceType | IntegratedDevices | Required | Device type to load instructions for |
onError | (error: Error) => void | undefined | Error handler for PDF loading failures |
All other props from PDFViewerProps are supported except sourceURI.
Error Messages
| Error | Description | Cause |
|---|---|---|
| "Device instruction pdf not found for 'deviceType'" | No PDF configured for device type | Device not in DEVICE_INSTRUCTION_PDFS mapping |
| PDF loading errors | Network or file issues | URL unreachable, file corrupted, etc. |
Dependencies
@modules/pdf: PDF viewer functionality@modules/bt-management: Device type definitions (IntegratedDevices)react: Core React functionality
Related Documentation
PDFViewer: Underlying PDF viewing componentBTDeviceList: Device list with instruction accessBTDeviceItem: Individual device cardsBT Management: Device type definitions and management