Skip to content

feat(iv): add externalId and requiresJwt to Operation base class (2/6)#2624

Open
nan-li wants to merge 2 commits intofeat/iv-foundation-01from
feat/iv-oprepo-gating-02
Open

feat(iv): add externalId and requiresJwt to Operation base class (2/6)#2624
nan-li wants to merge 2 commits intofeat/iv-foundation-01from
feat/iv-oprepo-gating-02

Conversation

@nan-li
Copy link
Copy Markdown
Contributor

@nan-li nan-li commented Apr 24, 2026

Description

One Line Summary

PR 2 of 6 against the Identity Verification integration branch — mechanical refactor that adds externalId: String? and requiresJwt: Boolean to the Operation base class so the next PR can implement queue-level IV gating. No runtime behavior change.

Details

Motivation

The next PR in the series (PR 3/6, "queue-level IV") needs to read op.externalId polymorphically from OperationRepo. Today externalId is declared on LoginUserOperation and TrackCustomEventOperation only — OperationRepo can't read it from a generic Operation reference without casting to each of the 16 subclass types. This PR hoists the property (and a companion requiresJwt flag) to the base class so downstream consumers can read them uniformly.

Kept deliberately narrow so the base-class refactor has its own reviewable surface; the behavior changes (pre-HYDRATE deferral, FAIL_UNAUTHORIZED handling, IdentityVerificationService with anon-op purge) come in the next PR.

Scope

Operation base class:

  • New var externalId: String? — captures the externalId of the user the op was enqueued for. Nullable because anonymous users have no externalId; base-class declaration keeps the type String? uniformly across all subclasses.
  • New open val requiresJwt: Boolean get() = true — declares whether an op's backend endpoint needs a JWT when IV is active. Subclasses may override to false for endpoints that don't require auth (none do yet — default true covers every current op).

Operation subclasses (16):

  • All 16 constructors gain externalId: String? as the 3rd parameter (after appId and onesignalId).
  • LoginUserOperation and TrackCustomEventOperation shed their local externalId property in favor of the inherited base-class property (previously defined on each subclass independently).
  • TransferSubscriptionOperation is the one exception in parameter position — its constructor is (appId, subscriptionId, onesignalId, externalId) to match the existing layout where onesignalId comes after subscriptionId.

Call sites updated to pass externalId at construction time (10 files):

  • IdentityModelStoreListener, PropertiesModelStoreListener, SubscriptionModelStoreListener — read _identityModelStore.model.externalId.
  • SessionListener (session start + end).
  • TrackGooglePurchase (purchase tracking).
  • UserSwitcher (legacy player ID migration).
  • RebuildUserService (reconstructs queued ops from cached models).
  • UserRefreshService (foreground user refresh).
  • LoginUserOperationExecutor, LoginUserFromSubscriptionOperationExecutor, SubscriptionOperationExecutor (ops they emit as side effects, e.g. post-login RefreshUserOperation, post-subscription-recreate CreateSubscriptionOperation).

PropertiesModelStoreListener gains IdentityModelStore as a constructor dependency — the properties model doesn't carry externalId, so the listener has to read it from the identity model.

Placement decision: base class vs per-subclass

