Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 0 additions & 44 deletions .github/workflows/claude-code-review.yml

This file was deleted.

50 changes: 0 additions & 50 deletions .github/workflows/claude.yml

This file was deleted.

142 changes: 142 additions & 0 deletions demo/LiveActivity.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
75 changes: 68 additions & 7 deletions demo/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -253,12 +274,49 @@ 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`):

Launched by "Next Activity" button at bottom of main screen:
| Status | Message | ETA |
| ---------- | ---------------------------- | ------ |
| preparing | Your order is being prepared | 15 min |
| on_the_way | Driver is heading your way | 10 min |
| delivered | Order delivered! | |

- Title: "Secondary Activity"
- Centered large headline text "Secondary Activity"
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)
- 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`
- `hasApiKey()` on the API service checks that the key is present and not the placeholder

### Prompt 2.15 - Secondary Screen

Launched by "Next Screen" button at bottom of main screen:

- Title: "Secondary Screen"
- Centered large headline text "Secondary Screen"

---

Expand Down Expand Up @@ -431,6 +489,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().

Expand All @@ -442,4 +501,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`.
22 changes: 22 additions & 0 deletions demo/tooltip_content.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
{
"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."
}
]
}
}
Loading