Skip to content

Conversation

@sumeruchat
Copy link
Collaborator

@sumeruchat sumeruchat commented Jan 16, 2026

What

Fixes custom actions not working when SDK is not initialized and openApp=false. This enables "snooze" type push notification flows where a button action can trigger a journey without opening the app.

Jira ticket: SDK-307

Changes

  • Store application context early in handlePushAction() via initializeForPush() so the SDK can process actions even before initialize() is called
  • Only clear pendingAction if the action was actually handled, allowing deferred processing when SDK is initialized later with a customActionHandler
  • Process pending actions when app comes to foreground (if SDK is initialized), handling cases where the handler returned false or wasn't available when the action arrived

Impact

  • Breaking changes: None
  • Dependencies: None
  • Performance: No impact

Testing

How to test:

  1. Set up a push notification with an action button that has openApp=false and a custom action type
  2. Receive the push while the app is backgrounded/not initialized
  3. Tap the action button
  4. Verify the custom action handler is called (either immediately if context is available, or when SDK initializes, or when app comes to foreground)

Unit tests added:

  • testBackgroundCustomActionWithNonInitializedSDK - verifies context is stored even without SDK init
  • testBackgroundCustomActionProcessedAfterSDKInit - verifies pending actions are processed after initialization

When a push action with openApp=false is received before SDK initialization,
the custom action handler was never invoked because getMainActivityContext()
returned null.

This fix:
1. Stores the application context early in handlePushAction() so
   getMainActivityContext() returns non-null even before initialize() is called
2. Only clears pendingAction if the action was actually handled, allowing
   deferred processing when SDK is initialized later with a customActionHandler

