feat: [SDK-5088] add device gesture that marks the user as a test user - #1730
Draft
nan-li wants to merge 2 commits into
Draft
feat: [SDK-5088] add device gesture that marks the user as a test user#1730nan-li wants to merge 2 commits into
nan-li wants to merge 2 commits into
Conversation
…ption ID to the pasteboard Backgrounding and foregrounding the app 6 times within 30 seconds copies os:<push subscription id> to the general pasteboard with a 5 minute expiry, ready to paste into the dashboard. A cycle is a didEnterBackground / didBecomeActive pair observed app-level (never per-scene, matching OSFeatureFlagsRefreshService), counted on a monotonic clock with dwell bounds (background 250ms..2.5s, foreground between counted cycles at most 2.5s) so ordinary app switching cannot complete the pattern. The detector starts alongside StartFeatureFlagsRefresh, including the protected-data recovery path, and resets in clearStatics. It skips when the SDK is not ready (appId, consent, storage) or the subscription does not exist yet, and adding sdk_test_device_gesture_disabled to an app's enabled feature keys turns it off remotely. The raw OSFeatureFlagsStore list is checked instead of OSFeatureManager because the KMP catalog hides unregistered keys.
…he pasteboard The gesture now sends an Update User request setting test_user_name to the device-local time, so the dashboard can surface the device without any copy-paste step. The request reuses OSRequestUpdateProperties and executes directly through the client, bypassing the operation repo; a queued operation can replay hours later, and a stale raise-hand signal is worse than a failed one. The detector in OneSignalOSCore cannot see the user layer, so OneSignal.m installs the sending handler at start. Manual testing showed the dwell maximums made the gesture nearly impossible to perform, one slow icon tap silently reset all progress, so the 30s sliding window is now the only rate rule. The 250ms background floor stays as the rotation filter, and each counted cycle logs at verbose so manual testing can watch progress. Also renames OSTestDeviceGestureDetector to OSDeviceGestureDetector so the class does not read as a test class, and renames the remote kill-switch key to sdk_device_gesture_disabled to match; nothing on the backend serves the old key yet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
One Line Summary
Six background/foreground cycles within 30 seconds mark the current user as a test user.
Details
Motivation
Finding your own device in the dashboard is the slowest part of sending a first test push. Today that means pulling a subscription ID out of verbose logs, or searching by external ID. This adds a gesture anyone can perform on a production build, with no app changes and no new permissions. Background and foreground the app six times within 30 seconds and the SDK sets a
test_user_nameproperty on the user, which the dashboard can then surface.Scope
OSDeviceGestureDetectorlives inOneSignalOSCoreand observes app-leveldidEnterBackgroundNotificationanddidBecomeActiveNotification, never the per-scene versions, which would over-count on multi-window iPad. Pairing the two is itself a filter, since Control Center pulls and Face ID sheets firewillResignActivewithout ever backgrounding the app. A cycle also needs a background phase of at least 250ms, matching Android where rotation emits a synthetic sub-millisecond pair. The 30 second sliding window is the only rate rule.OSCore cannot reach the user layer, so
OneSignal.minstalls a handler at start that callsmarkCurrentUserAsTestUser()onOneSignalUserManagerImpl. That method reusesOSRequestUpdatePropertiesand executes it directly throughOneSignalCoreImpl.sharedClient(), bypassing the operation repo because a queued operation can replay hours later and a stale mark is worse than one that failed outright. Reusing the existing request class means it inherits the subscription-id header and theprepareForExecutionreadiness guard, and it picks up the JWT header for free onceaddJWTHeaderstops being a no-op under identity verification.The detector starts alongside
StartFeatureFlagsRefresh(), including the protected-data recovery path, and resets inclearStatics.Nothing is sent when the SDK is not ready (no appId, consent withheld, storage still locked), when the user has no backend ID yet, or when
sdk_device_gesture_disabledis in the app's enabled feature keys. That key is an inverted kill switch, so absent means enabled and a device that has never fetched flags still has a working gesture. It reads the rawOSFeatureFlagsStorelist rather thanOSFeatureManager, because the manager only resolves keys the KMP catalog registers and this key is deliberately not in the catalog.markCurrentUserAsTestUser()is@objc publicbecauseOneSignal.mhas to call it from Objective-C, which rules out@_spi. It is internal plumbing rather than documented public API, and it is not exposed through theOneSignal.Usernamespace.Other
Two commits are on this branch. The first writes the subscription ID to the pasteboard, the second replaces that with the Update User request. The net diff is the request version alone. The first commit was already pushed when the approach changed, which is why the history reads that way.
The backend does not accept
test_user_nameyet and the dashboard does not surface test users yet, both tracked separately. Until that lands this ships inert, which is why it is a draft.Testing
Unit testing
OSDeviceGestureDetectorTestsdrives synthetic notification sequences against an injectedNotificationCenter, a fake monotonic clock, and a handler that counts invocations. Fourteen tests cover the counting rules, including six cycles firing exactly once, five firing nothing, the detector re-arming afterward, sub-250ms blips not counting, cycles spread wider than the window never accumulating six, a long pause mid-gesture keeping earlier progress, repeated activations with no intervening background not counting, and cold launch not counting. The rest cover the skip conditions, idempotent registration, and observer teardown.Manual testing
Exercised by hand in the demo app on the iOS simulator. The first version enforced maximum dwell times on both phases and cleared all progress on any miss, which made the gesture nearly impossible to perform, so the timing loosened to the single window rule now in the branch. Verbose logging of each counted cycle was added at the same time, since silent resets were the reason the failures were hard to diagnose.
Affected code checklist
Checklist
Overview
Testing
Final pass
🤖 Generated with Claude Code