From 1f152e09c2bc591ffdcc4098a8a432709589800b Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 31 Mar 2026 13:43:08 -0700 Subject: [PATCH 1/6] feat(demo): add Live Activities tooltip content Made-with: Cursor --- demo/tooltip_content.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/demo/tooltip_content.json b/demo/tooltip_content.json index 71b79a0..c840718 100644 --- a/demo/tooltip_content.json +++ b/demo/tooltip_content.json @@ -78,5 +78,27 @@ "description": "Takes over the entire screen. Best for onboarding, promotions, or rich media content." } ] + }, + "liveActivities": { + "title": "Live Activities", + "description": "Display real-time updates on the iOS Lock Screen and Dynamic Island. Uses the DefaultLiveActivityAttributes type provided by the OneSignal SDK.", + "options": [ + { + "name": "Start", + "description": "Launch a new Live Activity with an activity ID and initial content using the SDK's startDefault method." + }, + { + "name": "Update", + "description": "Update the Live Activity's content state via the REST API. Cycles through order statuses: preparing → on the way → delivered." + }, + { + "name": "End", + "description": "Terminate the Live Activity via the REST API with immediate dismissal." + }, + { + "name": "Stop Updating", + "description": "Call the SDK's exit method to unsubscribe this device from future updates for the given activity ID." + } + ] } } From 5d464d47fe062bb7ab5d225cc38e375ee840b972 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 31 Mar 2026 13:45:52 -0700 Subject: [PATCH 2/6] refactor(demo): simplify Live Activity tooltip text --- demo/tooltip_content.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/tooltip_content.json b/demo/tooltip_content.json index c840718..f5c1dda 100644 --- a/demo/tooltip_content.json +++ b/demo/tooltip_content.json @@ -89,7 +89,7 @@ }, { "name": "Update", - "description": "Update the Live Activity's content state via the REST API. Cycles through order statuses: preparing → on the way → delivered." + "description": "Update the Live Activity's content state via the REST API." }, { "name": "End", From d46173bc864530a80fefae7596c3dbc9e6502db8 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 31 Mar 2026 15:19:52 -0700 Subject: [PATCH 3/6] feat(demo): add iOS Live Activities support --- demo/LiveActivity.swift | 142 ++++++++++++++++++++++++++++++++++++++++ demo/build.md | 66 +++++++++++++++++-- 2 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 demo/LiveActivity.swift diff --git a/demo/LiveActivity.swift b/demo/LiveActivity.swift new file mode 100644 index 0000000..f4a95f7 --- /dev/null +++ b/demo/LiveActivity.swift @@ -0,0 +1,142 @@ +import ActivityKit +import WidgetKit +import SwiftUI +import OneSignalLiveActivities + +@available(iOS 16.2, *) +struct OneSignalWidgetLiveActivity: Widget { + + private func statusIcon(for status: String) -> String { + switch status { + case "on_the_way": return "box.truck.fill" + case "delivered": return "checkmark.circle.fill" + default: return "bag.fill" + } + } + + private func statusColor(for status: String) -> Color { + switch status { + case "on_the_way": return .blue + case "delivered": return .green + default: return .orange + } + } + + private func statusLabel(for status: String) -> String { + switch status { + case "on_the_way": return "On the Way" + case "delivered": return "Delivered" + default: return "Preparing" + } + } + + var body: some WidgetConfiguration { + ActivityConfiguration(for: DefaultLiveActivityAttributes.self) { context in + let orderNumber = context.attributes.data["orderNumber"]?.asString() ?? "Order" + let status = context.state.data["status"]?.asString() ?? "preparing" + let message = context.state.data["message"]?.asString() ?? "Your order is being prepared" + let eta = context.state.data["estimatedTime"]?.asString() ?? "" + + VStack(spacing: 10) { + HStack { + Text(orderNumber) + .font(.caption) + .foregroundColor(.gray) + Spacer() + if !eta.isEmpty { + Text(eta) + .font(.caption) + .foregroundColor(.white.opacity(0.7)) + } + } + + HStack(spacing: 12) { + Image(systemName: statusIcon(for: status)) + .font(.title2) + .foregroundColor(statusColor(for: status)) + + VStack(alignment: .leading, spacing: 2) { + Text(statusLabel(for: status)) + .font(.headline) + .foregroundColor(.white) + Text(message) + .font(.subheadline) + .foregroundColor(.white.opacity(0.8)) + .lineLimit(1) + } + Spacer() + } + + DeliveryProgressBar(status: status) + } + .padding() + .activityBackgroundTint(Color(red: 0.11, green: 0.13, blue: 0.19)) + .activitySystemActionForegroundColor(.white) + + } dynamicIsland: { context in + let status = context.state.data["status"]?.asString() ?? "preparing" + let message = context.state.data["message"]?.asString() ?? "Preparing" + let eta = context.state.data["estimatedTime"]?.asString() ?? "" + + return DynamicIsland { + DynamicIslandExpandedRegion(.leading) { + Image(systemName: statusIcon(for: status)) + .font(.title2) + .foregroundColor(statusColor(for: status)) + } + DynamicIslandExpandedRegion(.center) { + Text(statusLabel(for: status)) + .font(.headline) + } + DynamicIslandExpandedRegion(.trailing) { + if !eta.isEmpty { + Text(eta) + .font(.caption) + .foregroundColor(.secondary) + } + } + DynamicIslandExpandedRegion(.bottom) { + Text(message) + .font(.caption) + .foregroundColor(.secondary) + } + } compactLeading: { + Image(systemName: statusIcon(for: status)) + .foregroundColor(statusColor(for: status)) + } compactTrailing: { + Text(statusLabel(for: status)) + .font(.caption) + } minimal: { + Image(systemName: statusIcon(for: status)) + .foregroundColor(statusColor(for: status)) + } + } + } +} + +@available(iOS 16.2, *) +struct DeliveryProgressBar: View { + let status: String + + private var progress: CGFloat { + switch status { + case "on_the_way": return 0.6 + case "delivered": return 1.0 + default: return 0.25 + } + } + + var body: some View { + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 3) + .fill(Color.white.opacity(0.2)) + .frame(height: 6) + RoundedRectangle(cornerRadius: 3) + .fill(progress >= 1.0 ? Color.green : Color.blue) + .frame(width: geo.size.width * progress, height: 6) + } + } + .frame(height: 6) + } +} diff --git a/demo/build.md b/demo/build.md index 123454d..7abfbec 100644 --- a/demo/build.md +++ b/demo/build.md @@ -53,13 +53,14 @@ Plain class (not tied to UI framework) injected into the state management layer. - **Location**: setLocationShared(bool), requestLocationPermission() - **Privacy consent**: setConsentRequired(bool), setConsentGiven(bool) - **User IDs**: getExternalId() -> nullable, getOnesignalId() -> nullable -- **REST API** (delegated to OneSignalApiService): sendNotification(type) -> async bool, sendCustomNotification(title, body) -> async bool, fetchUser(onesignalId) -> async nullable UserData +- **Live Activities** (iOS only): startDefaultLiveActivity(activityId, attributes, content), exitLiveActivity(activityId) +- **REST API** (delegated to OneSignalApiService): sendNotification(type) -> async bool, sendCustomNotification(title, body) -> async bool, fetchUser(onesignalId) -> async nullable UserData, updateLiveActivity(activityId, event, eventUpdates?) -> async bool ### Prompt 1.4 - OneSignalApiService (REST API Client) Properties: \_appId (set during initialization) -Methods: setAppId(), getAppId(), sendNotification(type, subscriptionId), sendCustomNotification(title, body, subscriptionId), fetchUser(onesignalId) +Methods: setAppId(), getAppId(), hasApiKey(), sendNotification(type, subscriptionId), sendCustomNotification(title, body, subscriptionId), fetchUser(onesignalId), updateLiveActivity(activityId, event, eventUpdates?) sendNotification: @@ -75,6 +76,19 @@ fetchUser: - NO Authorization header (public endpoint) - Returns UserData with aliases, tags, emails, smsNumbers, externalId +updateLiveActivity (iOS only): + +- POST `https://api.onesignal.com/apps/{app_id}/live_activities/{activity_id}/notifications` +- Authorization: `Key {ONESIGNAL_API_KEY}` (requires REST API key) +- Body: `{ event: "update"|"end", event_updates, name, priority: 10 }` +- For end events: add `dismissal_date` (current unix timestamp), send `{ data: {} }` as `event_updates` if none provided +- Returns bool success + +hasApiKey: + +- Returns true if `ONESIGNAL_API_KEY` is set and not the placeholder default value +- Used to disable update/end buttons when no API key is configured + ### Prompt 1.5 - SDK Observers Initialize before UI renders: @@ -84,6 +98,12 @@ OneSignal.Debug.setLogLevel(verbose) OneSignal.consentRequired(cachedConsentRequired) OneSignal.consentGiven(cachedPrivacyConsent) OneSignal.initialize(appId) + +// iOS only +OneSignal.LiveActivities.setupDefault({ + enablePushToStart: true, + enablePushToUpdate: true, +}) ``` Register listeners: @@ -121,7 +141,8 @@ Clean up listeners on teardown (if platform requires it). 12. **Triggers Section** (Add/Add Multiple/Remove Selected/Clear All - IN MEMORY ONLY) 13. **Track Event Section** (JSON validation) 14. **Location Section** (Shared toggle, Prompt button) -15. **Next Page Button** +15. **Live Activities Section** (iOS only - Start, Update, Stop Updating, End) +16. **Next Page Button** ### Prompt 2.1a - App Section @@ -253,7 +274,41 @@ Separate SectionCard titled "User": - Toggle: "Location Shared" / "Share device location with OneSignal" - PROMPT LOCATION button -### Prompt 2.14 - Secondary Screen +### Prompt 2.14 - Live Activities Section (iOS Only) + +Only shown on iOS. Requires an iOS Widget Extension target with a Live Activity using `DefaultLiveActivityAttributes` from the OneSignal SDK. + +- Title: "Live Activities" with info icon +- Input card with two editable fields (pre-filled, not empty): + - "Activity ID" (default: "order-1") — identifies the Live Activity for all operations + - "Order #" (default: "ORD-1234") — attribute set at start, immutable after +- Four buttons: + 1. START LIVE ACTIVITY — calls `OneSignal.LiveActivities.startDefault(activityId, attributes, content)` with initial order status. Disabled when Activity ID is empty. + 2. UPDATE → {NEXT STATUS} — cycles through order statuses via REST API (`event: "update"`). Label dynamically shows the next status (e.g. "UPDATE → ON THE WAY"). Disabled when Activity ID is empty, while updating, or when no API key is configured. + 3. STOP UPDATING LIVE ACTIVITY — calls `OneSignal.LiveActivities.exitDefault(activityId)` to unsubscribe from remote updates. Outlined style. Disabled when Activity ID is empty. + 4. END LIVE ACTIVITY — ends the activity via REST API (`event: "end"`) with `dismissal_date`. Destructive style. Disabled when Activity ID is empty or when no API key is configured. + +Order status cycle (content state fields: `status`, `message`, `estimatedTime`): + +| Status | Message | ETA | +| --------- | ------------------------------ | ------ | +| preparing | Your order is being prepared | 15 min | +| on_the_way| Driver is heading your way | 10 min | +| delivered | Order delivered! | | + +Widget extension requirements: +- Uses `DefaultLiveActivityAttributes` from `OneSignalLiveActivities` +- Lock Screen banner: order number (from attributes), status icon, status label, message, ETA, progress bar +- Dynamic Island: expanded (icon, status, ETA, message), compact (icon + status label), minimal (icon) +- Status-based theming: preparing (orange), on_the_way (blue), delivered (green) + +API key setup: +- Store REST API key in `.env` file (e.g. `ONESIGNAL_API_KEY=your_rest_api_key`) +- Provide `.env.example` with placeholder value +- Add `.env` to `.gitignore` +- `hasApiKey()` on the API service checks that the key is present and not the placeholder + +### Prompt 2.15 - Secondary Screen Launched by "Next Activity" button at bottom of main screen: @@ -431,6 +486,7 @@ All actions show brief feedback via platform's transient message (SnackBar/Toast - IAM: "Sent In-App Message: {type}" - Outcomes: "Outcome sent: {name}" - Events: "Event tracked: {name}" +- Live Activities: "Started Live Activity: {activityId}", "Updated Live Activity: {activityId}", "Ended Live Activity: {activityId}", "Exited Live Activity: {activityId}" / "Failed to update Live Activity" / "Failed to end Live Activity" Clear previous message before showing new. All messages also logged via LogManager.i(). @@ -442,4 +498,6 @@ Default app id: `77e32082-ea27-42e3-a898-c72e141824ef` REST API key is NOT required for the fetchUser endpoint. +REST API key IS required for Live Activity update/end operations. Store in `.env` as `ONESIGNAL_API_KEY`. Disable update/end buttons when not configured. + Identifiers MUST be `com.onesignal.example` to work with existing `google-services.json` and `agconnect-services.json`. From 7072630ebefec7c2678760d3eec33ced9572c98e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 31 Mar 2026 15:42:27 -0700 Subject: [PATCH 4/6] docs(demo): add Live Activity replacement instruction --- demo/build.md | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/build.md b/demo/build.md index 7abfbec..bc03a9a 100644 --- a/demo/build.md +++ b/demo/build.md @@ -301,6 +301,7 @@ Widget extension requirements: - Lock Screen banner: order number (from attributes), status icon, status label, message, ETA, progress bar - Dynamic Island: expanded (icon, status, ETA, message), compact (icon + status label), minimal (icon) - Status-based theming: preparing (orange), on_the_way (blue), delivered (green) +- If the file `examples/demo/ios/OneSignalWidget/OneSignalWidgetLiveActivity.swift` already exists, replace its contents with the shared reference implementation at `https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/LiveActivity.swift` API key setup: - Store REST API key in `.env` file (e.g. `ONESIGNAL_API_KEY=your_rest_api_key`) From a939846425b8799e5c599aec9aedcba7714f06a6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 31 Mar 2026 16:20:39 -0700 Subject: [PATCH 5/6] docs(demo): format tables and fix secondary screen text --- demo/build.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/demo/build.md b/demo/build.md index bc03a9a..e5cb3c9 100644 --- a/demo/build.md +++ b/demo/build.md @@ -290,13 +290,14 @@ Only shown on iOS. Requires an iOS Widget Extension target with a Live Activity Order status cycle (content state fields: `status`, `message`, `estimatedTime`): -| Status | Message | ETA | -| --------- | ------------------------------ | ------ | -| preparing | Your order is being prepared | 15 min | -| on_the_way| Driver is heading your way | 10 min | -| delivered | Order delivered! | | +| Status | Message | ETA | +| ---------- | ---------------------------- | ------ | +| preparing | Your order is being prepared | 15 min | +| on_the_way | Driver is heading your way | 10 min | +| delivered | Order delivered! | | Widget extension requirements: + - Uses `DefaultLiveActivityAttributes` from `OneSignalLiveActivities` - Lock Screen banner: order number (from attributes), status icon, status label, message, ETA, progress bar - Dynamic Island: expanded (icon, status, ETA, message), compact (icon + status label), minimal (icon) @@ -304,6 +305,7 @@ Widget extension requirements: - If the file `examples/demo/ios/OneSignalWidget/OneSignalWidgetLiveActivity.swift` already exists, replace its contents with the shared reference implementation at `https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/LiveActivity.swift` API key setup: + - Store REST API key in `.env` file (e.g. `ONESIGNAL_API_KEY=your_rest_api_key`) - Provide `.env.example` with placeholder value - Add `.env` to `.gitignore` @@ -311,10 +313,10 @@ API key setup: ### Prompt 2.15 - Secondary Screen -Launched by "Next Activity" button at bottom of main screen: +Launched by "Next Screen" button at bottom of main screen: -- Title: "Secondary Activity" -- Centered large headline text "Secondary Activity" +- Title: "Secondary Screen" +- Centered large headline text "Secondary Screen" --- From 114f5b8a79e50828047d39a435f26d86defd3e3e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 31 Mar 2026 16:23:15 -0700 Subject: [PATCH 6/6] ci: remove Claude code review workflows --- .github/workflows/claude-code-review.yml | 44 --------------------- .github/workflows/claude.yml | 50 ------------------------ 2 files changed, 94 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index 4f6145b..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' - plugins: 'code-review@claude-code-plugins' - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 79fe056..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - # claude_args: '--allowed-tools Bash(gh pr:*)' -