Skip to content

feat: Multiple SDK-level fixes for anonymous-to-password conversion (iOS parity)#1136

Closed
chadpav wants to merge 9 commits into
parse-community:masterfrom
chadpav:fix/anonymous-link-cleanup
Closed

feat: Multiple SDK-level fixes for anonymous-to-password conversion (iOS parity)#1136
chadpav wants to merge 9 commits into
parse-community:masterfrom
chadpav:fix/anonymous-link-cleanup

Conversation

@chadpav
Copy link
Copy Markdown
Contributor

@chadpav chadpav commented Apr 28, 2026

Issue

No existing issue; problems described below.

Approach

The Dart SDK's anonymous-to-password user conversion
(user.username = email; user.password = pw; user.save() after
loginAnonymous()) has three independent gaps vs the iOS SDK. All
three surfaced during empirical testing in a production-mirror setup
as causes of invalidSessionToken rejections or orphan _Session
row accumulation. This PR ports iOS's behavior into the Dart SDK
across three logically-distinct commits.

Fix 1 — Reconcile local authData on conversion

When a persisted anonymous user has its username set and is saved,
parseUser.save() issues a PUT /classes/_User/:objectId carrying
only the dirty fields. The Dart SDK treated this as a generic
property update — it neither emitted an authData unlink signal in
the request body nor cleaned up the local cache after the save.

The iOS Parse SDK handles this via two helpers in PFUser.m:

  1. stripAnonymity (PFUser.m:593-607): the username setter
    writes authData[anonymous] = NSNull into the local user. The
    PUT body re-injects the map, so the request carries
    authData: { anonymous: null } — Parse Server's documented
    unlink signal.
  2. cleanUpAuthData (PFUser.m:300-313): after a successful
    save, the local authData has any null entries stripped.

Ported to ParseUser as two private helpers:

  • _stripAnonymity() invoked from the username setter. On a
    persisted user it sets authData['anonymous'] = null locally; on
    a lazy user (no objectId) it removes the entry outright.
  • _cleanUpAuthData() invoked from save() and update() between
    the response merge and _onResponseSuccess().

A dartdoc paragraph was added to loginAnonymous documenting the
conversion path.

Fix 2 — Adopt sessionToken from the save/update response

Parse Server mints a fresh _Session row when password is set on
an existing _User (with revokeSessionOnPasswordReset = true, the
current default in parse-server 9.x). The new sessionToken is
embedded in the save response. Without adopting it, the global
session in ParseCoreData keeps pointing at the prior session —
which the server just destroyed — and every subsequent request
fails with invalidSessionToken until the next login.

This mirrors PFUser._mergeFromServerWithResult on iOS, which reads
PFUserSessionTokenRESTKey out of the response and installs it.

Implemented as _adoptResponseSessionTokenIfChanged(String?)
invoked from save() and update(). The helper is a no-op when
the response carries no sessionToken (e.g. a plain field update),
preserving the existing behavior for non-auth saves.

Fix 3 — Send X-Parse-Installation-Id on all HTTP requests

iOS's PFURLSessionCommandRunner attaches the install ID header to
every outgoing request
(PFCommandURLRequestConstructor.m:_getURLRequestHeadersAsyncForCommand:).
The Dart SDK previously built the header inline only in the four
user-creation methods (signUp, login, loginAnonymous,
loginWith). Every other call — including PUT save on an
existing user — went out without it.

Without the install ID on writes, parse-server cannot bind the
_Session row it mints during an authData/password change to the
device's installation, and destroyDuplicatedSessions skips
cleanup of prior sessions on the same install. Empirically this
leaves one orphan _Session row per anonymous-to-password link.

Changes:

  • Added sendInstallationId: bool? to ParseNetworkOptions
    (null/true → attach, false → suppress).
  • Hoisted a shared buildHeaders helper onto the abstract
    ParseClient (@protected); both ParseHTTPClient and
    ParseDioClient inherit and route through it.
  • ParseUser.signUp/login/loginAnonymous/_loginWith drop their
    inline _getInstallationId() and pass
    sendInstallationId: !doNotSendInstallationID through options —
    the user-facing opt-out parameter continues to work.

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)

