Skip to content

RG-T132 Push linking fix - #275

Merged
ucswift merged 2 commits into
masterfrom
develop
Aug 19, 2026
Merged

RG-T132 Push linking fix#275
ucswift merged 2 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 19, 2026

Copy link
Copy Markdown
Member

Summary

Fixes push notification deep linking so tapped notifications route users to the correct chat or call screen more reliably, including on cold app starts and on iOS payload formats where eventCode is not exposed in the standard notification data.

What changed

  • Added support for extracting eventCode from multiple push payload locations, not just content.data.
    • This covers iOS/APNs-style payloads where custom keys may appear on the raw trigger payload instead of the standard Expo notification data.
  • Improved notification tap handling to deep-link directly when the notification represents:
    • a chat conversation (t: / g: prefixes, now handled case-insensitively), or
    • a call (C: and equivalent parsed call event codes)
  • Added fallback behavior so notifications that do not map to a supported route still open the existing notification modal.
  • Added validation to prevent navigation when route parameters are unsafe or malformed.
  • Updated notification parsing to support both:
    • colon-delimited event codes like C:1234, t:channel-id, g:group-id
    • legacy non-colon formats like C1234
  • Preserved support for IDs containing additional colons by splitting only on the first separator.

Functional impact

  • Chat push taps now navigate correctly for both lowercase and uppercase chat prefixes.
  • Call push taps now open the call detail screen directly instead of falling back to the modal.
  • iOS notification taps are more likely to route correctly because the app now reads eventCode from raw push payload fields when needed.
  • Cold-start notification taps are more resilient due to retry behavior while navigation/session state becomes ready.

Testing

  • Added and expanded automated tests covering:
    • chat deep-link routing and invalid payload rejection
    • retry behavior when navigation is not ready
    • extraction of eventCode from Android and iOS payload shapes
    • direct deep-linking for tapped call and chat notifications
    • modal fallback for non-routable notifications
    • updated parsing for colon-form and legacy event codes

@Resgrid-Bot

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 6 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 57 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd9e7b5c-1fb5-4e20-934b-2d673b573cb6

📥 Commits

Reviewing files that changed from the base of the PR and between 3dfa48c and 36085fa.

📒 Files selected for processing (7)
  • src/components/push-notification/__tests__/push-notification-modal.test.tsx
  • src/components/push-notification/push-notification-modal.tsx
  • src/services/__tests__/push-notification-chat-deeplink.test.ts
  • src/services/__tests__/push-notification.test.ts
  • src/services/push-notification.ts
  • src/stores/push-notification/__tests__/store.test.ts
  • src/stores/push-notification/store.ts

Comment @coderabbitai help to get the list of available commands.

freshRouterPushWithRetry.mockResolvedValue(undefined);

jest.useFakeTimers();
await pushNotificationService.initialize();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled rejection risk in src/services/tests/push-notification.test.ts at lines 631, 638, and 645: await pushNotificationService.initialize() adds an awaited async operation without local error handling. Guard the call with try/catch so initialization failures are handled explicitly instead of surfacing as unstructured unhandled rejections.

Kody rule violation: Handle async operations with proper error handling

try {
  await pushNotificationService.initialize();
} catch (err) {
  throw err;
}
Prompt for LLM

File src/services/__tests__/push-notification.test.ts:

Line 567:

Unhandled rejection risk in src/services/__tests__/push-notification.test.ts at lines 631, 638, and 645: await pushNotificationService.initialize() adds an awaited async operation without local error handling. Guard the call with try/catch so initialization failures are handled explicitly instead of surfacing as unstructured unhandled rejections.

