Skip to content

chore: [SDK-4220] add Live Activities support to demo app#170

Merged
fadi-george merged 11 commits into
mainfrom
fadi/sdk-4220-dotnet-sdk-live-activities
Apr 7, 2026
Merged

chore: [SDK-4220] add Live Activities support to demo app#170
fadi-george merged 11 commits into
mainfrom
fadi/sdk-4220-dotnet-sdk-live-activities

Conversation

@fadi-george

@fadi-george fadi-george commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Add Live Activities section to the DotNet MAUI demo app with SDK integration, REST API controls, and a pre-built widget extension.

Screenshot 2026-04-06 at 4 41 00 PM Screenshot 2026-04-06 at 4 20 07 PM Screenshot 2026-04-06 at 4 35 06 PM Screenshot 2026-04-06 at 4 35 16 PM

Details

Motivation

The DotNet SDK demo app was missing Live Activities support. Other platform demos (React Native, Flutter) already have this feature. This PR brings parity by adding a full Live Activities section with start/update/end controls.

Scope

  • SDK deprecation: Exit (exitLiveActivity) marked as [Obsolete] across the interface and both platform implementations
  • Demo app Live Activities section: New UI section with Activity ID and Order # inputs, plus START/UPDATE/END buttons
  • REST API integration: Update and End operations use the OneSignal REST API via OneSignalApiService
  • Widget extension: Pre-built OneSignalWidgetExtension.appex bundled in Platforms/iOS/PlugIns/ (Swift source in OneSignalWidget/ for reference)
  • Environment-based API key: .env file support via DotEnv helper (reads bundled .env as a MauiAsset)
  • Button disabled states: Added VisualStateManager disabled styles to PrimaryButtonStyle and OutlineButtonStyle
  • Minor UI renames: "Next Activity" → "Next Screen", "Secondary Activity" → "Secondary Screen"

Files changed

  • ILiveActivities.cs, iOSLiveActivitiesManager.cs, AndroidLiveActivitiesManager.cs — deprecate Exit method
  • LiveActivitiesSection.xaml/.cs — new UI section
  • AppViewModel.cs — Live Activity state and commands
  • OneSignalRepository.csStartDefaultLiveActivity, UpdateLiveActivityAsync, HasApiKey
  • OneSignalApiService.csUpdateLiveActivityAsync, DotEnv-based API key
  • DotEnv.cs — reads .env from app bundle
  • MauiProgram.csSetupDefault call, DotEnv.Load()
  • demo.csproj — bundles .env and widget extension .appex
  • Styles.xaml / Colors.xaml — disabled button visual states
  • OneSignalWidget/ — Swift widget source files (reference)
  • Platforms/iOS/PlugIns/OneSignalWidgetExtension.appex — pre-built widget

Testing

Manual testing

  • Tested on iOS 26.2 simulator (arm64)
  • Verified START creates a Live Activity and registers tokens with OneSignal (confirmed via SDK logs)
  • Verified UPDATE/END buttons are disabled when no API key is configured
  • Verified .env file is read correctly and API key is available
  • Verified button disabled styles show gray appearance
  • Verified app launches correctly both with and without .env file

Affected code checklist

  • Notifications
  • 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

@fadi-george fadi-george requested a review from a team as a code owner April 6, 2026 23:36

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

This PR includes a public API change (deprecating ILiveActivities.Exit), deletion of 5 GitHub Actions workflows, and a pre-built binary bundle — these warrant a human look beyond the demo-app scope.

Extended reasoning...

Overview

This PR adds Live Activities support to the DotNet MAUI demo app across 29 files. Most changes are demo-app-only (new UI section, DotEnv helper, REST API service, widget extension, button styles), but a meaningful subset touches the SDK public interface and repository infrastructure.

Security risks

No significant security risks. The DotEnv service reads a gitignored .env file from the app bundle; the API key never enters source control. REST calls use HTTPS. The bundled .appex binary should be verified to match the Swift source in OneSignalWidget/, but that is a trust/supply-chain concern rather than a runtime security issue.

