Skip to content

🧵 app: fix onesignal prompt persistence race condition#669

Merged
cruzdanilo merged 2 commits intomainfrom
onesignal
Jan 23, 2026
Merged

🧵 app: fix onesignal prompt persistence race condition#669
cruzdanilo merged 2 commits intomainfrom
onesignal

Conversation

@cruzdanilo
Copy link
Member

@cruzdanilo cruzdanilo commented Jan 22, 2026


Open with Devin

Summary by CodeRabbit

  • New Features

    • App hydration now exposes a readiness promise and a persistence API so UI flows can await and save restored state.
  • Bug Fixes

    • Notification prompt handling now awaits hydration before checking dismissal, reliably removes/stores prompts, and reports persistence errors to avoid race conditions.
  • Chores

    • Added a release patch note and improved local emulator startup sequencing for more reliable testing.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 22, 2026

🦋 Changeset detected

Latest commit: 1aa1fe6

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist
Copy link

Summary of Changes

Hello @cruzdanilo, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a timing issue where OneSignal in-app message prompts might be evaluated using stale or unhydrated data. By ensuring that the application's query client has fully restored its state from persistence before checking notification dismissal status, the system now accurately determines when to display in-app messages, leading to more reliable user experience.

Highlights

  • Query Client Hydration: Introduced a mechanism to explicitly wait for the queryClient to hydrate its state from persistence before proceeding with certain operations.
  • OneSignal Prompt Logic: Modified the enablePrompt function in onesignal.ts to ensure that the check for notification dismissal (lastDismiss) only occurs after the queryClient has been fully hydrated, preventing potential race conditions.
  • Exported Hydration Promise: Exported a hydrated promise from queryClient.ts which resolves once the query client's state has been restored, making this state available for other parts of the application.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Warning

Rate limit exceeded

@cruzdanilo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 32 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Adds client-side hydration promise and a persist() API to the query client; OneSignal onboarding prompt gating now awaits hydration and persists dismissal state; adds a changeset noting the OneSignal fix; updates local Android emulator startup flags and readiness waits.

Changes

Cohort / File(s) Summary
Changeset documentation
\.changeset/good-tips-care.md
Add patch changeset noting OneSignal prompt persistence race-condition fix.
Query client hydration & persistence
src/utils/queryClient.ts
Export hydrated: Promise<void> and persist(); add dehydrateOptions; set persister throttleTime: 0; move restore/subscribe behind hydration/client guard; add error handling for hydration.
OneSignal prompt gating
src/utils/onesignal.ts
Replace synchronous last-dismiss gating with async flow that awaits hydrated; read restored state for gating; call persist().catch(reportError) on dismiss; add hydration error handling.
Local Android emulator workflow
.eas/workflows/local.yaml
Restart ADB server before startup; add -netfast and adjust cores/RAM; introduce background readiness probe and timeout-bounded waits; run adb devices -l.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant QueryClient
  participant Persister
  participant OneSignal

  Browser->>QueryClient: import `hydrated` (Promise)
  Browser->>Persister: client-only restore/subscribe after hydration
  Persister-->>QueryClient: restored state
  OneSignal->>QueryClient: enablePrompt() called
  OneSignal->>QueryClient: await hydrated.then(...).catch(reportError)
  QueryClient-->>OneSignal: hydrated resolved / restored data available
  OneSignal->>QueryClient: read lastDismiss from restored state
  alt appId present and not dismissed
    OneSignal->>OneSignal: show onboarding prompt
  end
  OneSignal->>QueryClient: onDismiss -> call persist().catch(reportError)
  QueryClient->>Persister: persister.persistClient(dehydrated state)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • 🐛 app fixes #664 — modifies src/utils/queryClient.ts and related OneSignal initialization; closely related.

Suggested reviewers

  • dieguezguille
  • franm91
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the pull request—fixing a OneSignal prompt persistence race condition. It directly corresponds to the core changes in src/utils/onesignal.ts and src/utils/queryClient.ts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch onesignal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses a potential race condition by ensuring that the queryClient is fully hydrated before attempting to read data for the OneSignal notification dismiss check. The introduction of the hydrated promise in queryClient.ts and its subsequent use in onesignal.ts provides a robust and asynchronous way to manage this dependency. The changes are well-implemented and include appropriate error handling, improving the reliability of the notification prompt logic.