Suggested Code:

      try {
        await pushNotificationService.initialize();
      } catch (err) {
        throw err;
      }

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/services/push-notification.ts Outdated
Comment on lines 363 to 366
if (this.tryDeepLinkForData(data)) {
return;
}
this.showModalForData(data, content.title, content.body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug high

Deep-link success is reported in src/services/push-notification.ts before the async navigation completes, so failed routerPushWithRetry retries or a waitUntil condition that never becomes true suppress the modal fallback and drop tapped call/chat notifications after only logging an error. Make tryDeepLinkForData async and suppress showModalForData only after routerPushWithRetry resolves, or call showModalForData from the catch path when the deep link fails.

void this.tryDeepLinkForData(data)
  .then((didNavigate) => {
    if (!didNavigate) {
      this.showModalForData(data, content.title, content.body);
    }
  })
  .catch(() => {
    this.showModalForData(data, content.title, content.body);
  });
Prompt for LLM

File src/services/push-notification.ts:

Line 363 to 366:

Deep-link success is reported in src/services/push-notification.ts before the async navigation completes, so failed routerPushWithRetry retries or a waitUntil condition that never becomes true suppress the modal fallback and drop tapped call/chat notifications after only logging an error. Make tryDeepLinkForData async and suppress showModalForData only after routerPushWithRetry resolves, or call showModalForData from the catch path when the deep link fails.

Suggested Code:

      void this.tryDeepLinkForData(data)
        .then((didNavigate) => {
          if (!didNavigate) {
            this.showModalForData(data, content.title, content.body);
          }
        })
        .catch(() => {
          this.showModalForData(data, content.title, content.body);
        });

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/services/push-notification.ts Outdated
Comment on lines +291 to +303
if (parsed.type === 'call' && parsed.id !== '' && !/[/\\?#]/.test(parsed.id)) {
void routerPushWithRetry(
{ pathname: '/call/[id]', params: { id: parsed.id } },
{
maxAttempts: 40,
retryDelayMs: 250,
// On a cold start the session is still hydrating — see handleChatDeepLink.
waitUntil: () => useAuthStore.getState().status === 'signedIn',
}
).catch((error) => {
logger.error({ message: 'Failed to deep-link to call from push notification', context: { error, eventCode } });
});
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug high

False success in tryDeepLinkForData causes tap handlers in src/services/push-notification.ts to skip the modal fallback before routerPushWithRetry actually navigates. Return true only after routerPushWithRetry resolves, and return false after logging the error when the retry budget expires or useAuthStore.getState().status === 'signedIn' never becomes true so the caller can fall back to showNotificationModal.

if (parsed.type === 'call' && parsed.id !== '' && !/[/\\?#]/.test(parsed.id)) {
  try {
    await routerPushWithRetry(
      { pathname: '/call/[id]', params: { id: parsed.id } },
      {
        maxAttempts: 40,
        retryDelayMs: 250,
        waitUntil: () => useAuthStore.getState().status === 'signedIn',
      }
    );
    return true;
  } catch (error) {
    logger.error({ message: 'Failed to deep-link to call from push notification', context: { error, eventCode } });
    return false;
  }
}
Prompt for LLM

File src/services/push-notification.ts:

Line 291 to 303:

False success in tryDeepLinkForData causes tap handlers in src/services/push-notification.ts to skip the modal fallback before routerPushWithRetry actually navigates. Return true only after routerPushWithRetry resolves, and return false after logging the error when the retry budget expires or useAuthStore.getState().status === 'signedIn' never becomes true so the caller can fall back to showNotificationModal.

Suggested Code:

if (parsed.type === 'call' && parsed.id !== '' && !/[/\\?#]/.test(parsed.id)) {
  try {
    await routerPushWithRetry(
      { pathname: '/call/[id]', params: { id: parsed.id } },
      {
        maxAttempts: 40,
        retryDelayMs: 250,
        waitUntil: () => useAuthStore.getState().status === 'signedIn',
      }
    );
    return true;
  } catch (error) {
    logger.error({ message: 'Failed to deep-link to call from push notification', context: { error, eventCode } });
    return false;
  }
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

});

// Delay so the React tree is mounted and the modal store is ready.
setTimeout(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Timer leak risk in src/services/push-notification.ts: the added setTimeout starts asynchronous work without a deterministic cleanup path. Store the timeout handle and clear it during teardown or disposal, or ensure the surrounding lifecycle guarantees the timer cannot outlive the service.

Kody rule violation: Clear timers on teardown/unmount

const timeoutId = setTimeout(() => {
  // Deep-link chat and call notifications straight to their screen; anything
  // else falls back to the persistent modal.
  if (this.tryDeepLinkForData(data)) {
    return;
  }
  this.showModalForData(data, content.title, content.body);
}, 500);
// clearTimeout(timeoutId) in the deterministic teardown/unmount path if this listener/service can be disposed before it fires.
Prompt for LLM

File src/services/push-notification.ts:

Line 360:

Timer leak risk in src/services/push-notification.ts: the added setTimeout starts asynchronous work without a deterministic cleanup path. Store the timeout handle and clear it during teardown or disposal, or ensure the surrounding lifecycle guarantees the timer cannot outlive the service.

Suggested Code:

      const timeoutId = setTimeout(() => {
        // Deep-link chat and call notifications straight to their screen; anything
        // else falls back to the persistent modal.
        if (this.tryDeepLinkForData(data)) {
          return;
        }
        this.showModalForData(data, content.title, content.body);
      }, 500);
      // clearTimeout(timeoutId) in the deterministic teardown/unmount path if this listener/service can be disposed before it fires.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

Resgrid-Bot commented Aug 19, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

// microtask after the timer — advanceTimersByTimeAsync flushes both.
it('falls back to the modal for a message tap', async () => {
responseHandler(makeResponse('tap-msg', { eventCode: 'M:5' }));
await jest.advanceTimersByTimeAsync(400);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

This awaited async operation is not guarded with error handling. Wrap the await in try/catch so timer-flush failures are handled explicitly and do not become unhandled rejections.

Also found in:

  • src/services/__tests__/push-notification.test.ts:619-619
  • src/services/__tests__/push-notification.test.ts:609-609
  • src/services/__tests__/push-notification.test.ts:629-629

Kody rule violation: Handle async operations with proper error handling

try {
  await jest.advanceTimersByTimeAsync(400);
} catch (err) {
  throw err;
}
Prompt for LLM

File src/services/__tests__/push-notification.test.ts:

Line 601:

This awaited async operation is not guarded with error handling. Wrap the await in try/catch so timer-flush failures are handled explicitly and do not become unhandled rejections.

Suggested Code:

      try {
        await jest.advanceTimersByTimeAsync(400);
      } catch (err) {
        throw err;
      }

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

await routerPushWithRetry(href, DEEP_LINK_RETRY_OPTIONS);
return true;
} catch (error) {
logger.error({ message: failureMessage, context: { error, eventCode } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Include explicit structured context fields for the operation and identifiers at the top level of the log payload. This makes the error log compliant with structured logging requirements and easier to query by operation/eventCode.

Kody rule violation: Include error context in structured logs

logger.error({ message: failureMessage, op: 'deepLinkWithRetry', eventCode, error });
Prompt for LLM

File src/services/push-notification.ts:

Line 107:

Include explicit structured context fields for the operation and identifiers at the top level of the log payload. This makes the error log compliant with structured logging requirements and easier to query by operation/eventCode.

Suggested Code:

    logger.error({ message: failureMessage, op: 'deepLinkWithRetry', eventCode, error });

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

});

// Delay so the React tree is mounted and the modal store is ready.
setTimeout(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Store the timeout handle and provide a deterministic cleanup path that clears it. Starting a timer without retaining and clearing it can leave pending work running after teardown.

Kody rule violation: Clear timers on teardown/unmount

const timeoutId = setTimeout(() => {
  // ...
}, delayMs);
// ensure timeoutId is cleared during teardown/unmount or service cleanup
Prompt for LLM

File src/services/push-notification.ts:

Line 378:

Store the timeout handle and provide a deterministic cleanup path that clears it. Starting a timer without retaining and clearing it can leave pending work running after teardown.

Suggested Code:

    const timeoutId = setTimeout(() => {
      // ...
    }, delayMs);
    // ensure timeoutId is cleared during teardown/unmount or service cleanup

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@ucswift

ucswift commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift
ucswift merged commit e8521dc into master Aug 19, 2026
19 of 20 checks passed
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