Level of scrutiny

  • Public API change: ILiveActivities.Exit is marked [Obsolete] in the interface and both platform implementations. This is a breaking-change signal to SDK consumers and deserves human sign-off.
  • CI/CD deletions: Five GitHub Actions workflows (asana-add-comment, asana-create-task, asana-update-issue, claude-code-review, claude) are removed entirely. Intentional housekeeping or accidental removal should be confirmed.
  • Pre-built binary: OneSignalWidgetExtension.appex is committed and bundled — humans should verify it matches the reference Swift source and is the correct build artifact.

Other factors

The three reported bugs are all nits (dotenv quote-stripping, dead-code overwrite, blocking async + narrow exception catch) and do not block correctness for users following the documented .env.example format. However, the combination of public API surface changes, infrastructure deletions, and a committed binary makes this PR too broad for automated approval alone.

Comment thread examples/demo/Services/DotEnv.cs
Comment thread examples/demo/Services/OneSignalApiService.cs Outdated
Comment thread examples/demo/Services/DotEnv.cs Outdated
@fadi-george fadi-george changed the title feat: [SDK-4220] add Live Activities support to demo app chore: [SDK-4220] add Live Activities support to demo app Apr 6, 2026
- Strip surrounding quotes from .env values (standard dotenv convention)
- Move _loaded flag to finally block so exceptions don't leave DotEnv
  permanently broken; broaden catch to Exception
- Restructure UpdateLiveActivityAsync as if/else-if to eliminate dead
  code when eventType is "end"

Made-with: Cursor
@fadi-george fadi-george force-pushed the fadi/sdk-4220-dotnet-sdk-live-activities branch from 5f481ba to 943d12f Compare April 7, 2026 00:12
@fadi-george fadi-george force-pushed the fadi/sdk-4220-dotnet-sdk-live-activities branch from 943d12f to c48cf0e Compare April 7, 2026 00:20
@fadi-george

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Apr 7, 2026

Copy link
Copy Markdown

Claude finished @fadi-george's task in 1m 19s —— View job


Code Review

  • Gather context and read changed files
  • Review SDK public API changes
  • Review demo app services (DotEnv, OneSignalApiService)
  • Review demo app UI and ViewModels
  • Review infrastructure changes (workflows, binary)
  • Post final review

Overall this is a well-structured PR that successfully adds Live Activities to the demo app. The implementation is clean and follows existing patterns. There are a few items that need attention before merging.

Items Requiring Human Sign-Off

1. Workflow deletions — Five GitHub Actions workflows are deleted (asana-add-comment, asana-create-task, asana-update-issue, claude-code-review, claude). The PR description doesn't mention this. Were these intentionally removed as housekeeping, or is this accidental? Worth a quick confirmation.

2. Pre-built binaryPlatforms/iOS/PlugIns/OneSignalWidgetExtension.appex/OneSignalWidgetExtension is a compiled binary committed to source control. The Swift reference source is included in OneSignalWidget/, which is good for auditability, but reviewers should verify the binary matches the source and was built from this repo. Consider adding a note in the PR or a README for how to rebuild it.


Issues Found

3. DotEnv.Load() robustness (examples/demo/Services/DotEnv.cs:8-53)

The current implementation has two problems that were partially addressed since the previous review comments, but one remains:

  • Quote stripping is now implemented (lines 33-40) — the previous nit has been fixed.
  • _loaded = true is now in finally — exception safety is handled correctly.
  • ⚠️ GetAwaiter().GetResult() on the main threadFileSystem.OpenAppPackageFileAsync is called synchronously via .GetAwaiter().GetResult() from MauiProgram.CreateMauiApp(), which runs on the main thread. This is the async-over-sync deadlock anti-pattern. While it appears to work in practice on the tested iOS simulator, it's fragile across MAUI versions and platforms. The safer fix is to use a synchronous file API or read the bundled asset differently. Fix this →

4. Dead code in UpdateLiveActivityAsync (examples/demo/Services/OneSignalApiService.cs:152-167)

