Skip to content

feat: [SDK-5024] add composite login(externalId, profile) as one Create User POST - #2743

Open
abdulraqeeb33 wants to merge 5 commits into
ar/sdk-4988from
ar/sdk-5024
Open

feat: [SDK-5024] add composite login(externalId, profile) as one Create User POST#2743
abdulraqeeb33 wants to merge 5 commits into
ar/sdk-4988from
ar/sdk-5024

Conversation

@abdulraqeeb33

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Add login(externalId, profile) so email, SMS, tags, and aliases go out on a single Create User upsert, and return HTTP 4xx on the waiting OneSignalResult.

Details

Motivation

Identity-only login(externalId) cannot attach profile fields. Composite login needs those fields on one Create User POST (upsert by external_id) so a bad SMS/email cannot freeze the op repo, and the suspend caller gets the backend body instead of a generic "did not complete".

Stacked on #2710 (ar/sdk-4988). Parent: SDK-5023. Types/signature: SDK-5024. One POST: SDK-5025.

Scope

  • New OneSignalUserProfile (email, phoneNumber in E.164, tags, aliases) plus Java Builder. JWT stays a separate argument. externalId stays the login key.
  • Identity-only login / loginSuspend are unchanged.
  • Same externalId with no profile fields is still a no-op. Same externalId with profile fields upserts.
  • Composite 400/409 drop the op (FAIL_NORETRY) instead of pausing the repo. Identity-only 4xx still pause.
  • enqueueAndAwaitResult carries HTTP status/body so composite login returns OneSignalResult.failure(BACKEND_ERROR, message=body, backendCode=HTTP status). Catalog codes are not parsed yet.
  • Demo: LOGIN WITH PROFILE dialog.

Not in this PR: persistence (SDK-5026), telemetry (SDK-5027), a login callback, catalog backendCode.

Testing

Unit testing

Executor tests cover identity-only payload unchanged, profile aliases/email/SMS/tags on Create User, reserved alias skip, 400/409 FAIL_NORETRY with HTTP fields, 401, and hydration. OperationRepo wakes enqueueAndAwaitResult with status/body. LoginHelper, UserBackendService nested tags, and profile builder tests added.

Manual testing

Demo app on a physical device. Composite login with a non-E.164 SMS (4129089471) returns HTTP 400 Invalid token format for device type SMS, logs the body, returns OneSignalResult.failure, and does not pause the op repo. Retry with +14129089471 succeeds.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

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

Made with Cursor

AR Abdul Azeez and others added 2 commits September 9, 2026 14:14
…te User POST

Apply email, SMS, tags, and aliases in a single upsert so identity-only login stays unchanged, and surface HTTP 4xx on the waiting OneSignalResult instead of pausing the op repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@abdulraqeeb33
abdulraqeeb33 requested a review from a team as a code owner September 9, 2026 19:14
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📊 Diff Coverage Report

Diff Coverage Report (Changed Lines Only)

Gate: aggregate coverage on changed executable lines must be ≥ 80% (JaCoCo line data for lines touched in the diff).

Changed Files Coverage

  • IOneSignal.kt: 2/2 touched executable lines (100.0%) (9 touched lines in diff)
  • OneSignal.kt: 0/3 touched executable lines (0.0%) (11 touched lines in diff)
    • 3 uncovered touched lines in this file
  • OneSignalResultData.kt: 8/9 touched executable lines (88.9%) (22 touched lines in diff)
  • OneSignalUserProfile.kt: 18/18 touched executable lines (100.0%) (48 touched lines in diff)
  • IOperationExecutor.kt: 2/2 touched executable lines (100.0%) (2 touched lines in diff)
  • IOperationRepo.kt: 8/8 touched executable lines (100.0%) (15 touched lines in diff)
  • OperationRepo.kt: 14/15 touched executable lines (93.3%) (22 touched lines in diff)
  • OperationRepoIvExtensions.kt: 3/3 touched executable lines (100.0%) (5 touched lines in diff)
  • OperationWaitResultExtensions.kt: 1/1 touched executable lines (100.0%) (7 touched lines in diff)
  • OneSignalImp.kt: 4/18 touched executable lines (22.2%) (37 touched lines in diff)
    • 14 uncovered touched lines in this file
  • LoginHelper.kt: 22/22 touched executable lines (100.0%) (46 touched lines in diff)
  • UserBackendService.kt: 1/1 touched executable lines (100.0%) (3 touched lines in diff)
  • LoginUserOperation.kt: 38/38 touched executable lines (100.0%) (76 touched lines in diff)
  • LoginUserOperationExecutor.kt: 75/78 touched executable lines (96.2%) (125 touched lines in diff)

Overall (aggregate gate)

196/218 touched executable lines covered (89.9% — requires ≥ 80%)

Per-file detail (informational; gate is aggregate above):

  • OneSignal.kt: 0.0% (3 uncovered touched lines)

  • OneSignalImp.kt: 22.2% (14 uncovered touched lines)

📥 View workflow run

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

Multi-model review (Claude Opus 5, GPT 5.6 Sol, Cursor Grok 4.6) on the composite login(externalId, profile) change. Substantive diff is +1,331/−64 (1,333 after excluding generated core.api).

