Skip to content

Commit 12ad310

Browse files
author
Rover Release Bot 🤖
committed
Releasing 4.11.0
1 parent 7888d1f commit 12ad310

File tree

50 files changed

+2944
-120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2944
-120
lines changed

AGENTS.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# AGENTS.md
2+
3+
This file provides guidance to agents such as Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is the Rover iOS SDK, a modular collection of Swift frameworks for mobile experiences, campaigns automation, and marketing. The SDK follows a modular architecture allowing inclusion of only relevant functionality.
8+
9+
## Architecture
10+
11+
### Relationships with External Systems
12+
13+
- **Rover GraphQL API Gateway**: The GraphQL API is used for outbound events and classic experiences, and various legacy features. These outbound events are used for behavioural automation, setting up APNs push tokens, and other local device/user context and preferences into the Rover cloud.
14+
- **Bobcat/Engage API**: Rover is moving towards a new set of cloud services. This is the new API for the Rover platform, and use of the old API will be eventually phased out.
15+
16+
### Modular Design
17+
The SDK is organized into independent modules (Swift Package Manager targets):
18+
19+
- **RoverFoundation**: Core dependency injection container, utilities, and base types
20+
- **RoverData**: HTTP client, event queue, sync coordination, context management
21+
- **RoverUI**: UI services, routing, session management, image handling
22+
- **RoverExperiences**: Rendering of experiences, dynamic UI content. There are versions of the Experiences product, classic and modern. Classic is the old legacy version of experiences, whereas modern is modelled on a subset of SwiftUI. They have separate authoring tools.
23+
- **RoverNotifications**: Push notifications, a legacy version Inbox (sometimes called Notification Center), and Communication Hub (the new Inbox that replaces it)
24+
- **RoverLocation**: Capturing of device location for targeting purposes, along with Geofencing and beacon support. Deprecated for privacy reasons.
25+
- **RoverDebug**: Development tools and settings UI
26+
- **RoverTelephony**: Telephony context provider. Deprecated for privacy reasons.
27+
- **Third-party integrations**: RoverTicketmaster, RoverSeatGeek, RoverAxs, RoverAdobeExperience
28+
- **RoverAppExtensions**: Notification service extension support. This is used to add behaviour at APNs push reception time, such as enabling rich media support, or persisting notification content in local storage.
29+
30+
### Module Dependencies
31+
```
32+
RoverFoundation (base)
33+
├── RoverData (depends on Foundation)
34+
│ ├── RoverUI (depends on Data)
35+
│ │ ├── RoverExperiences (depends on UI, Foundation, Data)
36+
│ │ ├── RoverNotifications (depends on Data, UI)
37+
│ │ └── RoverDebug (depends on UI)
38+
│ ├── RoverLocation (depends on Data)
39+
│ ├── RoverTelephony (depends on Data)
40+
│ ├── RoverTicketmaster (depends on Data)
41+
│ ├── RoverSeatGeek (depends on Data)
42+
│ ├── RoverAxs (depends on Data)
43+
│ └── RoverAdobeExperience (depends on Data)
44+
└── RoverAppExtensions (depends on Foundation)
45+
```
46+
47+
### Dependency Injection
48+
The SDK uses a custom DI container (`Sources/Foundation/Container/`) with assemblers for each module:
49+
- Each module has an `*Assembler.swift` that registers services
50+
- Services are resolved through the main `Rover` singleton
51+
- Supports singleton and transient scopes with factory patterns
52+
- `Assembler` protocol defines module configuration
53+
- `Container` manages service registration
54+
- `Resolver` handles service resolution
55+
56+
Each module follows the pattern:
57+
```swift
58+
class ModuleAssembler: Assembler {
59+
func assemble(container: Container) {
60+
// Register services
61+
}
62+
63+
func containerDidAssemble(resolver: Resolver) {
64+
// Post-registration configuration
65+
}
66+
}
67+
```
68+
69+
### Communication Hub (New Inbox)
70+
New messaging feature located in `Sources/Notifications/Communication Hub/`:
71+
- Core Data models for posts and subscriptions
72+
- SwiftUI views for inbox and post detail
73+
- Sync logic using the new Bobcat/Engage API
74+
- Sync participant for syncing when the rest of the SDK is syncing
75+
76+
### Experience Rendering Architecture
77+
The Experiences module has dual rendering paths:
78+
- **Classic Experiences**: Legacy UIKit-based renderer (`Sources/Experiences/ClassicExperiences/`)
79+
- **Modern Experiences**: SwiftUI-style declarative renderer (`Sources/Experiences/Experiences/`)
80+
81+
They are different products with different formats (and different authoring tools to be found elsewhere). Experiences are rendered through a hierarchical node system where each UI element is a `Node` with view-specific implementations in both classic and modern renderers.
82+
83+
### Data Management
84+
- **Event System**: Queue-based event tracking with offline support (`Sources/Data/EventQueue/`)
85+
- **Sync System**: Paginated data synchronization framework (`Sources/Data/SyncCoordinator/`)
86+
- **Context System**: Device and user context collection (`Sources/Data/Context/`)
87+
88+
### External Dependencies
89+
- `ZIPFoundation`: For experience asset archive handling
90+
- `iOS-TicketmasterSDK`: The RoverTicketmaster module directly depends on Ticketmaster's Ignite SDK. However, this strategy is being phased out.
91+
92+
93+
## Development Commands
94+
95+
### Build and Test
96+
```bash
97+
# Build the Testbench app (main development target)
98+
99+
xcodebuild -scheme "Rover Bench" -project "Testbench/Rover Bench.xcodeproj" build
100+
2>&1 | grep -A2 -B2 "error:" | head -20
101+
102+
### Running Tests
103+
```bash
104+
# Run unit tests for specific modules
105+
xcodebuild test -scheme "RoverFoundation" -project "Testbench/Rover Bench.xcodeproj" -destination "platform=iOS Simulator,name=iPhone 15"
106+
xcodebuild test -scheme "RoverData" -project "Testbench/Rover Bench.xcodeproj" -destination "platform=iOS Simulator,name=iPhone 15"
107+
# (Similar for other modules)
108+
109+
# Alternative: Test using the Example project
110+
xcodebuild test -project Example/Example.xcodeproj -scheme Example -destination "platform=iOS Simulator,name=iPhone 15"
111+
112+
# Test individual modules via workspace (if needed)
113+
xcodebuild test -workspace Example/Example.xcworkspace -scheme RoverFoundation -destination "platform=iOS Simulator,name=iPhone 15"
114+
```
115+
116+
### Package Verification
117+
Since this is a Swift Package, you can verify the package structure:
118+
```bash
119+
swift package dump-package
120+
swift package resolve
121+
```
122+
123+
## Key Files and Patterns
124+
125+
### Entry Points
126+
- `Sources/Foundation/Rover.swift`: Main SDK singleton and container
127+
- `Testbench/Rover Bench/TestbenchApp.swift`: SwiftUI test app
128+
- `Example/Example/AppDelegate.swift`: UIKit example app
129+
130+
### Service Registration
131+
Services are registered in assemblers following this pattern:
132+
```swift
133+
container.register(ServiceProtocol.self) { resolver in
134+
ServiceImplementation(dependency: resolver.resolve(Dependency.self)!)
135+
}
136+
```
137+
138+
### Core Data Models
139+
- Location: `Sources/Location/Model/RoverLocation.xcdatamodeld/`
140+
- Communication Hub: `Sources/Notifications/Communication Hub/RoverCommHubModel.xcdatamodeld/`
141+
142+
### Extension Points
143+
- Context Providers: `Sources/Data/Context/Providers/` for adding app context
144+
- Route Handlers: Handle deep links and navigation
145+
- Sync Participants: Integrate with server synchronization
146+
147+
## Common Development Patterns
148+
149+
### Adding New Features
150+
1. Create service interface in appropriate module
151+
2. Implement service with dependency injection
152+
3. Register service in module's assembler
153+
4. Add route handler if navigation is needed
154+
5. Update sync participant (or standalone sync participant if it is not using the GraphQL API) if server communication is required
155+
156+
### Testing
157+
- Unit tests are in `Tests/` directory organized by module
158+
- Use XCTest framework with `@testable import` for internal access
159+
- Mock services using the dependency injection container
160+
161+
### Privacy
162+
Each module includes `Resources/PrivacyInfo.xcprivacy` for App Store privacy declarations.
163+
164+
## WORKFLOW INSTRUCTIONS
165+
166+
- You must always ensure tests pass between each step.
167+
- You must always use `swift format -i` on a file after you've edited it.
168+
- If tests do not already exist for the components involved in feature being planned, include steps adding them in the same plan.
169+
170+
### CODE STYLE
171+
172+
- Prefer guard statements over if statements with else blocks.
173+
- Use Swift structured concurrency when possible.
174+
- Never change Public APIs (any API that is marked as public), without explicit consent from the developer. This is an SDK with a public API with existing customers.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

Example/Example/AppDelegate.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
132132
Rover.shared.syncCoordinator.sync(completionHandler: completionHandler)
133133
}
134134

135+
// Called when a push arrives while app is backgrounded and it has content-available is set to 1
135136
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
136-
// Sync notifications, beacons and geofences when the Rover server issues requests a sync via remote push.
137-
Rover.shared.syncCoordinator.sync(completionHandler: completionHandler)
137+
if Rover.shared.didReceiveRemoteNotification(userInfo: userInfo, fetchCompletionHandler: completionHandler) {
138+
return
139+
}
140+
141+
completionHandler(.noData)
138142
}
139-
143+
140144
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
141145
// The device successfully registered for push notifications. Pass the token to Rover.
142146
Rover.shared.tokenManager.setToken(deviceToken)
@@ -202,17 +206,21 @@ extension AppDelegate: CLLocationManagerDelegate {
202206

203207
extension AppDelegate: UNUserNotificationCenterDelegate {
204208
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
205-
// A notification was received while the app was in the foreground.
206-
if let roverNotification = notification.roverNotification {
207-
// If it's a Rover notification, add it to the Rover Inbox immediately. This means if the app is currently open to the inbox the table view can live update to include it immediately.
208-
Rover.shared.notificationStore.addNotification(roverNotification)
209+
if Rover.shared.userNotificationCenterWillPresent(notification: notification, withCompletionHandler: completionHandler) {
210+
return
209211
}
212+
210213
// Tell the operating system to display the notification the same way as if the app was in the background.
211-
completionHandler([.badge, .sound, .alert])
214+
completionHandler([.badge, .sound, .banner])
212215
}
213216

214217
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
215218
// The user tapped a notification. Pass the response to Rover to handle the intended behavior.
216-
Rover.shared.notificationHandler.handle(response, completionHandler: completionHandler)
219+
if Rover.shared.userNotificationCenterDidReceive(response: response, withCompletionHandler: completionHandler) {
220+
return
221+
}
222+
223+
// If Rover didn't handle the notification and it returns false, then we need to handle it ourselves.
224+
completionHandler()
217225
}
218226
}

GEMINI.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

Package.resolved

Lines changed: 48 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/AXS/AXSManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class AXSManager: AXSAuthorizer, PrivacyListener {
5757

5858
func setUserID(_ userID: String?, flashMemberID: String?, flashMobileID: String?) {
5959
guard privacyService.trackingMode == .default else {
60+
os_log("AXS user ID (with flash IDs) set while privacy is in anonymous/anonymized mode, ignored", log: .axs, type: .info)
6061
return
6162
}
6263

Sources/AdobeExperience/AdobeExperienceManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class AdobeExperienceManager: AdobeExperienceAuthorizer, PrivacyListener {
3333

3434
func setECID(_ ecid: String) {
3535
guard privacyService.trackingMode == .default else {
36+
os_log("Adobe Experience ECID set while privacy is in anonymous/anonymized mode, ignored", log: .AdobeExperience, type: .info)
3637
return
3738
}
3839

Sources/Data/Auth/AuthenticationContext.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import Foundation
1717
import os.log
1818

1919
public class AuthenticationContext {
20-
public private(set) var sdkAuthenticationEnabledDomains = Set<String>(["*.rover.io"])
21-
20+
public private(set) var sdkAuthenticationEnabledDomains = Set<String>(["api.rover.io"])
21+
2222
private var sdkAuthenticationIDTokenRefreshCallback: () -> Void = {}
2323

2424
private let userDefaults: UserDefaults

Sources/Data/DataAssembler.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ public struct DataAssembler: Assembler {
2424
public var flushEventsInterval: Double
2525
public var maxEventBatchSize: Int
2626
public var maxEventQueueSize: Int
27+
public var engageEndpoint: URL
2728

28-
public init(accountToken: String, endpoint: URL = URL(string: "https://api.rover.io/graphql")!, flushEventsAt: Int = 20, flushEventsInterval: Double = 30.0, maxEventBatchSize: Int = 100, maxEventQueueSize: Int = 1_000) {
29+
public init(accountToken: String, endpoint: URL = URL(string: "https://api.rover.io/graphql")!, engageEndpoint: URL = URL(string: "https://engage.rover.io")!, flushEventsAt: Int = 20, flushEventsInterval: Double = 30.0, maxEventBatchSize: Int = 100, maxEventQueueSize: Int = 1_000) {
2930
self.accountToken = accountToken
3031
self.endpoint = endpoint
3132

3233
self.flushEventsAt = flushEventsAt
3334
self.flushEventsInterval = flushEventsInterval
3435
self.maxEventBatchSize = maxEventBatchSize
3536
self.maxEventQueueSize = maxEventQueueSize
37+
self.engageEndpoint = engageEndpoint
3638
}
3739

3840
// swiftlint:disable:next function_body_length // Assemblers are fairly declarative.
@@ -96,6 +98,7 @@ public struct DataAssembler: Assembler {
9698
return HTTPClient(
9799
accountToken: accountToken,
98100
endpoint: endpoint,
101+
engageEndpoint: engageEndpoint,
99102
session: URLSession(configuration: URLSessionConfiguration.default),
100103
authContext: authContext
101104
)

Sources/Data/EventQueue/EventQueue.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ public class EventQueue {
255255
os_log("Successfully uploaded %d event(s)", log: .events, type: .debug, events.count)
256256
self.removeEvents(events)
257257
} catch {
258-
os_log("Failed to upload events: %@", log: .events, type: .error, error.logDescription)
258+
let responseBodyString = String(data: data, encoding: .utf8) ?? "none"
259+
os_log("Failed to upload events: %@, response body: %s", log: .events, type: .error, error.logDescription, responseBodyString)
259260
os_log("Will retry failed events", log: .events, type: .info)
260261
}
261262
}

0 commit comments

Comments
 (0)