Skip to content

feat: [SDK-5627] teach the setup skill composite login, gated on 5.11.0 - #85

Open
abdulraqeeb33 wants to merge 1 commit into
mainfrom
ar/sdk-5627
Open

abdulraqeeb33 wants to merge 1 commit into
mainfrom
ar/sdk-5627

Conversation

@abdulraqeeb33

@abdulraqeeb33 abdulraqeeb33 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

One-Line Summary

The setup skill now writes a composite login(externalId, profile) wrapper when the resolved SDK is 5.11.0 or newer, and keeps the existing wrapper below that.

Ticket: SDK-5627

Details

Android 5.11.0 added login(externalId, profile), which sends email, SMS, tags, and aliases in one Create User upsert and returns OneSignalResult<LoginData>. The generated wrapper used login(externalId) plus addEmail/addSms, so an integration the skill produced had no way to set an email or SMS subscription status and discarded the login outcome.

android.md now picks the wrapper from the version Step 4 resolved, and OneSignalManagerComposite.kt.tmpl is the 5.11.0+ asset. api-reference.md gains the composite signature, the setEmailEnabled/setSmsEnabled semantics, and the failure codes.

Notes

  • Why version-gated instead of just switching. The resolver defaults to --track stable, which the feed currently puts at 5.9.2. A wrapper calling composite login against that pin does not compile. Gating leaves the default pin alone; moving the default track is a release-policy decision, not a docs one.
  • New template rather than editing the existing one, so the pre-5.11.0 path keeps an asset that is still verified to compile. Both rows share Application.kt.tmpl, since the composite wrapper's initialize() stays non-suspend.
  • Scoped to match the live docs. I checked every Android page on documentation.onesignal.com. OneSignalUserProfile and setEmailEnabled are published on the Mobile SDK reference and the Identity Verification page. initWithContextSuspend, getUserSuspend, and getNotificationsSuspend appear nowhere, so this PR says nothing about them. The skill should not teach an API set a customer cannot look up.
  • Correction carried in this PR. I first wrote that the composite wrapper needs kotlinx-coroutines on the compile classpath. It does not: a suspend fun is backed by kotlin.coroutines.Continuation in kotlin-stdlib. Left in, it would have had the agent add an unrequested dependency, which safety contract §7 forbids. The template and skill now say so explicitly, because the verification helper genuinely does need that dependency and the distinction is easy to lose.

Testing

Compiled in the Android SDK demo app. The version gate is only worth having if it is load-bearing, so it was checked in the failing direction too:

Scenario Session ID Verdict
Composite wrapper + shared Application template, published 5.11.0 local, :app:compileGmsDebugKotlin -PSDK_VERSION=5.11.0 Pass, 3 classes emitted
Composite wrapper against stable 5.9.2 local, -PSDK_VERSION=5.9.2 Fails as intended: unresolved OneSignalUserProfile, LoginData, OneSignalResult, setEmailEnabled, setSmsEnabled
Composite wrapper with zero coroutines jars on the classpath local kotlinc against the 5.11.0 AARs + android.jar Pass, which is what disproved the coroutines claim above

No eval sessions run. The gate is a decision the skill text makes, and the compile results above are the evidence it is the right one.

Affected areas

  • skills/setup/SKILL.md
  • skills/setup/assets/
  • skills/credentials/SKILL.md
  • skills/verify/SKILL.md
  • scripts/
  • references/

Also skills/setup/android.md, which the list above does not name.

The Android wrapper the skill generates used login(externalId) plus
addEmail/addSms, so the generated integration never got the one-request
login or any way to set an email/SMS subscription status.

Composite login(externalId, profile) only exists on 5.11.0+, and the
resolver defaults to --track stable, which is 5.9.2. So this selects the
wrapper by the version Step 4 resolved rather than switching the default
track, which is a release-policy call. Added a second template for the
composite path so the pre-5.11.0 path keeps a verified asset.

Verified both directions by compiling the new template in the demo app:
clean against published 5.11.0, and failing on 5.9.2 with unresolved
OneSignalUserProfile / LoginData / setEmailEnabled, which is what the
gate is for.