The if/else if structure is correct — the previous review comment about two sequential if blocks overwriting each other has been fixed. The code looks correct as written.

5. IsLiveActivityUpdating guards UPDATE/END but not START (examples/demo/ViewModels/AppViewModel.cs:500, 537)

UpdateLiveActivityAsync and EndLiveActivityAsync both check IsLiveActivityUpdating to prevent concurrent calls. But StartLiveActivity (line 473) has no such guard. If the user taps START while an update/end is in flight, there's no protection. For a demo app this is minor, but it's an inconsistency.

6. START button has no enabled/disabled binding to ActivityIdEntry (examples/demo/Controls/Sections/LiveActivitiesSection.xaml:73-79)

The START button starts enabled in XAML regardless of whether ActivityIdEntry has text. The OnActivityIdChanged handler does disable it correctly when text is cleared, but on initial render with a pre-populated default value ("order-1"), this is fine. However, if the entry is cleared and then the user rotates/reloads the view, the button could reappear enabled. Minor edge case.

7. HttpClient instantiated per-call (examples/demo/Services/OneSignalApiService.cs:87, 117, 142)

new HttpClient() is created inside SendAsync, FetchUserAsync, and UpdateLiveActivityAsync. This is a known anti-pattern in .NET that can exhaust socket connections under load. For a demo app with low request volume this is fine in practice, but worth noting for anyone using this as a reference implementation.


Nits

  • AppViewModel.UpdateLiveActivityAsync and EndLiveActivityAsync are public methods called only from LiveActivitiesSection.xaml.cs via await. StartLiveActivity is synchronous/void. The asymmetry is intentional (START is SDK-only, no async), which is fine — just noting it's consistent with the SDK's StartDefault being void.
  • LiveActivitiesSection.Initialize() subscribes to viewModel.PropertyChanged with a lambda but never unsubscribes. Given the view and viewmodel have the same lifetime in this app, this is fine.
  • The OrderStatuses array hardcodes three states (preparing → on_the_way → delivered) that cycle back to 0. The UPDATE button text correctly reflects the next state. This is a nice UX touch.

Summary

The core implementation is solid. The only actionable fix before merge is the async-over-sync call in DotEnv.Load(). The workflow deletions and committed binary both need a human reviewer to confirm intent. Everything else is demo-quality acceptable.

Comment on lines +36 to +42