Behavior change disclosure

  1. Wire — anonymous conversion save: when objectId is set
    and authData contains an anonymous entry, setting username
    and saving now produces a PUT body that includes
    authData: { anonymous: null }. Previously no authData field
    was sent for that save.
  2. Local cache — anonymous conversion: setting username on
    an anonymous-with-objectId ParseUser now mutates the local
    authData map. After a successful save, the anonymous entry
    is removed entirely.
  3. Session token — save/update response: when a _User
    save/update response carries a sessionToken field (i.e.
    parse-server minted a new session), the SDK installs it as the
    active session via ParseCoreData().setSessionId(...).
    Previously the new token was merged into the local user object
    but the global session continued pointing at the prior token.
  4. Request headers — all HTTP requests: every outgoing
    request now carries X-Parse-Installation-Id by default.
    Previously only signUp, login, loginAnonymous, and
    loginWith set it. Opt-out is preserved via
    ParseNetworkOptions(sendInstallationId: false) and the
    existing doNotSendInstallationID parameter on user methods.

The commit prefixes mix fix: and feat:. The PR is labeled
feat: overall — the install-ID change is a feature-level default
change, even with opt-out preserved.

If maintainers prefer to split commits 83244367 and 5b2cd09e
into separate PRs, they are independent of each other and of the
original three commits and can be cherry-picked onto clean branches.

Documentation follow-up (out of scope for this PR)

This PR adds a dartdoc paragraph to loginAnonymous covering the
conversion pattern. The companion user guide at
https://docs.parseplatform.org/dart/guide/ (separate repo) has no
section equivalent to the iOS guide's "Linking Anonymous Users".
Mirroring that section into the Dart guide is a natural follow-up
once this PR lands.

Tests

Added to packages/dart/test/src/:

  • objects/parse_user/parse_user_anonymous_link_test.dart — 6
    tests covering Fix 1 (authData reconciliation behavior).
  • objects/parse_user/parse_user_session_token_test.dart (new) —
    3 tests covering Fix 2 (sessionToken adoption from save/update
    response).
  • network/parse_client_test.dart (new) — 4 unit tests covering
    Fix 3's buildHeaders helper (default attach, opt-out via
    sendInstallationId, preserve caller-supplied header, merge with
    custom headers).
  • network/parse_dio_client_test.dart — 1 new integration test
    verifying buildHeaders output reaches dio's request pipeline
    via a custom HttpClientAdapter.
  • network/parse_http_client_test.dart — copy-pasted
    "ParseDioClient" group label corrected to "ParseHTTPClient".

Total: 14 new tests across 4 test files. All 221 tests in
packages/dart/test/ pass.

Setting username on a persisted anonymous user and calling save() now
unlinks the anonymous provider server-side and leaves the local
authData consistent with the cleaned server record — without requiring
a follow-up GET.

Previously, the save's PUT response carried only the dirty fields
written by the client (no authData), so the additive response merge
in _handleSingleResult could not reconcile the local authData copy.
Apps had to follow up with GET /users/me to refresh local state.

This mirrors iOS PFUser's stripAnonymity + cleanUpAuthData pattern.
No public API changes; the new helpers are private.
@parse-github-assistant
Copy link
Copy Markdown

parse-github-assistant Bot commented Apr 28, 2026

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Tip

  • Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs.
  • Use Test Driven Development. Write failing tests before implementing functionality. Ensure tests pass.
  • Group code into logical blocks. Add a short comment before each block to explain its purpose.
  • We offer conceptual guidance. Coding is up to you. PRs must be merge-ready for human review.
  • Our review focuses on concept, not quality. PRs with code issues will be rejected. Use an AI agent.
  • Human review time is precious. Avoid review ping-pong. Inspect and test your AI-generated code.

Note

Please respond to review comments from AI agents just like you would to comments from a human reviewer. Let the reviewer resolve their own comments, unless they have reviewed and accepted your commit, or agreed with your explanation for why the feedback was incorrect.

Caution

Pull requests must be written using an AI agent with human supervision. Pull requests written entirely by a human will likely be rejected, because of lower code quality, higher review effort and the higher risk of introducing bugs. Please note that AI review comments on this pull request alone do not satisfy this requirement. Our CI and AI review are safeguards, not development tools. If many issues are flagged, rethink your development approach. Invest more effort in planning and design rather than using review cycles to fix low-quality code.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds local reconciliation for anonymous authData in ParseUser: setting username strips or nulls anonymous provider entries, and post-save/update cleanup removes staged nulls. Also adopts response sessionToken after save/update. Separately, centralizes per-request installation-id header logic and threads it through Dio/HTTP clients; tests validate both behaviors.

