Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 6 additions & 31 deletions mobile/src/features/Links/components/ActionHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,7 @@ export const ActionHandler = () => {
// Update observables when React state changes
React.useEffect(() => {
if (pendingAction) {
const newActionId = createActionId(pendingAction)
// If this actionId was processed before, check if it's a new instance
if (newActionId && processedActionsRef.current.has(newActionId)) {
const previousAction = processedActionsRef.current.get(newActionId)
// If the action object reference is different, it's a new trigger
if (previousAction !== pendingAction) {
// Same actionId but new instance - clear to allow reprocessing
processedActionsRef.current.delete(newActionId)
isProcessingRef.current = false
}
}
currentActionIdRef.current = newActionId
currentActionIdRef.current = createActionId(pendingAction)
} else {
currentActionIdRef.current = null
}
Expand All @@ -168,10 +157,8 @@ export const ActionHandler = () => {
}, [wallet])

// Track processing state
// Map actionId -> pendingAction object reference to detect new triggers
const processedActionsRef = React.useRef<Map<string, PendingAction | null>>(
new Map(),
)
// Set of actionIds that have been processed
const processedActionsRef = React.useRef<Set<string>>(new Set())
const isProcessingRef = React.useRef(false)
const hasShownModalRef = React.useRef(false)
const prevWalletRef = React.useRef<typeof wallet>(wallet)
Expand Down Expand Up @@ -319,19 +306,8 @@ export const ActionHandler = () => {
return
}

// Check if we've already processed this exact action instance
if (actionId && processedActionsRef.current.has(actionId)) {
const processedAction = processedActionsRef.current.get(actionId)
// If it's the same action object reference, skip (already processing)
if (processedAction === action) {
return
}
// Different action object with same actionId - new trigger, allow it
processedActionsRef.current.delete(actionId)
isProcessingRef.current = false
}

if (!actionId) {
// Check if we've already processed this action
if (!actionId || processedActionsRef.current.has(actionId)) {
return
}

Expand All @@ -340,8 +316,7 @@ export const ActionHandler = () => {
}

isProcessingRef.current = true
// Store action object reference, not just actionId
processedActionsRef.current.set(actionId, action)
processedActionsRef.current.add(actionId)

InteractionManager.runAfterInteractions(() => {
try {
Expand Down
14 changes: 6 additions & 8 deletions mobile/src/features/Links/components/ClaimActionHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {logger} from '~/kernel/logger/logger'
export const ClaimActionHandler = () => {
const {pendingAction} = useLinks()
const {reset: resetClaimState, scanActionClaimChanged, address} = useClaim()
const processedActionRef = React.useRef<Links.CardanoActionClaim | null>(null)
// Track processed claim URLs to prevent re-processing
const processedClaimUrlsRef = React.useRef<Set<string>>(new Set())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing cleanup for processed claims prevents retry

Medium Severity

The processedClaimUrlsRef Set is never cleared, unlike ActionHandler which clears its processedActionsRef when pendingAction becomes null. Once a claim is added to this Set, it cannot be re-triggered during the component's lifecycle, even if the claim fails downstream or is cancelled. Users would need to navigate away (unmounting the component) to retry the same claim.

Fix in Cursor Fix in Web


// Handle claim action from pendingAction context
// Use useFocusEffect to ensure we process when screen is focused
Expand Down Expand Up @@ -48,12 +49,9 @@ export const ClaimActionHandler = () => {
) {
const cardanoAction = pendingAction.action as Links.CardanoActionClaim

// Check if we've already processed this action
if (
processedActionRef.current &&
processedActionRef.current.url === cardanoAction.url &&
processedActionRef.current.code === cardanoAction.code
) {
// Check if we've already processed this claim URL
const claimKey = `${cardanoAction.url}:${cardanoAction.code}`
if (processedClaimUrlsRef.current.has(claimKey)) {
logger.info(
'ClaimActionHandler: action already processed, skipping',
{
Expand All @@ -75,7 +73,7 @@ export const ClaimActionHandler = () => {
scanActionClaimChanged(cardanoAction)

// Mark as processed to prevent re-processing if screen refocuses
processedActionRef.current = cardanoAction
processedClaimUrlsRef.current.add(claimKey)
} else {
logger.info('ClaimActionHandler: conditions not met', {
hasPendingAction: !!pendingAction,
Expand Down
69 changes: 38 additions & 31 deletions mobile/src/features/Links/hooks/useDeepLinkWatcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const useDeepLinkWatcher = () => {
const {isLoggedIn} = useAuth()
const {pendingAction, setPendingAction} = useLinks()

// Track URLs that have already been processed to prevent re-processing
// when processLink callback reference changes
const processedUrlsRef = React.useRef<Set<string>>(new Set())

const processLink = React.useCallback(
(url: string) => {
// Try Yoroi links first (yoroi://)
Expand Down Expand Up @@ -70,7 +74,8 @@ export const useDeepLinkWatcher = () => {
React.useEffect(() => {
const getInitialURL = async () => {
const url = await Linking.getInitialURL()
if (url !== null) {
if (url !== null && !processedUrlsRef.current.has(url)) {
processedUrlsRef.current.add(url)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial URL marked processed before processing can fail

Medium Severity

In the "app is closed - check initial URL on mount" effect, the URL is added to processedUrlsRef on line 78 before processLink(url) is called on line 79. The processLink function can fail silently when parsing Cardano links (errors are caught and logged internally). If processing fails, the URL remains in the processed set and cannot be retried during the app session.

Fix in Cursor Fix in Web

processLink(url)
}
}
Expand All @@ -92,40 +97,42 @@ export const useDeepLinkWatcher = () => {
const checkInitialUrlAfterLogin = async () => {
const url = await Linking.getInitialURL()

if (url !== null) {
// Try both Yoroi and Cardano links
const parsedYoroiAction = linksYoroiParser(url)
if (parsedYoroiAction != null) {
if (
parsedYoroiAction.params?.isSandbox === true &&
__DEV__ === false
) {
return
}
const pendingAction: PendingAction = {
source: 'yoroi',
action: {info: parsedYoroiAction, isTrusted: false},
}
setPendingAction(pendingAction)
// Skip if URL was already processed
if (url === null || processedUrlsRef.current.has(url)) {
return
}

// Try both Yoroi and Cardano links
const parsedYoroiAction = linksYoroiParser(url)
if (parsedYoroiAction != null) {
if (parsedYoroiAction.params?.isSandbox === true && __DEV__ === false) {
return
}
processedUrlsRef.current.add(url)
const pendingAction: PendingAction = {
source: 'yoroi',
action: {info: parsedYoroiAction, isTrusted: false},
}
setPendingAction(pendingAction)
return
}

if (isWebCardanoLink(url)) {
try {
const cardanoAction = parseCardanoLink(url)
const pendingAction: PendingAction = {
source: 'cardano',
action: cardanoAction,
}
setPendingAction(pendingAction)
} catch (error) {
logger.error('useDeepLinkWatcher: error parsing URL after login', {
error,
errorMessage:
error instanceof Error ? error.message : String(error),
url,
})
if (isWebCardanoLink(url)) {
try {
processedUrlsRef.current.add(url)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL marked processed before parsing can fail

Medium Severity

In checkInitialUrlAfterLogin, the URL is added to processedUrlsRef before parseCardanoLink(url) is called. Since parseCardanoLink can throw errors (e.g., SchemeNotImplemented, ParamsValidationFailed, UnknownContent), if parsing fails the URL remains marked as processed and the user cannot retry. This contrasts with Yoroi link handling where the URL is only marked processed after successful parsing.

Fix in Cursor Fix in Web

const cardanoAction = parseCardanoLink(url)
const pendingAction: PendingAction = {
source: 'cardano',
action: cardanoAction,
}
setPendingAction(pendingAction)
} catch (error) {
logger.error('useDeepLinkWatcher: error parsing URL after login', {
error,
errorMessage:
error instanceof Error ? error.message : String(error),
url,
})
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions mobile/src/ui/Counter/Counter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const Counter = ({

{unitsText !== undefined && (
<Text style={[a.body_2_md_medium, {color: p.primary_600}]}>
{unitsText ?? ''}
{` ${unitsText}`}
</Text>
)}

Expand All @@ -56,7 +56,7 @@ export const Counter = ({
: [a.body_2_md_regular, {color: p.primary_600}],
]}
>
{closingText ?? ''}
{` ${closingText}`}
</Text>
)}
</Text>
Expand Down
Loading