This enables "snooze" type flows where a push button action with openApp=false
can trigger a journey without requiring the full app to open.
// This allows the action to be processed later when SDK is fully initialized
// (e.g., when customActionHandler becomes available after initialize() is called).
if (handled) {
pendingAction = null;
Copy link

@franco-zalamena-iterable franco-zalamena-iterable Jan 20, 2026

Choose a reason for hiding this comment

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

What happens if it does not manage to execute the action? Can't this pendingAction become residual?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! I've updated the code to clear any previous unhandled pending action when a new push action comes in. This prevents residual actions from accumulating if they were never handled. The logic now clears pendingAction at the start of handlePushAction() before setting up the new action.

Choose a reason for hiding this comment

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

as stated in the other comment, that should be defined by code here, if we should have a pendingAction after not being able to handle it that is fine, but i feel like if this was not handled here it won't be in the next time if nothing changes, so we can just be calling processPendingAction indefinitely with nothing changing

// Store the application context if not already set, so custom actions can be
// processed even when the SDK hasn't been fully initialized (e.g., openApp=false)
if (IterableApi.sharedInstance._applicationContext == null && context != null) {
IterableApi.sharedInstance._applicationContext = context.getApplicationContext();

Choose a reason for hiding this comment

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

Can we create a method for initializing it so we don't call _applicationContext directly, if something else is necessary for pushes to work we can add it there

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done! I've created a new initializeForPush(Context context) method in IterableApi that encapsulates the context initialization. This method:

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  • Only sets the context if it hasn't been set already
  • Includes proper null checking and logging
  • Makes it easy to add additional push-related initialization in the future

@sumeruchat
Copy link
Collaborator Author

sumeruchat commented Jan 21, 2026

Thanks for the review Franco! I've addressed both of your comments:

I've also added new unit tests:

  • testInitializeForPushSetsContext
  • testInitializeForPushDoesNotOverwriteExistingContext
  • testPreviousPendingActionClearedOnNewPush

// (e.g., if the SDK was never initialized or the action handler wasn't available).
if (pendingAction != null) {
IterableLogger.d(TAG, "Clearing previous unhandled pending action");
pendingAction = null;

Choose a reason for hiding this comment

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

Is this necessary? On line 97 we are already setting the pending action to something else. Is there a place this can be used accidentally that i am missing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not really necessary not it was just responding to your previous comment and being extra safe for avoiding future bugs

Choose a reason for hiding this comment

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

yeah, but i feel like this is just adding code that virtually does nothing. My comment was more regarding the pending action that would be as left over on that method, if we are just not processing it and that is the correct behavior that's fine, but if we should do something to the pending action if it was not handled that should be clear

@sumeruchat
Copy link
Collaborator Author

@Ayyanchira Based on your feedback here is the test plan

  1. Native Android - Double Initialization Check
  • Initialize the SDK normally, then trigger a background push action
  • Verify: initializeForPush() does NOT overwrite the existing context (null
    check)
  • Verify: Logs show "initializeForPush: Application context set..." only on
    first call, not on subsequent calls
  • Verify: No crashes or unexpected behavior from double init paths
  1. Background Custom Action Execution (openApp=false)
  • Send a push notification with a custom action button that has openApp:
    false (e.g., "Remind Me" / "Mark as Read")
  • Tap the action button from the notification tray
  • Verify: The custom action handler fires
  • Verify: The app does NOT open / come to foreground
  • Verify: The action is processed silently in the background
  1. Pending Action Retry After SDK Init
  • Kill the app, send a push with a background custom action
  • Tap the action button (SDK is not initialized yet)
  • Then open the app (SDK initializes)
  • Verify: The pending action is processed once the SDK initializes with the
    customActionHandler
  1. Standard Push Behavior (Regression)
  • Send a normal push notification (default action, openApp=true)
  • Tap the notification
  • Verify: App opens normally and deep link / default action works as before
  1. Edge Cases
  • initializeForPush(null) - should log warning and not crash
  • Multiple rapid push actions before SDK init - only the latest pending
    action should be retained
  • App in foreground receiving background custom action push

@sumeruchat
Copy link
Collaborator Author

Test Results: SDK-307 Background Custom Actions

Automated Unit Tests — All 10 Passed ✅

Test Result What it validates
testInitializeForPushSetsContext Context is set when SDK is not initialized
testInitializeForPushDoesNotOverwriteExistingContext Existing context is NOT overwritten (no double initialization)
testBackgroundCustomActionWithNonInitializedSDK Background push works without full SDK init; app does NOT open
testBackgroundCustomActionProcessedAfterSDKInit Pending action is processed once SDK fully initializes with customActionHandler
testPreviousPendingActionClearedOnNewPush Only the latest pending action is retained when multiple pushes arrive before SDK init
testTrackPushOpenWithCustomAction Push open tracking works with custom actions
testPushOpenWithNonInitializedSDK Push open handling when SDK is not initialized
testLegacyDeepLinkPayload Legacy deep link payloads still work (regression)
testPushActionWithTextInput Text input push actions still work (regression)
testPushActionWithSilentAction Silent push actions still work (regression)

Manual E2E Testing — Pending

Manual testing on a device with real FCM push notifications is still needed for the following scenarios:

  1. Background custom action (openApp=false): Tap a custom action button on a push notification and verify the app does NOT open
  2. Double initialization check: With the app already running, trigger a background push action and confirm logs do NOT show initializeForPush: Application context set... (context should not be overwritten)
  3. Pending action retry (app killed): Force-kill the app, tap a background custom action button, then reopen the app — verify the action is processed during SDK initialization
  4. Normal push regression: Send a regular push and verify the app opens and deep links work as before
  5. React Native deep linking: Verify RN deep linking flow is unaffected by the changes

Note: adb shell am broadcast cannot reach the IterablePushActionReceiver (declared exported=false) on API 36, so manual E2E testing requires real FCM push delivery.

sumchattering and others added 2 commits January 29, 2026 16:32
The test had a race condition: initializeInBackground() runs actual
init on a background thread, which could complete before all 5 API
calls were made, causing some operations to execute immediately
instead of being queued. Added deterministic test helpers
(simulateInitializingState/simulateInitializationComplete) to
IterableBackgroundInitializer and replaced Thread.sleep with a
polling loop for queue processing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When the app switches to foreground and SDK is initialized, check for any pending custom actions that couldn't be handled previously (e.g., handler returned false or wasn't available when action arrived).
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.

5 participants