@sentry
Copy link

sentry bot commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 70.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.46%. Comparing base (ef7185d) to head (1aa1fe6).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/utils/onesignal.ts 62.50% 3 Missing ⚠️
src/utils/queryClient.ts 75.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #669      +/-   ##
==========================================
+ Coverage   60.27%   61.46%   +1.19%     
==========================================
  Files         169      169              
  Lines        5226     5240      +14     
  Branches     1479     1481       +2     
==========================================
+ Hits         3150     3221      +71     
+ Misses       1908     1858      -50     
+ Partials      168      161       -7     
Flag Coverage Δ
e2e 61.45% <70.00%> (+1.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional flags.

Open in Devin Review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/utils/onesignal.ts`:
- Around line 75-81: Extract the multi-line inline callback passed to
hydrated.then into a named async (or regular) function—e.g., create a function
like handleHydration() that performs the current logic: read lastDismiss via
queryClient.getQueryData<number>(["onesignal","dismiss"]) ?? 0, check appId and
DISMISS_EXPIRY against Date.now(), and call
OneSignal.InAppMessages.addTrigger("onboard","1") when appropriate; then replace
hydrated.then(() => { ... }).catch(reportError) with
hydrated.then(handleHydration).catch(reportError) (and keep reportError usage
unchanged). Ensure the new function is defined in the same module and references
the same symbols (queryClient, appId, DISMISS_EXPIRY, OneSignal, reportError).

@cruzdanilo cruzdanilo changed the title 🩹 app: wait query hydration before notification dismiss check 🧵 app: fix onesignal prompt persistence race condition Jan 22, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @.changeset/good-tips-care.md:
- Line 5: Update the changeset entry that currently reads "🧵 fix onesignal
prompt persistence race condition" by replacing the thread emoji (🧵) with the
patch/adhesive bandage emoji (🩹) so the header becomes "🩹 fix onesignal prompt
persistence race condition"; locate the changeset file containing that header
string and edit the first token to the correct emoji to match the PR title and
semantic meaning.

In `@src/utils/queryClient.ts`:
- Around line 35-38: The hydrated promise can resolve to void on success but to
the value returned by reportError on error; change the catch to explicitly
return void so the promise always resolves to void. Update the expression that
builds hydrated (which currently calls persistQueryClientRestore({ queryClient,
persister, maxAge: ... }).catch(reportError)) to instead call .catch((err) => {
reportError(err); /* return void */ }) or chain a .then(() => undefined) after
the call so that hydrated consistently resolves to void; reference hydrated,
persistQueryClientRestore, reportError, queryClient and persister when locating
the code to change.
- Around line 47-55: Add an inline comment in the persist() function explaining
that buster is intentionally an empty string to disable version-based cache
invalidation because the codebase relies on maxAge (30 days) for staleness;
update the comment near the persister.persistClient call (and mention the buster
field) so future maintainers know this is a deliberate design choice and not a
placeholder.
♻️ Duplicate comments (1)
src/utils/onesignal.ts (1)

76-82: Extract the multi-line hydration handler into a named function.

The inline callback passed to hydrated.then is multi-line; prefer a function declaration to align with the coding guidelines about using function declarations for multi-line functions.

♻️ Proposed refactor
         OneSignal.InAppMessages.addEventListener("didDismiss", () => {
           queryClient.setQueryData(["onesignal", "dismiss"], Date.now());
           persist().catch(reportError);
         });
+        function triggerOnboardingPromptIfAllowed() {
+          const lastDismiss = queryClient.getQueryData<number>(["onesignal", "dismiss"]) ?? 0;
+          if (!appId || lastDismiss + DISMISS_EXPIRY >= Date.now()) return;
+          OneSignal.InAppMessages.addTrigger("onboard", "1");
+        }
         return {
           enablePrompt: () => {
-            hydrated
-              .then(() => {
-                const lastDismiss = queryClient.getQueryData<number>(["onesignal", "dismiss"]) ?? 0;
-                if (!appId || lastDismiss + DISMISS_EXPIRY >= Date.now()) return;
-                OneSignal.InAppMessages.addTrigger("onboard", "1");
-              })
-              .catch(reportError);
+            hydrated.then(triggerOnboardingPromptIfAllowed).catch(reportError);
           },

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.eas/workflows/local.yaml:
- Line 59: The YAML inline comment after the "name: start emulator" entry has
only one space before the '#' which violates lint rules; update the line
containing the name: start emulator key so there are two spaces before the
inline comment (i.e., ensure "name: start emulator  # cspell:ignore ..."),
preserving the existing comment text and spacing elsewhere.
♻️ Duplicate comments (3)
src/utils/queryClient.ts (2)

35-38: Hydration promise implementation is correct for the race condition fix.

The hydrated promise ensures persistence restoration completes before any code reads persisted query data. The server-side check (typeof window === "undefined") correctly short-circuits to an immediate resolution.

Note: A previous review flagged that the promise type resolves inconsistently (void vs string from reportError). That's still applicable here.


47-55: The Promise.resolve() wrapper may be redundant.

If persister.persistClient() already returns a promise (which it does based on the async-storage persister API), wrapping it in Promise.resolve() is unnecessary. However, this is a minor nitpick and doesn't affect correctness.

The empty buster comment concern was flagged in a previous review.

♻️ Optional simplification
 export function persist() {
-  return Promise.resolve(
-    persister.persistClient({
-      timestamp: Date.now(),
-      buster: "",
-      clientState: dehydrate(queryClient, dehydrateOptions),
-    }),
-  );
+  return persister.persistClient({
+    timestamp: Date.now(),
+    buster: "",
+    clientState: dehydrate(queryClient, dehydrateOptions),
+  });
 }
src/utils/onesignal.ts (1)

80-86: Consider extracting the multi-line callback (per past review).

A previous review suggested extracting the inline callback passed to shouldPrompt.then() into a named function, per coding guidelines preferring function declarations for multi-line functions. This is a style nitpick.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/utils/queryClient.ts`:
- Around line 55-63: The persist function wraps persister.persistClient(...) in
an unnecessary Promise.resolve; modify persist to return the promise directly by
calling persister.persistClient with the same payload (timestamp: Date.now(),
buster: "", clientState: dehydrate(queryClient, dehydrateOptions)). Locate the
persist function and replace the Promise.resolve(...) wrapper so the function
directly returns persister.persistClient(...) (keep dehydrate, queryClient, and
dehydrateOptions usage unchanged).
♻️ Duplicate comments (1)
src/utils/onesignal.ts (1)

76-84: Core race condition fix looks correct.

Awaiting hydrated before checking the dismiss timestamp ensures the persisted state is loaded before making the prompt decision. The logic correctly suppresses the prompt if the user dismissed within the last 30 days.


Extract the inline callback to a named function.

Per coding guidelines, prefer function declarations for multi-line functions.

♻️ Proposed refactor
+        function maybeShowOnboardingPrompt() {
+          const lastDismiss = queryClient.getQueryData<number>(["onesignal", "dismiss"]) ?? 0;
+          if (!appId || lastDismiss + DISMISS_EXPIRY >= Date.now()) return;
+          OneSignal.InAppMessages.addTrigger("onboard", "1");
+        }
         return {
           enablePrompt: () => {
-            hydrated
-              .then(() => {
-                const lastDismiss = queryClient.getQueryData<number>(["onesignal", "dismiss"]) ?? 0;
-                if (!appId || lastDismiss + DISMISS_EXPIRY >= Date.now()) return;
-                OneSignal.InAppMessages.addTrigger("onboard", "1");
-              })
-              .catch(reportError);
+            hydrated.then(maybeShowOnboardingPrompt).catch(reportError);
           },

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View issue and 7 additional flags in Devin Review.

Open in Devin Review

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View issue and 11 additional flags in Devin Review.

Open in Devin Review

@cruzdanilo cruzdanilo merged commit 1aa1fe6 into main Jan 23, 2026
15 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.

1 participant