Changes

Anonymous Auth Data Stripping and Session-Token Adoption

Layer / File(s) Summary
Auth key, username setter, and docs
packages/dart/lib/src/objects/parse_user.dart
Adds _keyAuthAnonymous, updates username setter to call _stripAnonymity(), and expands loginAnonymous documentation.
Login/signUp request options simplification
packages/dart/lib/src/objects/parse_user.dart
signUp, login, loginAnonymous, and _loginWith now use sendInstallationId: !doNotSendInstallationID and simplified revocable-session header handling; removes _getInstallationId().
Save/Update sessionToken adoption and authData cleanup
packages/dart/lib/src/objects/parse_user.dart, packages/dart/test/src/objects/parse_user/parse_user_session_token_test.dart
save() and update() capture prior sessionToken, call _adoptResponseSessionTokenIfChanged, run _cleanUpAuthData() before success handling; adds tests asserting ParseCoreData.sessionId behavior.

Network header pipeline and tests

Layer / File(s) Summary
ParseNetworkOptions and buildHeaders
packages/dart/lib/src/network/options.dart, packages/dart/lib/src/network/parse_client.dart
Adds sendInstallationId field to ParseNetworkOptions and protected async buildHeaders(ParseNetworkOptions?) to conditionally attach X-Parse-Installation-Id.
ParseDioClient integration
packages/dart/lib/src/network/parse_dio_client.dart
Threads await buildHeaders(options) into get, getBytes, put, post, postBytes, and delete calls so computed headers are forwarded to Dio.
ParseHTTPClient integration
packages/dart/lib/src/network/parse_http_client.dart
Threads await buildHeaders(options) into get, getBytes, put, post, postBytes, and delete calls so computed headers are forwarded to the underlying http client.
Header unit & integration tests
packages/dart/test/src/network/*
Adds tests for ParseClient.buildHeaders, a header-capturing Dio adapter integration test, and minor http client test wording updates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Engage In Review Feedback ❓ Inconclusive Only one visible commit in repo (6a53cfa formats code per CI feedback). Prior feature commits are referenced but not present. Cannot verify full feedback engagement. Review full PR #1136 history on GitHub including all commits, comments, and responses to verify engagement with all feedback items.
✅ Passed checks (6 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed No security vulnerabilities found. Session tokens, auth data cleanup, and header handling are all secure. Installation IDs are type-safe UUIDs. Error handling doesn't expose sensitive data.
Title check ✅ Passed The title begins with 'feat:' prefix and clearly summarizes the main change: SDK-level fixes for anonymous-to-password conversion.
Description check ✅ Passed The description follows the template structure with Issue, Approach, and Tasks sections completed. All three fixes are documented with implementation details and behavior changes are disclosed.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 61.53846% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.60%. Comparing base (785f76b) to head (9cd24a5).

Files with missing lines Patch % Lines
...ackages/dart/lib/src/network/parse_dio_client.dart 18.18% 9 Missing ⚠️
packages/dart/lib/src/objects/parse_user.dart 78.37% 8 Missing ⚠️
...ckages/dart/lib/src/network/parse_http_client.dart 0.00% 6 Missing ⚠️
packages/dart/lib/src/network/parse_client.dart 80.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1136      +/-   ##
==========================================
+ Coverage   44.42%   46.60%   +2.18%     
==========================================
  Files          62       62              
  Lines        3730     3761      +31     
==========================================
+ Hits         1657     1753      +96     
+ Misses       2073     2008      -65     

☔ 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
Copy Markdown

@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.

🧹 Nitpick comments (2)
packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart (1)

63-105: Please add an update() variant of this regression.

ParseUser.update() now has its own cleanup hook too, but the suite only exercises save(). A matching update() case would keep those two paths from drifting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart`
around lines 63 - 105, Add a parallel test that mirrors the existing "after a
successful save..." case but invokes ParseUser.update() instead of save(); use
anonymousUserWithObjectId() to create the user, set username/password the same
way, stub the same network response (matching the current mocked call pattern -
e.g. client.put/putPath or client.patch if update uses PATCH) and assert that
user.authData no longer contains 'anonymous' after a successful update response;
include the same expect checks and reason text so update()'s cleanup hook is
covered the same as save().
packages/dart/lib/src/objects/parse_user.dart (1)

539-545: Drop empty authData in the unsaved-user branch.

If anonymous was the only provider, Lines 539-545 leave authData as {} in local state. Removing keyVarAuthData entirely here would keep lazy users fully reconciled instead of carrying a synthetic empty map forward.

♻️ Proposed change
   if (objectId == null) {
     authData.remove(_keyAuthAnonymous);
+    if (authData.isEmpty) {
+      _objectData.remove(keyVarAuthData);
+      _unsavedChanges.remove(keyVarAuthData);
+      return;
+    }
   } else {
     authData[_keyAuthAnonymous] = null;
   }
   _unsavedChanges[keyVarAuthData] = authData;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dart/lib/src/objects/parse_user.dart` around lines 539 - 545, The
branch that updates auth providers leaves an empty map in
_unsavedChanges[keyVarAuthData] when anonymous was the only provider; update the
logic in the function handling objectId/authData (referencing objectId,
authData, _keyAuthAnonymous, _unsavedChanges, keyVarAuthData) so that after
removing the anonymous provider you check if authData.isEmpty and, if so, remove
keyVarAuthData from _unsavedChanges instead of assigning an empty map; otherwise
keep assigning authData as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/dart/lib/src/objects/parse_user.dart`:
- Around line 539-545: The branch that updates auth providers leaves an empty
map in _unsavedChanges[keyVarAuthData] when anonymous was the only provider;
update the logic in the function handling objectId/authData (referencing
objectId, authData, _keyAuthAnonymous, _unsavedChanges, keyVarAuthData) so that
after removing the anonymous provider you check if authData.isEmpty and, if so,
remove keyVarAuthData from _unsavedChanges instead of assigning an empty map;
otherwise keep assigning authData as before.

In
`@packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart`:
- Around line 63-105: Add a parallel test that mirrors the existing "after a
successful save..." case but invokes ParseUser.update() instead of save(); use
anonymousUserWithObjectId() to create the user, set username/password the same
way, stub the same network response (matching the current mocked call pattern -
e.g. client.put/putPath or client.patch if update uses PATCH) and assert that
user.authData no longer contains 'anonymous' after a successful update response;
include the same expect checks and reason text so update()'s cleanup hook is
covered the same as save().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bfa836a-98b4-4656-84cf-277a08836115

📥 Commits

Reviewing files that changed from the base of the PR and between 785f76b and 0860324.

📒 Files selected for processing (2)
  • packages/dart/lib/src/objects/parse_user.dart
  • packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart

coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 28, 2026
chadpav added 2 commits April 28, 2026 09:43
save() and update() both run _cleanUpAuthData() in their success branch
but were patched independently. Add a parallel test that invokes
update() instead of save() so a future regression in either path is
caught.

Also tightened test descriptions to focus on client-side behavior.
On the lazy (no objectId) path of _stripAnonymity, removing the
anonymous provider could leave an empty map in _unsavedChanges
under keyVarAuthData. The empty map then serialized into the POST
body via toJson(forApiRQ: true) on signUp, sending wire noise the
SDK shouldn't emit. Mirrors the isEmpty handling already present
in _cleanUpAuthData.
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
packages/dart/lib/src/objects/parse_user.dart (1)

551-568: Consider extracting duplicated null-pruning into a tiny helper.

_cleanUpAuthData() repeats the same “remove null providers, drop map if empty” flow for _objectData and _unsavedChanges. A helper would reduce drift risk.

♻️ Optional refactor sketch
+  void _pruneNullAuthProviders(Map<String, dynamic> map) {
+    map.removeWhere((_, dynamic value) => value == null);
+  }
+
   void _cleanUpAuthData() {
     final Map<String, dynamic>? authData =
         _objectData[keyVarAuthData] as Map<String, dynamic>?;
     if (authData != null) {
-      authData.removeWhere((_, dynamic value) => value == null);
+      _pruneNullAuthProviders(authData);
       if (authData.isEmpty) {
         _objectData.remove(keyVarAuthData);
       }
     }
     final Map<String, dynamic>? dirty =
         _unsavedChanges[keyVarAuthData] as Map<String, dynamic>?;
     if (dirty != null) {
-      dirty.removeWhere((_, dynamic value) => value == null);
+      _pruneNullAuthProviders(dirty);
       if (dirty.isEmpty) {
         _unsavedChanges.remove(keyVarAuthData);
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dart/lib/src/objects/parse_user.dart` around lines 551 - 568,
Extract the repeated "remove null entries and drop empty map" logic in
_cleanUpAuthData into a small helper (e.g., _pruneNullMap) that takes a
Map<String, dynamic>? and the container map to remove the key from (or returns a
bool indicating emptiness); then call it for both _objectData[keyVarAuthData]
and _unsavedChanges[keyVarAuthData] instead of duplicating the removeWhere +
isEmpty checks. Ensure the helper uses the same null-safe casts and removes
keyVarAuthData from the appropriate parent map when the child map becomes empty,
preserving current behavior of _cleanUpAuthData.
packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart (1)

18-23: Optional: de-duplicate repeated PUT stubbing and provider key literals.

A small local helper (and a shared const anonymousProvider = 'anonymous') would make these tests easier to evolve.

Also applies to: 67-82, 105-120, 146-161, 192-205, 244-247, 266-281

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart`
around lines 18 - 23, Duplicate literals and repeated PUT stubbing in
parse_user_anonymous_link_test.dart should be consolidated: introduce a shared
const anonymousProvider = 'anonymous' and a small helper function (e.g.,
buildPutPath(userObjectId) or stubPutForUser(userObjectId, body)) to return the
putPath or register the PUT stub using existing symbols keyEndPointClasses,
keyClassUser, userObjectId and anonymousId; replace repeated inline
Uri.parse(...) and literal 'anonymous' occurrences with the new helper/const and
update tests that reference putPath, userObjectId, anonymousId to call the
helper to remove duplication and make stubbing consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/dart/lib/src/objects/parse_user.dart`:
- Around line 551-568: Extract the repeated "remove null entries and drop empty
map" logic in _cleanUpAuthData into a small helper (e.g., _pruneNullMap) that
takes a Map<String, dynamic>? and the container map to remove the key from (or
returns a bool indicating emptiness); then call it for both
_objectData[keyVarAuthData] and _unsavedChanges[keyVarAuthData] instead of
duplicating the removeWhere + isEmpty checks. Ensure the helper uses the same
null-safe casts and removes keyVarAuthData from the appropriate parent map when
the child map becomes empty, preserving current behavior of _cleanUpAuthData.

In
`@packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart`:
- Around line 18-23: Duplicate literals and repeated PUT stubbing in
parse_user_anonymous_link_test.dart should be consolidated: introduce a shared
const anonymousProvider = 'anonymous' and a small helper function (e.g.,
buildPutPath(userObjectId) or stubPutForUser(userObjectId, body)) to return the
putPath or register the PUT stub using existing symbols keyEndPointClasses,
keyClassUser, userObjectId and anonymousId; replace repeated inline
Uri.parse(...) and literal 'anonymous' occurrences with the new helper/const and
update tests that reference putPath, userObjectId, anonymousId to call the
helper to remove duplication and make stubbing consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12dc8694-f354-42fe-8a92-7a38f83366ba

📥 Commits

Reviewing files that changed from the base of the PR and between 0860324 and 64ec48a.

📒 Files selected for processing (2)
  • packages/dart/lib/src/objects/parse_user.dart
  • packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart

coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 28, 2026
@chadpav
Copy link
Copy Markdown
Contributor Author

chadpav commented Apr 28, 2026

@mtrezza There are a few other PR's open. Do we have people merging PR's? You can add me as a maintainer if needed.

@chadpav
Copy link
Copy Markdown
Contributor Author

chadpav commented May 13, 2026

@parse-community/ios-sdk-maintenance do we have any active maintainers on this SDK? I can help where possible.

@mtrezza
Copy link
Copy Markdown
Member

mtrezza commented May 13, 2026

@chadpav it can take some time until your PR gets reviewed; in the meantime you can test this change out and make sure there are no issues.

@chadpav
Copy link
Copy Markdown
Contributor Author

chadpav commented May 16, 2026

@chadpav it can take some time until your PR gets reviewed; in the meantime you can test this change out and make sure there are no issues.

ok, no problem. I'm running my fork in production. I just wanted to make sure someone was still watching these.

Parse Server mints a fresh session row when password is set on an existing _User and revokeSessionOnPasswordReset is true (default since 9.x). The new sessionToken is embedded in the save response. Without adopting it, the global session in ParseCoreData keeps pointing at the prior session — which the server just destroyed — and every subsequent request fails with invalidSessionToken until the next login.

Mirrors PFUser._mergeFromServerWithResult on iOS, which reads PFUserSessionTokenRESTKey out of the response and installs it.

The helper is a no-op when the response carries no sessionToken (e.g. a plain field update with no password change), preserving the existing behavior for non-auth saves.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 16, 2026
Match iOS PFURLSessionCommandRunner behaviour, which attaches the
install ID header to every outgoing request. Previously, only the
three user-creation methods (signUp, login, loginAnonymous, loginWith)
built the header inline; every other call — including PUT save on an
existing user — went out without it.

Without the install ID on writes, parse-server cannot bind the
_Session row it mints during an authData/password change to the
device's installation, and destroyDuplicatedSessions skips cleanup
of prior sessions on the same install. Empirically this leaves one
orphan _Session row per anonymous-to-password link.

Changes:

* Add `sendInstallationId` flag to `ParseNetworkOptions` (null/true →
  attach, false → suppress) so the existing `doNotSendInstallationID`
  parameter on user methods continues to opt out cleanly.
* Hoist a shared `buildHeaders` helper onto the abstract `ParseClient`
  so both `ParseHTTPClient` and `ParseDioClient` route through one
  implementation. `@protected`; no public API surface change.
* Both clients call `await buildHeaders(options)` from each of get,
  put, post, postBytes, getBytes, and delete.
* `ParseUser.signUp/login/loginAnonymous/_loginWith` drop their
  inline `_getInstallationId()` + conditional header building and
  pass `sendInstallationId: !doNotSendInstallationID` through
  `ParseNetworkOptions` instead. Removes the unused
  `_getInstallationId` helper.

Tests:

* `parse_client_test.dart` (new): 4 unit tests for `buildHeaders` —
  default attaches, sendInstallationId:false suppresses, caller-
  supplied header preserved, custom-header + auto install ID coexist.
* `parse_dio_client_test.dart`: 1 integration test via a custom
  `HttpClientAdapter` that captures outgoing request headers, proves
  `buildHeaders` output reaches dio's wire.
* `parse_http_client_test.dart`: copy-pasted "ParseDioClient" labels
  corrected to "ParseHTTPClient".
CI runs `dart format --set-exit-if-changed` and rejects unformatted
files. Brings the three test files added in the prior two commits
(commits 8324436 and 5b2cd09) into conformance.
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
packages/dart/lib/src/objects/parse_user.dart (1)

46-46: 💤 Low value

Consider using _keyAuthAnonymous constant at line 306 for consistency.

The loginAnonymous method uses the string literal 'anonymous' on line 306, while the new constant _keyAuthAnonymous is defined for this purpose. Using the constant would improve maintainability.

♻️ Suggested fix
         data: jsonEncode(<String, dynamic>{
           'authData': <String, dynamic>{
-            'anonymous': <String, dynamic>{'id': uuid.v4()},
+            _keyAuthAnonymous: <String, dynamic>{'id': uuid.v4()},
           },
         }),

Also applies to: 304-308

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dart/lib/src/objects/parse_user.dart` at line 46, Replace the string
literal 'anonymous' used inside the loginAnonymous method with the existing
constant _keyAuthAnonymous to ensure consistency and maintainability; locate the
loginAnonymous function and update any occurrences (around where the
payload/credential map is built at lines ~304-308) to reference the static const
_keyAuthAnonymous instead of the literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/dart/lib/src/objects/parse_user.dart`:
- Line 46: Replace the string literal 'anonymous' used inside the loginAnonymous
method with the existing constant _keyAuthAnonymous to ensure consistency and
maintainability; locate the loginAnonymous function and update any occurrences
(around where the payload/credential map is built at lines ~304-308) to
reference the static const _keyAuthAnonymous instead of the literal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c216132-3cce-4766-a7e6-74c71830e9d1

📥 Commits

Reviewing files that changed from the base of the PR and between 8324436 and 5b2cd09.

📒 Files selected for processing (8)
  • packages/dart/lib/src/network/options.dart
  • packages/dart/lib/src/network/parse_client.dart
  • packages/dart/lib/src/network/parse_dio_client.dart
  • packages/dart/lib/src/network/parse_http_client.dart
  • packages/dart/lib/src/objects/parse_user.dart
  • packages/dart/test/src/network/parse_client_test.dart
  • packages/dart/test/src/network/parse_dio_client_test.dart
  • packages/dart/test/src/network/parse_http_client_test.dart
✅ Files skipped from review due to trivial changes (1)
  • packages/dart/test/src/network/parse_http_client_test.dart

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 16, 2026
Addresses upstream PR parse-community#1136 review feedback. The loginAnonymous
request body literally wrote 'anonymous' while the rest of the file
(stripAnonymity branches at lines 537/541/543) references the
`_keyAuthAnonymous` constant defined at line 46. One-line change
brings the body into alignment.
@chadpav chadpav changed the title fix: Reconcile local authData on anonymous-to-email conversion feat: Multiple SDK-level fixes for anonymous-to-password conversion (iOS parity) May 16, 2026
chadpav added 2 commits May 16, 2026 18:34
We don't know when parse-server first defaulted
revokeSessionOnPasswordReset to true. "default since 9.x" overclaims
historical knowledge. Reword to "current default in 9.x" — accurate,
no implicit version-history assertion.
Adding the protected buildHeaders helper to ParseClient (commit
5b2cd09) requires regenerating MockParseClient via build_runner.
CI's pub publish --dry-run check rejects dirty generated files;
this commit lands the up-to-date mocks.
@chadpav
Copy link
Copy Markdown
Contributor Author

chadpav commented May 17, 2026

The failing CI build is unrelated to this PR. Do you guys need me to address that?

@mtrezza
Copy link
Copy Markdown
Member

mtrezza commented May 17, 2026

@coderabbitai should the issues addressed here be split into separate PRs, to satisfy the rules in #1136 (comment)?

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh api repos/parse-community/Parse-SDK-Flutter/issues/comments/4335856554 --jq '.body'

Length of output: 1732


@mtrezza Yes — based on the referenced rule ("Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs."), this PR bundles at least three independent concerns that would benefit from being separate PRs:


PR 1 — Core fix (the stated purpose of this PR)

Anonymous-to-email/credential conversion reconciliation

  • _stripAnonymity() and _cleanUpAuthData() helpers in parse_user.dart
  • The username setter wiring and save()/update() cleanup calls
  • parse_user_anonymous_link_test.dart (6 new tests)
  • Expanded loginAnonymous dartdoc

PR 2 — Session token adoption

Adopt server-returned sessionToken after save()/update()

  • _adoptResponseSessionTokenIfChanged() wiring in save() and update()
  • parse_user_session_token_test.dart (3 new tests)

This is a generally useful improvement that was bundled in while touching save()/update(), but it is independent of the anonymous link fix and can be reasoned about on its own.


PR 3 — Network layer: installation ID header control

sendInstallationId option + buildHeaders helper

  • ParseNetworkOptions.sendInstallationId field (options.dart)
  • ParseClient.buildHeaders() (parse_client.dart)
  • Updated ParseDioClient and ParseHTTPClient to use buildHeaders
  • parse_client_test.dart, parse_dio_client_test.dart, parse_http_client_test.dart

This is an entirely separate network-layer concern with no dependency on the anonymous fix.


Splitting this way would keep each PR small and focused, matching the contribution guidelines, and make each change much easier to review and merge independently.

@chadpav
Copy link
Copy Markdown
Contributor Author

chadpav commented May 17, 2026

Per @mtrezza and CodeRabbit's guidance above, this PR has been split into three independent PRs against master:

Each branch is cut from current upstream/master, isolates its respective concern, and includes only the tests for that concern. Full test suite passes on all three.

Closing this PR in favor of the split.

@chadpav chadpav closed this May 17, 2026
@mtrezza
Copy link
Copy Markdown
Member

mtrezza commented May 18, 2026

@chadpav Is there any specific order in which these PRs need to or should get merged?

@chadpav
Copy link
Copy Markdown
Contributor Author

chadpav commented May 18, 2026

@mtrezza No required order — the three PRs are functionally independent. There are small textual conflicts in parse_user.dart, but no semantic dependencies:

Merge in whatever order is convenient. I'll rebase the remaining PRs immediately after each merge so the next one lands conflict-free. If you'd like to minimize rebase touchpoints, #1139#1140#1138 keeps #1138 as the last to land, since it's the only one that conflicts with either of the others.

@chadpav chadpav self-assigned this May 19, 2026
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