Documentation Index Fetch the complete documentation index at: https://mintlify.com/sovranBitcoin/sovran/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Sovran’s QR scanner provides a comprehensive interface for scanning payment strings, ecash tokens, Lightning invoices, and more. The scanner supports animated UR codes, clipboard paste, and gallery image import.
Camera Screen Component
The CameraScreen component (components/screens/CameraScreen.tsx) provides the core scanning interface:
export interface ScanningData {
data : string ;
/**
* Optional source hint for scans.
* Common values: 'paste', 'deeplink', 'qr'.
*/
type ?: string ;
}
interface CameraScreenProps {
onScan : ( data : ScanningData ) => Promise < void | { urInProgress : boolean ; progress ?: number }>;
onReset ?: () => void ;
scanLocked ?: boolean ;
}
From components/screens/CameraScreen.tsx:34-47.
Camera Permission Handling
The useHandleCameraPermission hook manages camera access:
export function useHandleCameraPermission () {
const [ permission , requestPermission ] = useCameraPermissions ();
const [ isChecking , setIsChecking ] = useState ( true );
const handlePermission = async () : Promise < boolean > => {
if ( ! permission ) return false ;
if ( permission . granted ) return true ;
if ( permission . canAskAgain ) {
const res = await requestPermission ();
if ( res . granted ) {
cameraPermissionPopup ( 'granted' );
return true ;
}
}
// For both denied and blocked, show error with Open Settings button
popup ({
message: permission . canAskAgain ? 'Camera Permission Denied' : 'Camera Permission Blocked' ,
text: permission . canAskAgain
? 'Camera access is denied. Please enable it in your device settings.'
: 'Camera access is blocked. Please enable it in your device settings.' ,
emoji: '🚨' ,
type: 'error' ,
buttons: [{ text: 'Open Settings' , onPress : () => Linking . openURL ( 'app-settings:' ) }],
});
return false ;
};
return { permission , isChecking , handlePermission };
}
From hooks/useHandleCameraPermission.ts:8-42.
Scanner Features
Focus Detection
Scanner only processes codes when screen is focused:
const [ isFocused , setIsFocused ] = useState < boolean >( true );
const appStateRef = useRef < string >( AppState . currentState );
useFocusEffect (
useCallback (() => {
setIsFocused ( true );
setProgress ( 0 );
setLoading ( false );
isProcessingRef . current = false ;
onReset ?.();
return () => {
setIsFocused ( false );
};
}, [ onReset ])
);
useEffect (() => {
const handleAppStateChange = ( nextAppState : string ) => {
appStateRef . current = nextAppState ;
};
const subscription = AppState . addEventListener ( 'change' , handleAppStateChange );
return () => subscription ?. remove ();
}, []);
From components/screens/CameraScreen.tsx:59-80.
Scan Handling
Smart scan processing with UR code support:
const isProcessingRef = useRef < boolean >( false );
const handleScan = useCallback (
async ( data : ScanningData ) => {
if ( scanLocked ) {
return ;
}
// For UR codes, allow processing even when isProcessingRef is true
// to accumulate multiple parts
const isUrCode = data . data . toLowerCase (). startsWith ( 'ur:' );
if ( appStateRef . current !== 'active' || ! isFocused ) {
return ;
}
// For non-UR codes, skip if already processing
if ( ! isUrCode && isProcessingRef . current ) {
return ;
}
isProcessingRef . current = true ;
setLoading ( true );
try {
const result = await onScan ( data );
// Only reset loading state if UR is not in progress
const urInProgress =
result && typeof result === 'object' && 'urInProgress' in result
? result . urInProgress
: false ;
// Update progress if available
if (
result &&
typeof result === 'object' &&
'progress' in result &&
typeof result . progress === 'number'
) {
setProgress ( result . progress );
}
if ( ! urInProgress ) {
setLoading ( false );
setProgress ( 0 );
isProcessingRef . current = false ;
}
} catch {
setLoading ( false );
setProgress ( 0 );
isProcessingRef . current = false ;
}
},
[ onScan , isFocused , scanLocked ]
);
From components/screens/CameraScreen.tsx:82-136.
Scanning Overlay
Visual feedback with corner indicators:
const scanBoxSize = screenWidth * 0.8 ;
< View
className = "absolute left-1/2 top-1/2"
style = {{
width : scanBoxSize ,
height : scanBoxSize ,
transform : [{ translateX: - scanBoxSize / 2 }, { translateY: - scanBoxSize / 2 }],
}} >
{ /* Top-left corner */ }
< View className = "absolute left-0 top-0 h-[30px] w-[30px] border-l-4 border-t-4 border-white shadow-sm shadow-black" />
{ /* Top-right corner */ }
< View
className = "absolute right-0 top-0 h-[30px] w-[30px] shadow-sm"
style = {{
borderTopWidth : 4 ,
borderRightWidth : 4 ,
borderColor : 'white' ,
}}
/>
{ /* Bottom-left corner */ }
< View
className = "absolute bottom-0 left-0 h-[30px] w-[30px] shadow-sm"
style = {{
borderBottomWidth : 4 ,
borderLeftWidth : 4 ,
borderColor : 'white' ,
}}
/>
{ /* Bottom-right corner */ }
< View
className = "absolute bottom-0 right-0 h-[30px] w-[30px] shadow-sm"
style = {{
borderBottomWidth : 4 ,
borderRightWidth : 4 ,
borderColor : 'white' ,
}}
/>
{ /* Progress text */ }
< View className = "absolute bottom-0 self-center rounded-lg bg-black/50 p-2" >
{ progress > 0 ? (
< Text className = "text-foreground" size = { 16 } >
Progress : {Math.round(progress * 100 ) } %
</ Text >
) : loading ? (
< Text className = "text-foreground" size = { 16 } >
Loading ...
</ Text >
) : (
< Text className = "text-foreground" size = { 16 } >
Scanning ...
</ Text >
)}
</ View >
</ View >
From components/screens/CameraScreen.tsx:209-279.
Additional Features
Torch/Flashlight Toggle
const [ flashlightOn , setFlashlightOn ] = useState < boolean | null >( null );
const toggleFlashlight = useCallback (() : void => {
setFlashlightOn (( prev ) => ! prev );
}, []);
const handleCameraReady = useCallback ( async () : Promise < void > => {
try {
setTimeout (() => {
setFlashlightOn ( false );
}, 1000 );
} catch {
// Silent error handling
}
}, []);
< CameraView
enableTorch = {flashlightOn ?? false }
onCameraReady = { handleCameraReady }
// ...
/>
From components/screens/CameraScreen.tsx:138-150, 200.
Gallery Import
Scan QR codes from gallery images:
const handleGalleryPress = useCallback ( async () : Promise < void > => {
try {
const result = await ImagePicker . launchImageLibraryAsync ({
mediaTypes: [ 'images' ],
allowsEditing: false ,
quality: 1 ,
});
if ( result . canceled || ! result . assets ?.[ 0 ]?. uri ) return ;
const scannedCodes = await scanFromURLAsync ( result . assets [ 0 ]. uri , [ 'qr' ]);
if ( scannedCodes . length === 0 ) {
noQrCodeFoundPopup ();
return ;
}
const scanning : ScanningData = { data: scannedCodes [ 0 ]. data , type: 'qr' };
await handleScan ( scanning );
} catch {
qrScanFailedPopup ();
}
}, [ handleScan ]);
From components/screens/CameraScreen.tsx:152-174.
Clipboard Paste
Paste payment strings from clipboard:
const handleClipboardPress = useCallback ( async () : Promise < void > => {
const text = await Clipboard . getStringAsync ();
if ( ! text ) return ;
const scanning : ScanningData = { data: text , type: 'paste' };
await handleScan ( scanning );
}, [ handleScan ]);
From components/screens/CameraScreen.tsx:176-182.
{ Platform . OS === 'ios' ? (
<>
{ /* Clipboard */ }
< Host style = {{ height : 52 , width : 52 }} matchContents = { false } >
< SwiftUIButton
modifiers = { [
buttonStyle ( 'glass' ),
frame ({ height: 52 , width: 52 }),
glassEffect ({
shape: 'circle' ,
glass: { variant: 'regular' , interactive: true },
}),
]}
onPress={handleClipboardPress}>
<SwiftUIHStack alignment="center">
<SwiftUIImage systemName="doc.on.clipboard" size={22} color="white" />
</SwiftUIHStack>
</SwiftUIButton>
</Host>
{ /* Gallery */ }
<Host style={{ height: 52 , width: 52 }} matchContents = { false } >
< SwiftUIButton
modifiers = { [
buttonStyle ( 'glass' ),
frame ({ height: 52 , width: 52 }),
glassEffect ({
shape: 'circle' ,
glass: { variant: 'regular' , interactive: true },
}),
]}
onPress={handleGalleryPress}>
<SwiftUIHStack alignment="center">
<SwiftUIImage systemName="photo" size={22} color="white" />
</SwiftUIHStack>
</SwiftUIButton>
</Host>
{ /* Flashlight */ }
<Host style={{ height: 52 , width: 52 }} matchContents = { false } >
< SwiftUIButton
modifiers = { [
buttonStyle ( 'glass' ),
frame ({ height: 52 , width: 52 }),
glassEffect ({
shape: 'circle' ,
glass: { variant: 'regular' , interactive: true },
}),
]}
onPress={toggleFlashlight}>
<SwiftUIHStack alignment="center">
<SwiftUIImage
systemName={flashlightOn ? 'flashlight.on.fill' : 'flashlight.off.fill' }
size = { 22 }
color = "white"
/>
</ SwiftUIHStack >
</ SwiftUIButton >
</ Host >
</>
)}
From components/screens/CameraScreen.tsx:287-357.
: (
<>
{ /* Android fallback — blur buttons */ }
< Button
onPress = { handleClipboardPress }
icon = {<Icon name = "lets-icons:copy" color = { foreground } /> }
blur
/>
< Button
onPress = { handleGalleryPress }
icon = {<Icon name = "proicons:photo" color = { foreground } /> }
blur
/>
< Button
onPress = { toggleFlashlight }
icon = {
! flashlightOn ? (
< Icon name = "mdi:lightbulb-on-outline" color = { foreground } />
) : (
< Icon name = "mdi:lightbulb-on" color = { foreground } />
)
}
blur
/>
</>
)
From components/screens/CameraScreen.tsx:359-383.
UR Code Support
Animated QR codes for large payloads:
import { URDecoder } from '@gandlaf21/bc-ur' ;
const [ urDecoder , setUrDecoder ] = useState < URDecoder >( new URDecoder ());
if ( scanning . data . startsWith ( 'ur:' )) {
// Don't process if UR is already complete
if ( urDecoder . isComplete () && urDecoder . isSuccess ()) {
return { urInProgress: false };
}
const prevPer = urDecoder . getProgress ();
urDecoder . receivePart ( scanning . data );
const nextPer = urDecoder . getProgress ();
onProgress ?.( nextPer );
// Haptic feedback based on progress
if ( prevPer !== nextPer ) {
if ( nextPer < 0.33 ) {
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light );
} else if ( nextPer < 0.66 ) {
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Medium );
} else if ( nextPer < 1 ) {
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Heavy );
} else {
Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success );
}
}
if ( urDecoder . isComplete () && urDecoder . isSuccess ()) {
const ur = urDecoder . resultUR ();
const decoded = ur . decodeCBOR ();
const tokenString = new TextDecoder (). decode ( decoded );
// Reset decoder for next scan
setUrDecoder ( new URDecoder ());
return { urInProgress: false };
}
return { urInProgress: true , progress: nextPer };
}
From hooks/coco/useProcessPaymentString.ts:153-199.
Integration with Payment Processing
The scanner delegates payment processing to useProcessPaymentString:
const { processPaymentString , reset } = useProcessPaymentString ({
unit: account . unit ,
selectedMint ,
isFocused ,
onProgress: setProgress ,
onLoading: setLoading ,
onScanned: setScanned ,
});
< CameraScreen
onScan = { processPaymentString }
onReset = { reset }
scanLocked = { scanned }
/>
Scan Sources
All scans are tracked with source attribution:
qr - Camera QR scan or gallery import
paste - Clipboard paste
deeplink - App deep link
nfc - NFC tap
addScan ( scanning . data , trimmedData , 'ecash' , source );
Key Features
Multi-Source Input Camera, clipboard, gallery, and NFC support
UR Code Support Animated QR codes for large tokens with progress tracking
Smart Processing Background state detection and duplicate scan prevention
Visual Feedback Corner indicators, progress bars, and haptic feedback