Act on

  1. Composite 400/409 drops the whole grouped batch (3/3). FAIL_NORETRYdropAndWake(ops) removes login and grouped push create/transfer ops. After switchUser has already replaced local identity, a bad email/SMS can strand the user on a local OneSignal ID with no push create. Fail the waiter without dropping followers, or revert the switch.
  2. Same-user composite login can return success before HTTP (3/3). Dedupe by onesignalId wakes a second waiter with OperationWaitResult(true) when the queued op already has a waiter. Combined with first-wins mergeProfileFrom on email/phone, a later login(id, profile) reports success while its address is discarded, and a later 400 never reaches that caller.
  3. Same-user context is captured off the login lock (2/3). contextForCurrentUser and loginDataFromStores read live identity/subscription stores. Concurrent login/logout can pair profile A with user B, or return {externalId: A, onesignalId: B}.

Consider

  • Full profile plus raw HTTP bodies are logged (3/3).
  • Reserved-only aliases still count as profile fields, so same-user login is no longer a no-op and 4xx uses FAIL_NORETRY (2/3).
  • Any profile field skips the anonymous→identified SetAlias merge (Opus).
  • persistProfileSubscription hardcodes SUBSCRIBED/optedIn=true; match-by-type can bind the wrong email/SMS (Opus/Grok).
  • backendCode is the HTTP status, not a catalog code; 404/410 still pauses the repo (Opus).

Noted / dismissed
OneSignalUserProfile as a public data class (copy/componentN API surface), constructor binary breaks on ExecutionResponse/LoginUserOperation, hydrateProfile writing the request not the response, and catalog backendCode parsing — last is explicitly out of scope.

Open in Web View Automation 

Sent by Cursor Automation: PR Reviews

Comment on lines +290 to +294
NetworkUtils.ResponseStatusType.INVALID,
NetworkUtils.ResponseStatusType.CONFLICT,
->
if (createUserOperation.hasProfileFields()) {
backendExecutionResponse(ExecutionResult.FAIL_NORETRY, ex)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act on (3/3): Composite 400/409 returns FAIL_NORETRY, and OperationRepo then dropAndWakes the entire grouped batch — not just this login.

LoginUserOperation, CreateSubscriptionOperation, and TransferSubscriptionOperation share createComparisonKey = "$appId.User.$onesignalId". After a new-user switchUser, the push create is in that group. A malformed SMS/email therefore drops the device subscription create while local identity already shows the new externalId and a still-local onesignalId.

A retry takes the same-user path and POSTs profile-only, so push never reattaches. Identity-only 4xx still pause and keep the ops; this path should fail the waiter without dropping grouped followers, or roll the local switch back.

Comment on lines +227 to +232
existingOp.mergeProfileFrom(op)
// Transfer the waiter so enqueueAndWait callers see the queued op's real execution result.
if (queueItem.waiter != null && existing.waiter == null) {
existing.waiter = queueItem.waiter
} else {
queueItem.waiter?.wake(true)
queueItem.waiter?.wake(OperationWaitResult(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.

Act on (3/3): When a second composite login(id, profile) dedupes onto a queued LoginUserOperation that already has a waiter, this wakes the incoming waiter with success immediately.

Every login path now uses enqueueAndAwaitResult, so the second caller always hits this else. OneSignalImp.login then returns OneSignalResult.success(loginDataFromStores(...)) before createUser runs — often with a local onesignalId and null subscription IDs. If the surviving op later 400s, only the first caller sees failure.

Attach/chain waiters (or wait on the surviving op). Do not wake(true) for a composite login that has not executed.

Comment on lines +102 to 107
internal fun mergeProfileFrom(other: LoginUserOperation) {
if (email.isNullOrEmpty()) email = other.email
if (phoneNumber.isNullOrEmpty()) phoneNumber = other.phoneNumber
if (other.tags.isNotEmpty()) tags = tags + other.tags
if (other.aliases.isNotEmpty()) aliases = aliases + other.aliases
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act on (2/3, bundled with the dedupe success above): Email/phone are first-write-wins; tags/aliases are last-write-wins.

login("alice", profile(email="old@x.com")) then login("alice", profile(email="new@x.com")) while the first is queued sends old@x.com and drops the new address — and the second call already returned success. Make addresses last-write-wins, or do not merge ops whose email/phone differ.

Comment on lines +103 to +109
internal fun contextForCurrentUser(externalId: String): LoginEnqueueContext =
LoginEnqueueContext(
appId = configModel.appId,
newIdentityOneSignalId = identityModelStore.model.onesignalId,
externalId = externalId,
existingOneSignalId = null,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act on (2/3): Same-user composite login snapshots onesignalId here with no loginLogoutLock, after switchUser already released it.

A concurrent logout/login can enqueue LoginUserOperation(onesignalId=otherUser, externalId=original, profile). Combined with loginDataFromStores reading the live identity/subscription stores after await, caller A can get {externalId: A, onesignalId: B} or attach profile fields to the wrong local user.

Capture {appId, onesignalId, externalId} under the same lock, and abort if externalId no longer matches.

…Repo

LoginData lookup lives on LoginHelper, profile emptiness on OneSignalUserProfile, nested properties use JSONUtils.mapToJson, and wait results are an ExecutionResponse extension.

Co-authored-by: Cursor <cursoragent@cursor.com>
)

OutlineButton(
text = "LOGIN WITH PROFILE",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this needs to be added to iOS and all the other wrappers

AR Abdul Azeez and others added 2 commits September 9, 2026 14:43
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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