Skip to main content

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

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:

  1. Upload the PDF to the CDN
  2. Add the mapping to DEVICE_INSTRUCTION_PDFS
  3. Ensure the device type exists in IntegratedDevices enum
// Example: Adding a new device
[IntegratedDevices.NEW_DEVICE]: `${pdfBaseUrl}/new_device_manual.pdf`,

Error Handling

Error Types

The component handles several error scenarios:

  1. PDF Not Found: When no PDF is configured for the device type
  2. Network Errors: When the PDF URL is unreachable
  3. PDF Load Errors: When the PDF file is corrupted or invalid
  4. 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

  1. Error Handling: Always implement onError callback for production apps
  2. Loading States: Show loading indicators while PDF loads
  3. Fallback Content: Provide alternative content when PDFs are unavailable
  4. Type Safety: Use TypeScript to ensure valid device types

Performance

  1. Lazy Loading: Only load instructions when needed
  2. Caching: Leverage built-in PDF caching mechanisms
  3. Memory: Unmount components when not in use
  4. Network: Consider offline instruction storage for critical devices

User Experience

  1. Navigation: Provide clear navigation to and from instructions
  2. Search: Implement search functionality for long instruction documents
  3. Bookmarks: Allow users to bookmark frequently accessed sections
  4. Feedback: Collect user feedback on instruction clarity and usefulness

API Reference

Props

PropTypeDefaultDescription
deviceTypeIntegratedDevicesRequiredDevice type to load instructions for
onError(error: Error) => voidundefinedError handler for PDF loading failures

All other props from PDFViewerProps are supported except sourceURI.

Error Messages

ErrorDescriptionCause
"Device instruction pdf not found for 'deviceType'"No PDF configured for device typeDevice not in DEVICE_INSTRUCTION_PDFS mapping
PDF loading errorsNetwork or file issuesURL unreachable, file corrupted, etc.

Dependencies

  • @modules/pdf: PDF viewer functionality
  • @modules/bt-management: Device type definitions (IntegratedDevices)
  • react: Core React functionality