Skip to content

[SDK-717] Fix stale customAction replay when handler returns false - #1089

Open
joaodordio wants to merge 4 commits into
masterfrom
SDK-717-fix-stale-custom-action-replay
Open

[SDK-717] Fix stale customAction replay when handler returns false#1089
joaodordio wants to merge 4 commits into
masterfrom
SDK-717-fix-stale-custom-action-replay

Conversation

@joaodordio

@joaodordio joaodordio commented Sep 10, 2026

Copy link
Copy Markdown
Member

📝 Summary

Fix stale IterableCustomActionHandler replay when handler returns false, a regression present in 3.6.5–3.10.1.

🎟️ Jira Ticket: SDK-717

📖 Description

The IterableCustomActionHandler interface documents its boolean return value as "Reserved for future use". Clients commonly return false (Kotlin default, follows the javadoc), and the React Native SDK wrapper always does too. Since the SDK-307 fix (3.6.5, PR #975), returning false was treated as "action not consumed", leaving pendingAction alive and causing the handler to fire again on every foreground and every initialize() call.

Three replay paths:

  • onForeground() calls processPendingAction on every app foreground
  • initialize() called it twice — before and after loadLastSavedConfiguration()
  • The trampoline's super.onResume() triggers onActivityResumed → onForeground → processPendingAction on the stale action before handlePushAction sets the new one

IterableActionRunner.javacallCustomActionIfSpecified now invokes the handler and always returns true (consumed). Returns false only when the handler is null (SDK not yet initialized), keeping the SDK-307 retry intent intact.

IterableApi.java — removed the duplicate processPendingAction() call before loadLastSavedConfiguration() in initialize(). One call after full initialization is enough.

🧪 How to test?

Added testCustomActionHandlerReturnFalseDoesNotReplayOnForeground to IterablePushActionReceiverTest: sends a push with a handler that returns false, simulates a foreground event via processPendingAction, and asserts the handler fired exactly once.

Existing testBackgroundCustomActionProcessedAfterSDKInit continues to cover the SDK-307 scenario (handler null at push time, retried after initialize).

🧾 Changelog

Added a Fixed entry to CHANGELOG.md under [Unreleased] calling out affected versions 3.6.5–3.10.1.

📹 Loom recording if applicable

N/A

🐞 Github Issues solved

N/A

📚 Docs PR if applicable

N/A

The IterableCustomActionHandler interface documents its boolean return
value as 'Reserved for future use'. Clients routinely return false, as
does the React Native SDK wrapper. Since the SDK-307 fix (3.6.5),
returning false from the handler left pendingAction alive, causing the
handler to replay on every app foreground and every initialize() call.

Fix: treat invocation of the handler as consumed regardless of return
value. Only keep pendingAction alive when the handler is null (SDK not
yet initialized) — the original SDK-307 intent.

Also remove the duplicate processPendingAction() call early in
initialize(), before loadLastSavedConfiguration() runs. A single call
after full initialization is sufficient.

Affected versions: 3.6.5–3.10.1.
@joaodordio
joaodordio requested a review from a team as a code owner September 10, 2026 14:40
@joaodordio joaodordio self-assigned this Sep 10, 2026
The previous approach always returned true from callCustomActionIfSpecified
when a handler was present. That broke the openApp fallback in
handlePushAction: 'if (openApp && !handled)' never fired, so the launcher
activity was not started for push taps where the handler returned false.

Move the fix to processPendingAction instead. Before dispatching, capture
whether customActionHandler was present. After dispatching, clear
pendingAction if handled=true OR if the handler was present — invocation
itself is treated as consumed. The handler's return value is preserved
as-is so the openApp fallback in handlePushAction continues to work.
@franco-zalamena-iterable

franco-zalamena-iterable commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

This solution makes the openApp fallback work correctly, but customHandlerPresent does not necessarily mean the custom handler was invoked. From what i checked, an unhandled openUrl action is also cleared when an unrelated custom-action handler is configured, preventing its foreground retry.

I though about adding improving readability with something internal like this

enum ActionDispatchResult {
NOT_DISPATCHED,
DISPATCHED_WITH_FALLBACK,
HANDLED
}

This would make it easier to distinguish when to clear the pending action or when to run it.

I think having this boolean when it actually can represent more things can be a bit confusing to debug and to understand what is the intended behavior (like the bug itself)

Replace the customHandlerPresent boolean with a typed enum that cleanly
separates the two concerns a single boolean cannot express:
  - whether pendingAction should be cleared
  - whether the openApp launcher fallback should fire

NOT_DISPATCHED  — handler was null, keep pendingAction for retry (SDK-307)
DISPATCHED_WITH_FALLBACK — handler ran but returned false, or URL failed;
                           clear pendingAction, allow openApp fallback
HANDLED         — handler returned true or URL opened; clear pendingAction,
                  suppress openApp fallback

Also fixes Franco's callout: the previous customHandlerPresent check would
incorrectly clear a failed openUrl action whenever an unrelated custom
action handler was configured. dispatchPendingAction now checks handler
presence only for non-URL actions, so URL and custom action paths are
independently reasoned about.
@franco-zalamena-iterable

Copy link
Copy Markdown
Contributor

I was checking this PR with some tests and realized that we introduced a new problem, currently if the app is closed and a push url is passed, we silently fail it.

Currently on the initializeForPush we just pass the android context, not the iterable config.
Previously, we would return false in this case and then when the app was fully initialized it would be processed, but now it is returning DISPATCHED_WITH_FALLBACK
This is more specific to the openApp=false config.

We could rename NOT_DISPATCHED to RETRY_LATER and return it instead in this case, so a failed URL before full initialization should produce RETRY_LATER, not DISPATCHED_WITH_FALLBACK.

When only initializeForPush ran (_apiKey null), the SDK lacks the full
config (urlHandler, deep link handlers). A URL push action would fall
through dispatchPendingAction to DISPATCHED_WITH_FALLBACK, clearing
pendingAction and silently losing the action before initialize() ran.

Fix: URL actions now check _apiKey == null and return RETRY_LATER, so
they are deferred alongside custom actions when the SDK is not yet
fully initialized.

Also renames NOT_DISPATCHED -> RETRY_LATER throughout — clearer intent.

Adds testBackgroundUrlActionDeferredUntilSDKInit and a matching fixture
to cover the openApp=false + URL action + pre-init path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants