Skip to content

Commit 2208b83

Browse files
authored
feat(demo): add Live Activities tooltip content (#25)
1 parent 313a0a2 commit 2208b83

5 files changed

Lines changed: 232 additions & 101 deletions

File tree

.github/workflows/claude-code-review.yml

Lines changed: 0 additions & 44 deletions
This file was deleted.

.github/workflows/claude.yml

Lines changed: 0 additions & 50 deletions
This file was deleted.

demo/LiveActivity.swift

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import ActivityKit
2+
import WidgetKit
3+
import SwiftUI
4+
import OneSignalLiveActivities
5+
6+
@available(iOS 16.2, *)
7+
struct OneSignalWidgetLiveActivity: Widget {
8+
9+
private func statusIcon(for status: String) -> String {
10+
switch status {
11+
case "on_the_way": return "box.truck.fill"
12+
case "delivered": return "checkmark.circle.fill"
13+
default: return "bag.fill"
14+
}
15+
}
16+
17+
private func statusColor(for status: String) -> Color {
18+
switch status {
19+
case "on_the_way": return .blue
20+
case "delivered": return .green
21+
default: return .orange
22+
}
23+
}
24+
25+
private func statusLabel(for status: String) -> String {
26+
switch status {
27+
case "on_the_way": return "On the Way"
28+
case "delivered": return "Delivered"
29+
default: return "Preparing"
30+
}
31+
}
32+
33+
var body: some WidgetConfiguration {
34+
ActivityConfiguration(for: DefaultLiveActivityAttributes.self) { context in
35+
let orderNumber = context.attributes.data["orderNumber"]?.asString() ?? "Order"
36+
let status = context.state.data["status"]?.asString() ?? "preparing"
37+
let message = context.state.data["message"]?.asString() ?? "Your order is being prepared"
38+
let eta = context.state.data["estimatedTime"]?.asString() ?? ""
39+
40+
VStack(spacing: 10) {
41+
HStack {
42+
Text(orderNumber)
43+
.font(.caption)
44+
.foregroundColor(.gray)
45+
Spacer()
46+
if !eta.isEmpty {
47+
Text(eta)
48+
.font(.caption)
49+
.foregroundColor(.white.opacity(0.7))
50+
}
51+
}
52+
53+
HStack(spacing: 12) {
54+
Image(systemName: statusIcon(for: status))
55+
.font(.title2)
56+
.foregroundColor(statusColor(for: status))
57+
58+
VStack(alignment: .leading, spacing: 2) {
59+
Text(statusLabel(for: status))
60+
.font(.headline)
61+
.foregroundColor(.white)
62+
Text(message)
63+
.font(.subheadline)
64+
.foregroundColor(.white.opacity(0.8))
65+
.lineLimit(1)
66+
}
67+
Spacer()
68+
}
69+
70+
DeliveryProgressBar(status: status)
71+
}
72+
.padding()
73+
.activityBackgroundTint(Color(red: 0.11, green: 0.13, blue: 0.19))
74+
.activitySystemActionForegroundColor(.white)
75+
76+
} dynamicIsland: { context in
77+
let status = context.state.data["status"]?.asString() ?? "preparing"
78+
let message = context.state.data["message"]?.asString() ?? "Preparing"
79+
let eta = context.state.data["estimatedTime"]?.asString() ?? ""
80+
81+
return DynamicIsland {
82+
DynamicIslandExpandedRegion(.leading) {
83+
Image(systemName: statusIcon(for: status))
84+
.font(.title2)
85+
.foregroundColor(statusColor(for: status))
86+
}
87+
DynamicIslandExpandedRegion(.center) {
88+
Text(statusLabel(for: status))
89+
.font(.headline)
90+
}
91+
DynamicIslandExpandedRegion(.trailing) {
92+
if !eta.isEmpty {
93+
Text(eta)
94+
.font(.caption)
95+
.foregroundColor(.secondary)
96+
}
97+
}
98+
DynamicIslandExpandedRegion(.bottom) {
99+
Text(message)
100+
.font(.caption)
101+
.foregroundColor(.secondary)
102+
}
103+
} compactLeading: {
104+
Image(systemName: statusIcon(for: status))
105+
.foregroundColor(statusColor(for: status))
106+
} compactTrailing: {
107+
Text(statusLabel(for: status))
108+
.font(.caption)
109+
} minimal: {
110+
Image(systemName: statusIcon(for: status))
111+
.foregroundColor(statusColor(for: status))
112+
}
113+
}
114+
}
115+
}
116+
117+
@available(iOS 16.2, *)
118+
struct DeliveryProgressBar: View {
119+
let status: String
120+
121+
private var progress: CGFloat {
122+
switch status {
123+
case "on_the_way": return 0.6
124+
case "delivered": return 1.0
125+
default: return 0.25
126+
}
127+
}
128+
129+
var body: some View {
130+
GeometryReader { geo in
131+
ZStack(alignment: .leading) {
132+
RoundedRectangle(cornerRadius: 3)
133+
.fill(Color.white.opacity(0.2))
134+
.frame(height: 6)
135+
RoundedRectangle(cornerRadius: 3)
136+
.fill(progress >= 1.0 ? Color.green : Color.blue)
137+
.frame(width: geo.size.width * progress, height: 6)
138+
}
139+
}
140+
.frame(height: 6)
141+
}
142+
}

demo/build.md

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,14 @@ Plain class (not tied to UI framework) injected into the state management layer.
5353
- **Location**: setLocationShared(bool), requestLocationPermission()
5454
- **Privacy consent**: setConsentRequired(bool), setConsentGiven(bool)
5555
- **User IDs**: getExternalId() -> nullable, getOnesignalId() -> nullable
56-
- **REST API** (delegated to OneSignalApiService): sendNotification(type) -> async bool, sendCustomNotification(title, body) -> async bool, fetchUser(onesignalId) -> async nullable UserData
56+
- **Live Activities** (iOS only): startDefaultLiveActivity(activityId, attributes, content), exitLiveActivity(activityId)
57+
- **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
5758

5859
### Prompt 1.4 - OneSignalApiService (REST API Client)
5960

6061
Properties: \_appId (set during initialization)
6162

62-
Methods: setAppId(), getAppId(), sendNotification(type, subscriptionId), sendCustomNotification(title, body, subscriptionId), fetchUser(onesignalId)
63+
Methods: setAppId(), getAppId(), hasApiKey(), sendNotification(type, subscriptionId), sendCustomNotification(title, body, subscriptionId), fetchUser(onesignalId), updateLiveActivity(activityId, event, eventUpdates?)
6364

6465
sendNotification:
6566

@@ -75,6 +76,19 @@ fetchUser:
7576
- NO Authorization header (public endpoint)
7677
- Returns UserData with aliases, tags, emails, smsNumbers, externalId
7778

79+
updateLiveActivity (iOS only):
80+
81+
- POST `https://api.onesignal.com/apps/{app_id}/live_activities/{activity_id}/notifications`
82+
- Authorization: `Key {ONESIGNAL_API_KEY}` (requires REST API key)
83+
- Body: `{ event: "update"|"end", event_updates, name, priority: 10 }`
84+
- For end events: add `dismissal_date` (current unix timestamp), send `{ data: {} }` as `event_updates` if none provided
85+
- Returns bool success
86+
87+
hasApiKey:
88+
89+
- Returns true if `ONESIGNAL_API_KEY` is set and not the placeholder default value
90+
- Used to disable update/end buttons when no API key is configured
91+
7892
### Prompt 1.5 - SDK Observers
7993

8094
Initialize before UI renders:
@@ -84,6 +98,12 @@ OneSignal.Debug.setLogLevel(verbose)
8498
OneSignal.consentRequired(cachedConsentRequired)
8599
OneSignal.consentGiven(cachedPrivacyConsent)
86100
OneSignal.initialize(appId)
101+
102+
// iOS only
103+
OneSignal.LiveActivities.setupDefault({
104+
enablePushToStart: true,
105+
enablePushToUpdate: true,
106+
})
87107
```
88108

89109
Register listeners:
@@ -121,7 +141,8 @@ Clean up listeners on teardown (if platform requires it).
121141
12. **Triggers Section** (Add/Add Multiple/Remove Selected/Clear All - IN MEMORY ONLY)
122142
13. **Track Event Section** (JSON validation)
123143
14. **Location Section** (Shared toggle, Prompt button)
124-
15. **Next Page Button**
144+
15. **Live Activities Section** (iOS only - Start, Update, Stop Updating, End)
145+
16. **Next Page Button**
125146

126147
### Prompt 2.1a - App Section
127148

@@ -253,12 +274,49 @@ Separate SectionCard titled "User":
253274
- Toggle: "Location Shared" / "Share device location with OneSignal"
254275
- PROMPT LOCATION button
255276

256-
### Prompt 2.14 - Secondary Screen
277+
### Prompt 2.14 - Live Activities Section (iOS Only)
278+
279+
Only shown on iOS. Requires an iOS Widget Extension target with a Live Activity using `DefaultLiveActivityAttributes` from the OneSignal SDK.
280+
281+
- Title: "Live Activities" with info icon
282+
- Input card with two editable fields (pre-filled, not empty):
283+
- "Activity ID" (default: "order-1") — identifies the Live Activity for all operations
284+
- "Order #" (default: "ORD-1234") — attribute set at start, immutable after
285+
- Four buttons:
286+
1. START LIVE ACTIVITY — calls `OneSignal.LiveActivities.startDefault(activityId, attributes, content)` with initial order status. Disabled when Activity ID is empty.
287+
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.
288+
3. STOP UPDATING LIVE ACTIVITY — calls `OneSignal.LiveActivities.exitDefault(activityId)` to unsubscribe from remote updates. Outlined style. Disabled when Activity ID is empty.
289+
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.
290+
291+
Order status cycle (content state fields: `status`, `message`, `estimatedTime`):
257292

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

260-
- Title: "Secondary Activity"
261-
- Centered large headline text "Secondary Activity"
299+
Widget extension requirements:
300+
301+
- Uses `DefaultLiveActivityAttributes` from `OneSignalLiveActivities`
302+
- Lock Screen banner: order number (from attributes), status icon, status label, message, ETA, progress bar
303+
- Dynamic Island: expanded (icon, status, ETA, message), compact (icon + status label), minimal (icon)
304+
- Status-based theming: preparing (orange), on_the_way (blue), delivered (green)
305+
- 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`
306+
307+
API key setup:
308+
309+
- Store REST API key in `.env` file (e.g. `ONESIGNAL_API_KEY=your_rest_api_key`)
310+
- Provide `.env.example` with placeholder value
311+
- Add `.env` to `.gitignore`
312+
- `hasApiKey()` on the API service checks that the key is present and not the placeholder
313+
314+
### Prompt 2.15 - Secondary Screen
315+
316+
Launched by "Next Screen" button at bottom of main screen:
317+
318+
- Title: "Secondary Screen"
319+
- Centered large headline text "Secondary Screen"
262320

263321
---
264322

@@ -431,6 +489,7 @@ All actions show brief feedback via platform's transient message (SnackBar/Toast
431489
- IAM: "Sent In-App Message: {type}"
432490
- Outcomes: "Outcome sent: {name}"
433491
- Events: "Event tracked: {name}"
492+
- 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"
434493

435494
Clear previous message before showing new. All messages also logged via LogManager.i().
436495

@@ -442,4 +501,6 @@ Default app id: `77e32082-ea27-42e3-a898-c72e141824ef`
442501

443502
REST API key is NOT required for the fetchUser endpoint.
444503

504+
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.
505+
445506
Identifiers MUST be `com.onesignal.example` to work with existing `google-services.json` and `agconnect-services.json`.

demo/tooltip_content.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,27 @@
7878
"description": "Takes over the entire screen. Best for onboarding, promotions, or rich media content."
7979
}
8080
]
81+
},
82+
"liveActivities": {
83+
"title": "Live Activities",
84+
"description": "Display real-time updates on the iOS Lock Screen and Dynamic Island. Uses the DefaultLiveActivityAttributes type provided by the OneSignal SDK.",
85+
"options": [
86+
{
87+
"name": "Start",
88+
"description": "Launch a new Live Activity with an activity ID and initial content using the SDK's startDefault method."
89+
},
90+
{
91+
"name": "Update",
92+
"description": "Update the Live Activity's content state via the REST API."
93+
},
94+
{
95+
"name": "End",
96+
"description": "Terminate the Live Activity via the REST API with immediate dismissal."
97+
},
98+
{
99+
"name": "Stop Updating",
100+
"description": "Call the SDK's exit method to unsubscribe this device from future updates for the given activity ID."
101+
}
102+
]
81103
}
82104
}

0 commit comments

Comments
 (0)