Also corrected a claim I had written into the template and the skill:
the wrapper does NOT need kotlinx-coroutines. A suspend fun is backed by
kotlin.coroutines.Continuation in kotlin-stdlib. Confirmed by compiling
with zero coroutines jars on the classpath. Only the verification
helper, which calls launch, needs that dependency.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abdulraqeeb33
abdulraqeeb33 requested a review from a team October 29, 2026 16:02
@abdulraqeeb33 abdulraqeeb33 changed the title Teach the setup skill composite login, gated on 5.11.0 Teach the setup skill suspend init and composite login, gated on 5.11.0 Oct 29, 2026
@abdulraqeeb33 abdulraqeeb33 changed the title Teach the setup skill suspend init and composite login, gated on 5.11.0 Teach the setup skill composite login, gated on 5.11.0 Oct 29, 2026
@abdulraqeeb33 abdulraqeeb33 changed the title Teach the setup skill composite login, gated on 5.11.0 feat: [SDK-5627] teach the setup skill composite login, gated on 5.11.0 Oct 29, 2026

@jfishman1 jfishman1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. The version gate is the right call and I like that the reasoning is in the commit message: composite login doesn’t exist before 5.11.0, stable is still 5.9.2, and rather than quietly flipping the default track to get the newer API you gated on whatever the resolver actually returned and left the track choice to the user. That’s the correct boundary — the alternative would have had the plugin silently moving people onto a non-stable pin.

The verification being real rather than asserted matters too. Compiling both directions — clean on 5.11.0, failing on 5.9.2 with unresolved OneSignalUserProfile/LoginData/setEmailEnabled — is exactly the failure the gate exists to prevent, and you confirmed it actually happens.

Special credit for the coroutines correction. Catching that the wrapper never needed kotlinx-coroutines (a suspend fun is backed by kotlin.coroutines.Continuation in kotlin-stdlib), verifying it by compiling with zero coroutines jars, and still keeping the nuance that the verification helper does need it because it calls launch — that’s a precise fix rather than a blanket reversal, and it removes an unrequested dependency bump the agent would otherwise have made.

The three-way nullability on setEmailEnabled/setSmsEnabled is handled properly, including the warning that guessing true re-subscribes someone who opted out. That’s the whole reason composite login matters for email/SMS and it’d be easy to flatten into a boolean and get wrong.

Two non-blocking notes inline.

Separate heads-up, not about this PR: I have a branch coming that fixes the google-services.json guidance in platform-matrix.md, android.md, the credentials skill, and verify_integration.py. The plugin currently calls our docs’ Firebase guidance “a known bug” — that text came from the July vendor baseline and predates 5.11.0. Your PR doesn’t touch those lines, so there shouldn’t be a conflict, but you’ll see them move.

val result = OneSignal.login(externalId, profile)
if (!result.isSuccess) {
// The device is on the new external ID regardless, so report and move on.
Log.w(TAG, "login failed: ${result.error?.first?.code} ${result.error?.first?.message}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: this drops everything after the first error. If the backend returns more than one — say a malformed E.164 and a rejected alias label — the generated app logs one and silently discards the rest, which is the worst case for someone debugging their own integration. A joinToString over result.error would cost nothing. Not blocking.

## SDK data surface (verified signatures — the codegen targets)

- `OneSignal.login(externalId[, jwtToken])` / `logout()` — sets/clears external_id. **Call login BEFORE tags/email/sms** or data strands on the anonymous user.
- `OneSignal.login(externalId, profile[, jwtToken])`: **Android native ≥ 5.11.0 only** (not on iOS or any wrapper SDK yet). Kotlin `suspend fun` returning `OneSignalResult<LoginData>`; from Java pass `Continue.with` (needs API 24+, callback is off the main thread). Sends email, SMS, tags, and aliases inside the login request, so the ordering rule above does not apply to anything in the profile. Build with `OneSignalUserProfile.builder()`: `setEmail`, `setSms` (**E.164 with leading `+`**), `setEmailEnabled`, `setSmsEnabled`, `setTags`, `setAliases`, `build()`. Reserved alias labels (`external_id`, `onesignal_id`) are skipped with a warning, not an error. Prefer this over `login` + `addEmail`/`addSms` whenever the version allows and the contact details are known at sign-in.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

“Android native ≥ 5.11.0 only (not on iOS or any wrapper SDK yet)” is right today, but it’s a fact with a short shelf life sitting in the file the skill treats as the verified source of truth. Worth a note on where this gets updated when iOS or a wrapper ships it — otherwise the agent will keep telling people it’s Android-only after it isn’t.

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