public void Initialize(AppViewModel viewModel)
{
_viewModel = viewModel;

var hasApiKey = viewModel.HasApiKey();
UpdateButton.IsEnabled = hasApiKey;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 In LiveActivitiesSection.Initialize(), UpdateButton.IsEnabled and EndButton.IsEnabled are set to hasApiKey without also checking whether a live activity has been started, contradicting the XAML's explicit IsEnabled='False' defaults. With a valid API key in .env, both buttons become enabled at app launch before START is pressed, allowing REST calls to a non-existent activity that silently fail. Fix by also tracking whether StartLiveActivity() has been called before enabling these buttons.

Extended reasoning...

What the bug is and how it manifests

In LiveActivitiesSection.Initialize() (lines 36-42), the code sets UpdateButton.IsEnabled = hasApiKey and EndButton.IsEnabled = hasApiKey. This overrides the XAML's explicit IsEnabled='False' defaults for both buttons. With a valid API key in .env, both UPDATE and END are immediately enabled at app launch before any live activity has been created in the current session.

The specific code path that triggers it

  1. InitializeComponent() runs, loading XAML with IsEnabled='False' on both buttons. The ActivityIdEntry default text 'order-1' triggers OnActivityIdChanged, but since _viewModel is null at that point, _viewModel?.HasApiKey() == true evaluates to false, so the buttons stay disabled.
  2. Initialize(viewModel) is called. HasApiKey() returns true (valid .env key). Both buttons are unconditionally re-enabled.
  3. User sees UPDATE and END enabled before pressing START. Tapping either sends a POST to /apps/{appId}/live_activities/order-1/notifications for an activity that does not exist, which returns an HTTP error. UpdateLiveActivityAsync returns false and the error is only logged — no UI feedback.

Why existing code doesn't prevent it

Initialize() ignores whether StartLiveActivity() has been called. It has no knowledge of whether an activity ID in use was created in this session. The XAML author explicitly set IsEnabled='False' as the initial state, but Initialize() unconditionally overrides this based solely on API key presence.

Addressing the refutation

The refutation argues this is intentional for multi-session use (updating an activity from a previous session). While plausible as a future design goal, the XAML's explicit IsEnabled='False' defaults are a clear signal that the author intended these buttons to start disabled. If multi-session support were the intent, the XAML would not set IsEnabled='False'. Furthermore, the code has no UI affordance (e.g., instructions, placeholder text, or label) hinting that you can use UPDATE/END for a pre-existing activity — making the enabled state confusing rather than helpful.

Impact

With a valid API key, at app launch both buttons appear active. Pressing END or UPDATE sends a REST call for a non-existent activity, silently returns false, and leaves the buttons still enabled. No crash, no data loss, but the button state does not reflect reality.

Step-by-step proof

  1. User has a valid .env file with ONESIGNAL_API_KEY=abc123.
  2. App launches. DotEnv.Load() reads the key. HasApiKey() returns true.
  3. XAML loads with UpdateButton.IsEnabled=False and EndButton.IsEnabled=False.
  4. OnActivityIdChanged fires during InitializeComponent() with _viewModel == null; both buttons stay disabled (null check evaluates to false).
  5. Initialize(viewModel) is called. Lines 40-41: UpdateButton.IsEnabled = true, EndButton.IsEnabled = true.
  6. User sees both buttons enabled without having pressed START.
  7. User taps END. EndLiveActivityAsync calls UpdateLiveActivityAsync("order-1", "end", ...).
  8. HTTP POST to /apps/{appId}/live_activities/order-1/notifications fails (no such activity). LogManager.Instance.E logs the error. Buttons remain enabled.

Fix

Track a _liveActivityStarted boolean set to true in OnStartClicked. In Initialize(), set UpdateButton.IsEnabled = hasApiKey && _liveActivityStarted and EndButton.IsEnabled = hasApiKey && _liveActivityStarted. Update OnActivityIdChanged similarly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The refactoring to RefreshUpdateEndButtons() improved the logic (now also checks for non-empty activity ID and IsLiveActivityUpdating), but the core issue is still present: there is no check for whether StartLiveActivity() has been called.

With a valid API key and the default "order-1" activity ID, RefreshUpdateEndButtons() in Initialize() still evaluates to true at app launch — enabling both UPDATE and END before START is pressed.

The fix suggested in the original comment is still needed: add a private bool _liveActivityStarted; field, set it to true in OnStartClicked, and add && _liveActivityStarted to the canInteract check in RefreshUpdateEndButtons().

@fadi-george

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Apr 7, 2026

Copy link
Copy Markdown

Claude finished @fadi-george's task in 1m 37s —— View job


Code Review

  • Gather context
  • Review SDK public API changes
  • Review DotEnv.cs
  • Review LiveActivitiesSection.xaml.cs (button state issue from previous review)
  • Review OneSignalApiService.cs
  • Review AppViewModel.cs
  • Post final review

Good progress since the last round — three of the five issues from the previous review have been resolved. Two remain.

Fixed Since Last Review ✅

Issue Status
DotEnv quote stripping Fixed — single and double quotes are both stripped correctly
_loaded = true placement Fixed — moved to finally block; exception catch broadened to Exception
Dead code in UpdateLiveActivityAsync Fixed — restructured to if/else if, payload logic is now correct

The async-over-sync call in DotEnv.Load() was improved: the call is now wrapped in Task.Run(() => FileSystem.OpenAppPackageFileAsync(".env")).GetAwaiter().GetResult(). This avoids the SynchronizationContext deadlock that was the original concern. Blocking the main thread during startup is still not ideal, but it's acceptable for a demo app, and this pattern is a standard .NET workaround. Consider this resolved.


Still Outstanding

1. UPDATE/END buttons enabled at launch before START is pressed (examples/demo/Controls/Sections/LiveActivitiesSection.xaml.cs:37-49)

This was flagged in the previous two review rounds and is still present. Here is the exact flow:

  1. XAML loads — UpdateButton.IsEnabled="False", EndButton.IsEnabled="False" (set in XAML)
  2. InitializeComponent() fires OnActivityIdChanged with "order-1" as the text, but _viewModel is null at this point, so _viewModel?.HasApiKey() == true evaluates to false — buttons remain disabled ✓
  3. Initialize(viewModel) is called — RefreshUpdateEndButtons() runs. With a valid API key in .env and the default "order-1" text, canInteract evaluates to true — both buttons become enabled before the user has pressed START

The fix is a one-liner:

// Add field
private bool _liveActivityStarted;

// Set in OnStartClicked
private void OnStartClicked(object? sender, EventArgs e)
{
    _viewModel?.StartLiveActivity();
    _liveActivityStarted = true;  // add this
    RefreshUpdateEndButtons();
}

// Add to canInteract condition
bool canInteract =
    _liveActivityStarted    // add this
    && !string.IsNullOrWhiteSpace(ActivityIdEntry.Text)
    && _viewModel?.HasApiKey() == true
    && _viewModel?.IsLiveActivityUpdating != true;

Fix this →


Items Requiring Human Sign-Off

2. Workflow deletions — Five GitHub Actions workflows are removed (.github/workflows/asana-add-comment.yml, asana-create-task.yml, asana-update-issue.yml, claude-code-review.yml, claude.yml). The PR description notes this as chore: remove GitHub workflow files in the commit history but does not explain why. Intentional housekeeping or accidental? Please confirm.

3. Pre-built binaryPlatforms/iOS/PlugIns/OneSignalWidgetExtension.appex/OneSignalWidgetExtension is a compiled binary committed to the repo. The Swift reference source is in OneSignalWidget/ for auditability, but a reviewer should verify the binary was built from that source on a trusted machine.


Summary

The core implementation is solid and earlier issues are resolved. The one remaining code fix is the _liveActivityStarted guard in RefreshUpdateEndButtons() — straightforward to add. The workflow deletions and committed binary are judgment calls that need a human reviewer to confirm intent before merge.

Comment on lines +61 to +81
private void RefreshUpdateEndButtons()
{
bool canInteract =
!string.IsNullOrWhiteSpace(ActivityIdEntry.Text)
&& _viewModel?.HasApiKey() == true
&& _viewModel?.IsLiveActivityUpdating != true;
UpdateButton.IsEnabled = canInteract;
EndButton.IsEnabled = canInteract;
}

private void OnOrderNumberChanged(object? sender, TextChangedEventArgs e)
{
if (_viewModel != null)
_viewModel.LiveActivityOrderNumber = e.NewTextValue;
}

private void OnStartClicked(object? sender, EventArgs e)
{
_viewModel?.StartLiveActivity();
RefreshUpdateEndButtons();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The START button's enabled state is never updated when IsLiveActivityUpdating becomes true — RefreshUpdateEndButtons() correctly disables UPDATE and END during an in-flight async call, but StartButton.IsEnabled is only managed in OnActivityIdChanged based on text content alone. A user can tap START while an UPDATE or END REST call is in-flight, causing StartDefault() to be called on the SDK concurrently with an active update/end request for the same activityId. Fix by also incorporating IsLiveActivityUpdating into the START button's enabled condition, either inside RefreshUpdateEndButtons() or as an early-return guard in OnStartClicked.

Extended reasoning...

What the bug is and how it manifests

RefreshUpdateEndButtons() (lines 61–68 of LiveActivitiesSection.xaml.cs) computes a canInteract boolean that checks _viewModel?.IsLiveActivityUpdating \!= true before enabling UPDATE and END. The PropertyChanged handler (lines 47–48) calls this method whenever IsLiveActivityUpdating changes, so both those buttons correctly go disabled the moment an async HTTP call starts. However, the same handler never touches StartButton.IsEnabled. START's only enabled/disabled logic lives in OnActivityIdChanged (line 57), which sets it based purely on whether the text field is non-empty — completely independent of the updating state.

The specific code path that triggers it

  1. User presses START → StartLiveActivity() runs, activity is created, RefreshUpdateEndButtons() is called (START remains enabled because text is present).
  2. User presses UPDATE → UpdateLiveActivityAsync() sets IsLiveActivityUpdating = true, fires PropertyChanged, which calls RefreshUpdateEndButtons() — UPDATE and END are disabled, START is untouched and remains enabled.
  3. While the HTTP call is in-flight, user taps START again → OnStartClicked runs with no IsLiveActivityUpdating check, calls _viewModel?.StartLiveActivity(), which also has no such guard in AppViewModel.
  4. _repository.StartDefaultLiveActivity(activityId, ...) calls OneSignal.LiveActivities.StartDefault() for the same activityId that already has an active update in progress.

Why existing code does not prevent it

The IsLiveActivityUpdating guard was added to UpdateLiveActivityAsync and EndLiveActivityAsync in AppViewModel (the if (IsLiveActivityUpdating) return; checks at the top of each method), but no equivalent guard was added to StartLiveActivity(). Likewise, RefreshUpdateEndButtons() was written to gate only UPDATE and END, and no corresponding logic was added to gate START. The XAML also does not bind StartButton.IsEnabled to any view-model property.

Impact

If the user taps START during an in-flight update or end call, StartDefault() is invoked on the SDK for an activityId that is mid-update. The SDK may attempt to register a new activity token while simultaneously processing an update token, creating a race condition in the SDK's token management. In practice this is an edge-case timing window in a demo app, and the failure mode is non-fatal, but it is an inconsistency relative to the clearly intentional guards on UPDATE and END — hence nit severity.

Step-by-step proof

  1. ActivityIdEntry.Text is "order-1" (non-empty). StartButton.IsEnabled = true.
  2. User taps START → OnStartClickedStartLiveActivity()StartDefault("order-1", ...). Activity is live.
  3. User taps UPDATE → OnUpdateClickedawait _viewModel.UpdateLiveActivityAsync() → sets IsLiveActivityUpdating = truePropertyChanged fires → RefreshUpdateEndButtons()UpdateButton.IsEnabled = false, EndButton.IsEnabled = false. StartButton.IsEnabled is not changed; it is still true.
  4. While the HTTP POST is awaited (network I/O), user taps START again.
  5. OnStartClicked runs. No IsLiveActivityUpdating check. StartLiveActivity() runs. No IsLiveActivityUpdating check there either.
  6. StartDefault("order-1", ...) is called while the previous update call is still in-flight.

How to fix it

Option A — extend RefreshUpdateEndButtons() to also cover START:

private void RefreshUpdateEndButtons()
{
    bool isUpdating = _viewModel?.IsLiveActivityUpdating == true;
    bool hasText = \!string.IsNullOrWhiteSpace(ActivityIdEntry.Text);
    bool canInteract = hasText && _viewModel?.HasApiKey() == true && \!isUpdating;
    StartButton.IsEnabled = hasText && \!isUpdating;
    UpdateButton.IsEnabled = canInteract;
    EndButton.IsEnabled = canInteract;
}

Option B — add an early-return guard to OnStartClicked:

private void OnStartClicked(object? sender, EventArgs e)
{
    if (_viewModel?.IsLiveActivityUpdating == true) return;
    _viewModel?.StartLiveActivity();
    RefreshUpdateEndButtons();
}

Either option closes the gap consistently with the existing update/end guards.

/// </summary>
/// <param name="activityId">The (app-provided) ID of the activity that is being exited.</param>
/// <returns>Awaitable boolean of whether the operation succeeded or failed</returns>
[Obsolete("Currently unsupported, avoid using this method.")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The [Obsolete] message 'Currently unsupported, avoid using this method.' on ILiveActivities.Exit() is factually inaccurate for iOS: iOSLiveActivitiesManager.Exit() is fully implemented and calls OneSignalNative.LiveActivities.Exit() with success/failure callbacks. A more accurate message would be 'Deprecated: use the REST API to end live activities instead.' since the PR's stated intent is to deprecate the method in favor of the REST API path, not to signal that the method does not work.

Extended reasoning...

What the bug is

The [Obsolete] attribute added to Exit(activityId) on the interface (ILiveActivities.cs:22) uses the message 'Currently unsupported, avoid using this method.' This message is applied uniformly to both platform implementations. On Android, the message is accurate — AndroidLiveActivitiesManager.Exit() simply logs a message and returns false, making it a genuine no-op. On iOS, however, the message is factually wrong.

The specific code that contradicts the message

iOSLiveActivitiesManager.Exit() is a fully operational async method:

[Obsolete("Currently unsupported, avoid using this method.")]
public async Task<bool> Exit(string activityId)
{
    BooleanCallbackProxy proxy = new BooleanCallbackProxy();
    OneSignalNative.LiveActivities.Exit(
        activityId,
        response => proxy.OnResponse(true),
        response => proxy.OnResponse(false)
    );
    return await proxy;
}

It creates a BooleanCallbackProxy, calls the native iOS SDK's LiveActivities.Exit() with success and failure handlers, and awaits the result. This method works correctly on iOS.

Why existing code doesn't prevent the confusion

The [Obsolete] attribute is placed on the interface definition, which is the single source of truth for IDE tooling. When an iOS developer calls Exit() on their ILiveActivitiesManager reference, they will see 'Currently unsupported, avoid using this method.' as a compiler warning. The natural reading of 'Currently unsupported' is 'does not function right now,' not 'being deprecated in favor of another approach.' The word 'Currently' specifically reinforces the interpretation that the method is non-functional at present.

Impact

An iOS developer who reads this warning in their IDE will either: (a) conclude the method doesn't work and stop using it (desired behavior for the wrong reason), or (b) be confused because their calls to Exit() have been working. The PR's actual intent — as stated in the description — is to mark the method as deprecated in favor of the REST API approach, not to indicate the method is broken. The message fails to communicate this intent.

Addressing the refutation

The refutation argues 'unsupported' can reasonably mean 'not supported going forward.' While that interpretation is possible in isolation, the qualifier 'Currently' shifts the phrase toward describing present state rather than future direction. Compare: 'Currently deprecated, prefer the REST API' (intent clear) vs. 'Currently unsupported' (implies it does not function today). Furthermore, the [Obsolete] attribute already signals deprecation by its presence — the message text is an opportunity to explain why, which 'Currently unsupported' fails to do for iOS callers.

Step-by-step proof

  1. iOS developer has code calling OneSignal.LiveActivities.Exit(activityId).
  2. After this PR merges, their IDE shows: warning CS0612: 'ILiveActivitiesManager.Exit(string)' is obsolete: 'Currently unsupported, avoid using this method.'
  3. Developer reads 'Currently unsupported' and concludes Exit() does not work on their platform.
  4. In reality, Exit() calls the native SDK and returns a meaningful success/failure result.
  5. A more accurate message: 'Deprecated: use the REST API to end live activities instead.' correctly tells the developer why to migrate without misleading them about current functionality.

How to fix it

Change the [Obsolete] message on ILiveActivities.cs:22 (and the corresponding platform implementations for consistency) from 'Currently unsupported, avoid using this method.' to something like 'Deprecated: use the REST API to end live activities instead. This method may be removed in a future release.'

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.

The backend api no longer supports this behavior for iOS or wrappers.

@fadi-george fadi-george merged commit 2b17d4c into main Apr 7, 2026
6 checks passed
@fadi-george fadi-george deleted the fadi/sdk-4220-dotnet-sdk-live-activities branch April 7, 2026 16:09
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