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 supports multiple currency accounts through a swipeable pager interface. Users can switch between SAT, USD, EUR, and other currency units with smooth animations and haptic feedback.
The AccountPagerView component (components/blocks/AccountPagerView.tsx) orchestrates multi-account support:
Component Structure
interface AccountType {
unit : string ;
}
interface AccountPagerViewProps {
accounts : AccountType [];
setAccount : ( account : AccountType ) => void ;
account : AccountType ;
}
export function AccountPagerView ({
accounts ,
setAccount ,
account ,
} : AccountPagerViewProps ) : React . ReactElement {
// Implementation
}
From components/blocks/AccountPagerView.tsx:196-210.
Swipeable Interface
The pager uses react-native-web-infinite-swiper for smooth account switching:
const swiperRef = useRef < any >( null );
const pagerHeight = Math . max ( windowHeight * 0.3 , 250 );
< View className = "w-full" style = {{ height : pagerHeight }} >
< Swiper
containerStyle = {{ height : pagerHeight }}
controlsEnabled = { false }
loop
infinite
from = { 0 }
ref = { swiperRef }
minDistanceForAction = { 0.1 }
onIndexChanged = { onPageSelected }
controlsProps = {{ dotsTouchable : true , dotsPos : 'top' }} >
{ accounts . map (( acc , index ) => (
< VStack key = { ` ${ acc . unit } - ${ index } ` } align = "center" justify = "center" className = "flex-1" >
< Account accounts = { accounts } account = { acc } pagerHeight = { pagerHeight } />
</ VStack >
))}
</ Swiper >
</ View >
From components/blocks/AccountPagerView.tsx:323-341.
Page Selection Handler
Account switching triggers haptic feedback:
const onPageSelected = useCallback (
async ( index : number ) : Promise < void > => {
setAccount ( accounts [ index ]);
await EnhancedHaptics . successHaptic ();
},
[ accounts , setAccount ]
);
From components/blocks/AccountPagerView.tsx:229-235.
Programmatic Navigation
Accounts can be switched programmatically:
useEffect (() => {
const idx = accounts . findIndex (( a ) => a . unit === account . unit );
swiperRef . current ?. goTo ( idx );
}, [ accounts , account ]);
From components/blocks/AccountPagerView.tsx:237-240.
Payment Actions
The account pager integrates send/receive/scan actions:
Receive Action
const handleReceive = useCallback (() => {
router . navigate ({
pathname: '/(receive-flow)/receive' ,
params: { to: 'sendToken' , unit: account . unit },
});
}, [ account . unit ]);
Send Action
const handleSend = useCallback ( async () => {
let balance = 0 ;
try {
const balances = await getBalances ();
balance = balances [ selectedMintUrl || '' ] || 0 ;
} catch ( error ) {
if ( __DEV__ ) console . error ( 'Failed to get balance:' , error );
}
if ( balance <= 0 ) {
router . navigate ({
pathname: '/(send-flow)/mintSelect' ,
params: { to: 'sendToken' , unit: account . unit },
});
return ;
}
router . navigate ({
pathname: '/(send-flow)/currency' ,
params: { to: 'sendToken' , unit: account . unit },
});
}, [ getBalances , selectedMintUrl , account . unit ]);
From components/blocks/AccountPagerView.tsx:258-279.
QR Scanner Action
const handleScanQR = useCallback ( async () => {
const granted = await handlePermission ();
if ( ! granted ) return ;
router . navigate ({
pathname: '/camera' ,
params: { to: 'sendToken' , unit: account . unit },
});
}, [ handlePermission , account . unit ]);
From components/blocks/AccountPagerView.tsx:249-256.
The pager renders different button styles based on platform:
function LiquidCapsuleButton ({
label ,
systemIcon ,
color ,
onPress ,
} : {
label : string ;
systemIcon : React . ComponentProps < typeof SwiftUIImage >[ 'systemName' ];
color : string ;
onPress : () => void ;
}) {
return (
< Host style = {{ height : BUTTON_H , width : '100%' }} matchContents = { false } >
< SwiftUIButton
modifiers = { [
buttonStyle ( 'glass' ),
frame ({ height: BUTTON_H , maxWidth: Infinity , alignment: 'center' }),
]}
onPress={onPress}>
<SwiftUIHStack alignment="center" spacing={8}>
<SwiftUIImage systemName={systemIcon} size={18} color={color} />
<SwiftUIText
modifiers={[
font ({ size: 14 , weight: 'bold' }),
foregroundStyle (color),
]}>
{label}
</SwiftUIText>
</SwiftUIHStack>
</SwiftUIButton>
</Host>
);
}
From components/blocks/AccountPagerView.tsx:123-158.
function AndroidLiquidCapsuleButton ({
label ,
icon ,
color ,
onPress ,
} : {
label : string ;
icon : string ;
color : string ;
onPress : () => void ;
}) {
return (
< View className = "w-full" style = {{ height : BUTTON_H }} >
< LiquidButtonView
title = { INVISIBLE_TITLE_WIDE }
enabled
tint = "transparent"
blurRadius = { 3 }
onPress = { onPress }
style = {{ width : '100%' , height : BUTTON_H , borderRadius : BUTTON_H / 2 }}
/>
< View
pointerEvents = "none"
className = "absolute inset-0 flex-row items-center justify-center gap-2"
style = {{ elevation : 1 }} >
< Icon name = { icon } size = { 16 } color = { color } />
< Text size = { 14 } style = {{ color , fontFamily : 'OxygenBold' }} >
{ label }
</ Text >
</ View >
</ View >
);
}
From components/blocks/AccountPagerView.tsx:51-83.
return (
< Button
text = { label }
icon = {<Icon name = { rnIcon } size = { 16 } color = { foreground } /> }
onPress = { onPress }
variant = "secondary"
blur = {{ intensity : 70 , tint : 'dark' }}
haptics
style = {{ margin : 0 , marginBottom : 0 , width : '100%' , minHeight : BUTTON_H }}
/>
);
From components/blocks/AccountPagerView.tsx:309-319.
A floating QR scanner button sits between send/receive:
iOS Liquid Glass
function LiquidQRButton ({
tint ,
color ,
onPress ,
} : {
tint : string ;
color : string ;
onPress : () => void ;
}) {
return (
< Host style = {{ height : QR_SIZE , width : QR_SIZE }} matchContents = { false } >
< SwiftUIButton
modifiers = { [
buttonStyle ( 'glass' ),
frame ({ height: QR_SIZE , width: QR_SIZE }),
glassEffect ({
shape: 'circle' ,
glass: { tint, variant: 'regular' , interactive: true },
}),
]}
onPress={onPress}>
<SwiftUIHStack alignment="center">
<SwiftUIImage systemName="qrcode.viewfinder" size={22} color={color} />
</SwiftUIHStack>
</SwiftUIButton>
</Host>
);
}
From components/blocks/AccountPagerView.tsx:161-189.
function AndroidLiquidQRButton ({
tint ,
color ,
onPress ,
} : {
tint : string ;
color : string ;
onPress : () => void ;
}) {
return (
< View
className = "overflow-hidden"
style = {{
width : QR_SIZE ,
height : QR_SIZE ,
borderRadius : QR_SIZE / 2 ,
transform : [{ scale: 1.3 }],
}} >
< LiquidButtonView
title = { INVISIBLE_TITLE_SHORT }
enabled
tint = { tint }
blurRadius = { 4 }
lensX = { 24 }
lensY = { 24 }
onPress = { onPress }
style = {{ width : '100%' , height : '100%' }}
/>
< View
pointerEvents = "none"
className = "absolute inset-0 items-center justify-center"
style = {{ elevation : 1 }} >
< Icon name = "stash:qr-code" size = { 24 } color = { color } />
</ View >
</ View >
);
}
From components/blocks/AccountPagerView.tsx:85-121.
Buttons are arranged in a specific layout:
< View
className = "relative w-full justify-center px-3"
style = {{ marginTop : 8 , height : Math . max ( QR_SIZE , BUTTON_H ) }} >
{ /* Send and Receive buttons */ }
< View className = "flex-row gap-3" >
< View className = "flex-1" >
{ renderCapsuleButton ( 'Receive' , 'arrow.down.left' , 'lucide:arrow-down-left' , handleReceive )}
</ View >
< View className = "flex-1" >
{ renderCapsuleButton ( 'Send' , 'arrow.up.right' , 'lucide:arrow-up-right' , handleSend )}
</ View >
</ View >
{ /* Floating QR button */ }
< View pointerEvents = "box-none" className = "absolute inset-x-0 z-[1000] items-center" >
{ supportsLiquidGlass () ? (
< LiquidQRButton tint = { shadeColor300 } color = { foreground } onPress = { handleScanQR } />
) : useAndroidLiquidButtons ? (
< AndroidLiquidQRButton tint = { shadeColor300 } color = { foreground } onPress = { handleScanQR } />
) : (
{ /* Fallback gradient button */ }
)}
</ View >
</ View >
From components/blocks/AccountPagerView.tsx:343-392.
Account Display
Each account renders with:
Currency Icon
const CURRENCY_ICONS : Record < string , React . FC > = {
sat: BitcoinMaskIcon ,
usd: DollarMaskIcon ,
eur: EuroMaskIcon ,
gbp: PoundMaskIcon ,
};
const CurrencyIcon = CURRENCY_ICONS [ account . unit ];
From components/blocks/Account.tsx:25-37.
Balance Display
< VStack align = "center" gap = { 8 } >
< PrimaryBalance account = { account } />
< HStack spacing = { 2 } >
{ accounts . map (( acc , index ) => {
const isActive = acc . unit === account . unit ;
return (
< Text
key = { index }
weight = {isActive ? 'bold' : 'regular' }
size = { 16 }
style = {{
color : isActive ? foreground : surfaceTertiary ,
marginTop : 3 ,
}} >
•
</ Text >
);
})}
</ HStack >
</ VStack >
From components/blocks/Account.tsx:46-64.
Mint Management Integration
The pager integrates with mint management:
const { getBalances } = useMintManagement ();
const { keys } = useNostrKeysContext ();
const selectedMints = useMintStore (( state ) => state . selectedMints );
const selectedMintUrl = keys ?. pubkey ? selectedMints [ keys . pubkey ] : undefined ;
From components/blocks/AccountPagerView.tsx:221-225.
Camera Permission Handling
const { handlePermission } = useHandleCameraPermission ();
const handleScanQR = useCallback ( async () => {
const granted = await handlePermission ();
if ( ! granted ) return ;
router . navigate ({
pathname: '/camera' ,
params: { to: 'sendToken' , unit: account . unit },
});
}, [ handlePermission , account . unit ]);
Key Features
Swipeable Interface Smooth infinite swiper with loop support and haptic feedback
Platform Adaptation iOS liquid glass, Android liquid buttons, and fallback blur buttons
Visual Indicators Dot indicators show current account position
Smart Routing Context-aware navigation based on balance and mint availability