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)) {
Copy link

Choose a reason for hiding this comment

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

Action re-trigger detection logic removed breaks deep link retries

Medium Severity

The change from Map<string, PendingAction> to Set<string> removes logic that detected when the same actionId was triggered with a different object instance. Previously, the code compared object references to allow reprocessing when a "new trigger" occurred (same link clicked again). Now, once an actionId is in the Set, any subsequent trigger with the same actionId is silently ignored until processing completes. This could cause user-facing issues where clicking the same deep link multiple times appears to do nothing while an action is still processing.

Additional Locations (1)

Fix in Cursor Fix in Web

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())

// 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)
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)
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