onesignalId is declared on each subclass individually, not on the base. Considered matching that convention for externalId/requiresJwt, but chose base-class placement:

  • externalId is naturally nullable; String? works uniformly across every subclass without weakening any type.
  • Cross-cutting consumers (next PR's hasValidJwtIfRequired, removeOperationsWithoutExternalId) need to read op.externalId polymorphically — that requires at least an abstract declaration on the base. Per-subclass with abstract-on-base would mean ~80 lines of boilerplate (override + getter/setter × 16 subclasses) vs the 5-line base-class declaration.
  • requiresJwt has natural override semantics (open val with default), which is idiomatic on a base class.

name on Operation is precedent for a non-nullable universal property on the base, so there's no technical constraint keeping onesignalId off the base — that placement is a historical style preference that this PR does not touch.

What is NOT in this PR

  • IdentityVerificationService — PR 3/6.
  • OperationRepo IV gating (hasValidJwtIfRequired dispatch, FAIL_UNAUTHORIZED handler, pre-HYDRATE deferral in getNextOps) — PR 3/6.
  • HTTP layer JWT attachment — PR 4/6.
  • Public API (login(externalId, jwt), updateUserJwt, listeners) — PR 5/6.
  • Logout + IAM integration — PR 6/6.

Testing

Unit testing

  • Compile verified — every call site updated to pass externalId (see compile errors list narrowed from 50+ to 0).
  • All existing test fixtures updated to the new constructor signature. Most were mechanical substitutions (add null as 3rd arg for anon/un-scoped tests; add the relevant externalId where the test exercises it).
  • 791 core tests run, 789 pass, 2 fail — both are pre-existing SDKInitTests failures on the integration branch (unrelated: one is about an error-message assertion, the other about init not blocking). Same two failures exist on feat/iv-foundation-01 without this PR.

Manual testing

Not applicable — this PR adds a property + parameter with no runtime behavior change. Every call site still passes identically-shaped data; the only difference is that externalId is now available for polymorphic reads in the next PR.

Affected code checklist

  • Notifications
  • Outcomes
  • Sessions (session listener passes externalId to track-session ops)
  • In-App Messaging
  • REST API requests (only op shape; no new network traffic)
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing — adds two properties to Operation and updates every subclass + call site
  • Any Public API changes are explained in the PR details and conform to existing APIs (no public API changes)

Testing

  • I have included test coverage for these changes (existing tests cover via construction; no new behavior to test in this PR)
  • All automated tests pass locally (pre-existing SDKInitTests failures are unrelated — see Manual testing)
  • Manual testing not applicable — no runtime behavior change

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

@nan-li nan-li changed the title feat(iv): add externalId and requiresJwt to Operation base class feat(iv): add externalId and requiresJwt to Operation base class (2/6) Apr 24, 2026
@nan-li nan-li force-pushed the feat/iv-oprepo-gating-02 branch from 0caf0f3 to aec3996 Compare April 24, 2026 19:55
@nan-li
Copy link
Copy Markdown
Contributor Author

nan-li commented Apr 24, 2026

@claude review

@nan-li nan-li force-pushed the feat/iv-foundation-01 branch 3 times, most recently from 9a3c903 to 5835891 Compare April 27, 2026 23:06
nan-li added a commit that referenced this pull request Apr 27, 2026
…criptionId KDoc

Two PR review nits on #2624:

- Operation.externalId.set was implicitly public after the hoist from
  LoginUserOperation/TrackCustomEventOperation (both of which had
  `private set` before). Make it `internal set`, matching the precedent
  on LoginUserOperation.existingOnesignalId. Preserves the tripwire
  against accidental post-construction mutation that the new KDoc
  ("captured at construction time") promises.

- LoginUserFromSubscriptionOperation.subscriptionId had a copy-pasted
  "optional external ID..." KDoc that became actively misleading once
  externalId was hoisted to the base class — readers now see an
  external-ID description above subscriptionId with the real externalId
  silently inherited. Replace with an accurate description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nan-li nan-li force-pushed the feat/iv-oprepo-gating-02 branch from aec3996 to 3c2e5f9 Compare April 27, 2026 23:09
nan-li added a commit that referenced this pull request Apr 27, 2026
…criptionId KDoc

Two PR review nits on #2624:

- Operation.externalId.set was implicitly public after the hoist from
  LoginUserOperation/TrackCustomEventOperation (both of which had
  `private set` before). Make it `internal set`, matching the precedent
  on LoginUserOperation.existingOnesignalId. Preserves the tripwire
  against accidental post-construction mutation that the new KDoc
  ("captured at construction time") promises.

- LoginUserFromSubscriptionOperation.subscriptionId had a copy-pasted
  "optional external ID..." KDoc that became actively misleading once
  externalId was hoisted to the base class — readers now see an
  external-ID description above subscriptionId with the real externalId
  silently inherited. Replace with an accurate description.
@nan-li nan-li force-pushed the feat/iv-oprepo-gating-02 branch from 3c2e5f9 to e5a77f9 Compare April 27, 2026 23:13
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Both prior nits addressed (tightened setter visibility and fixed the subscriptionId KDoc) — mechanical refactor looks good.

Extended reasoning...

Overview

This is PR 2/6 of the Identity Verification series. It hoists two properties to the Operation base class — externalId: String? and open val requiresJwt: Boolean = true — and threads externalId through the constructors of all 16 Operation subclasses plus their ~10 call sites. LoginUserOperation and TrackCustomEventOperation shed their local externalId declarations in favor of the inherited base-class property. Tests are updated mechanically to the new constructor signatures.

Security risks

None introduced by this PR. The change is a structural refactor; it does not touch HTTP/auth/crypto code paths or change what data is sent to the backend. The new externalId field on operations is sourced from the same IdentityModelStore that already populated it on LoginUserOperation/TrackCustomEventOperation. The IV gating logic that will consume these properties is explicitly deferred to PR 3/6.

Level of scrutiny

Low-to-moderate. The diff is wide (34 files) but each touched site is mechanical — adding an externalId parameter at a fixed position and forwarding _identityModelStore.model.externalId. The two non-mechanical bits (introducing the new base-class property, and adding IdentityModelStore as a dep on PropertiesModelStoreListener) are localized and straightforward. There is no runtime behavior change in this PR.

Other factors

  • Both prior inline comments from my earlier review were resolved by recent commits (internal set on the setter, corrected subscriptionId KDoc).
  • Existing test coverage was preserved — every call site is exercised through compile-time wiring.
  • The PR is part of a 6-PR series, deliberately kept narrow so the base-class hoist has its own reviewable surface before downstream IV behavior lands.

@nan-li nan-li force-pushed the feat/iv-foundation-01 branch 2 times, most recently from fea5a86 to 0fb5092 Compare May 4, 2026 03:26
nan-li added 2 commits May 3, 2026 21:50
PR 2 of 6 — prerequisite refactor for queue-level IV behavior.

`externalId: String?` moves onto [Operation] as a protected property set at
construction time. Captures the user the op was enqueued for so the op stays
bound to that user even if the current user changes before execution.
`requiresJwt: Boolean` (default `true`) declares whether an op's backend
endpoint needs a JWT when Identity Verification is active; subclasses can
override to `false` for endpoints that don't require auth.

Every operation subclass constructor gains an `externalId: String?` parameter
as the third arg (after appId + onesignalId). Every call site that creates
an operation — model-store listeners, session listener, executors that build
result ops, RebuildUserService, UserRefreshService, etc. — now reads
`_identityModelStore.model.externalId` and passes it in. `LoginUserOperation`
and `TrackCustomEventOperation` previously had their own local `externalId`
property; those are removed in favor of the inherited base-class property.

No runtime behavior change in this PR. Queue-level IV work (pre-HYDRATE
deferral, FAIL_UNAUTHORIZED handler, IdentityVerificationService with anon
op purge) lands in PR 2.5.
…criptionId KDoc

Two PR review nits on #2624:

- Operation.externalId.set was implicitly public after the hoist from
  LoginUserOperation/TrackCustomEventOperation (both of which had
  `private set` before). Make it `internal set`, matching the precedent
  on LoginUserOperation.existingOnesignalId. Preserves the tripwire
  against accidental post-construction mutation that the new KDoc
  ("captured at construction time") promises.

- LoginUserFromSubscriptionOperation.subscriptionId had a copy-pasted
  "optional external ID..." KDoc that became actively misleading once
  externalId was hoisted to the base class — readers now see an
  external-ID description above subscriptionId with the real externalId
  silently inherited. Replace with an accurate description.
@nan-li nan-li force-pushed the feat/iv-oprepo-gating-02 branch from e5a77f9 to 8c5e762 Compare May 4, 2026 04:50
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Prior concerns from my last review were addressed in 8c5e762 (internal set on Operation.externalId; corrected subscriptionId KDoc); rest is a mechanical externalId-threading refactor with no runtime behavior change.

Extended reasoning...

Overview

This PR (2 of 6 in the Identity Verification series) hoists two properties onto the Operation base class — externalId: String? and open val requiresJwt: Boolean — and threads externalId through every subclass constructor (16 subclasses) plus every call site (10 producer files: listeners, executors, services, UserSwitcher, RebuildUserService, SessionListener, TrackGooglePurchase, UserRefreshService). PropertiesModelStoreListener gains IdentityModelStore as a constructor dependency to read externalId. Existing test fixtures across 6 test files are updated to the new constructor signature with null substitutions for anonymous cases.

Security risks

None directly introduced by this PR. The new externalId and requiresJwt fields are scaffolding — no consumer reads them yet. The actual IV gating (hasValidJwtIfRequired, FAIL_UNAUTHORIZED handling, pre-HYDRATE deferral, anon-op purge) lands in PR 3/6, and the HTTP JWT attachment in PR 4/6. Setter visibility on the new externalId property is internal set (matching existingOnesignalId precedent in LoginUserOperation.kt:42), preserving the "captured at construction" invariant for same-module subclass constructors.

Level of scrutiny

Low-medium. The change is mechanical and the runtime delta is zero — every call site that constructs an Operation now also passes the externalId from the same identity model that was already accessible to it, and no consumer reads externalId polymorphically yet. The risk surface is essentially "did all 26 producers thread the right externalId value through?" — and the call-site reads are uniformly _identityModelStore.model.externalId (or identityModel.externalId from a captured copy in RebuildUserService), which is the only sensible source. Test fixtures pass null consistently for anonymous cases.

Other factors

  • My two prior review comments (visibility setter, misplaced KDoc) were both addressed in commit 8c5e762 and are marked resolved.
  • Bug hunting system found no issues on this revision.
  • 34-file size is large but every touch is one of: add a parameter, pass through externalId, or update a test fixture. No new control flow, no new branching, no new I/O.
  • This is part of a security-sensitive feature series (Identity Verification), but the security-relevant logic deliberately lives in subsequent PRs, keeping this one a clean reviewable surface for the base-class change.

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