ROfferBanner Component
The ROfferBanner component is a dynamic offer banner that receives configuration from your backend via WebSocket, allowing for real-time updates and campaign management.
Features
- Dynamic logofrom URL
- Configurable title and subtitle - Background imagewith color fallback
- Real-time countdown timerwith automatic updates
- Discount badge - Call-to-action buttonwith dynamic color
- In-app navigationvia
onNavigateToStorecallback - WebSocket integrationfor live updates
- Skeleton loadingstates
- Automatic lifecycle management - Support for both
countdownandoffer_bannercomponent types - Automatic campaign state handling - Component automatically hides when campaign is inactive or paused
Quick Start
Global Singleton Pattern (Recommended)
The ComponentManager now works as a global singleton, similar to CartManager. This means you can access it from anywhere in your app without passing parameters.
- Simple Usage (Recommended)
- Advanced Usage
import SwiftUI
import ReachuUI
import ReachuCore
struct ContentView: View {
var body: some View {
VStack {
// Global singleton - no parameters needed
ROfferBannerDynamic()
// Rest of your content...
}
}
}
import SwiftUI
import ReachuUI
import ReachuCore
struct ContentView: View {
@StateObject private var componentManager = ComponentManager.shared
var body: some View {
VStack {
// Access global component manager
if let bannerConfig = componentManager.activeBanner {
ROfferBanner(config: bannerConfig)
}
// Show connection status
HStack {
Circle()
.fill(componentManager.isConnected ? .green : .red)
.frame(width: 8, height: 8)
Text(componentManager.isConnected ? "Connected" : "Disconnected")
.font(.caption)
}
// Rest of your content...
}
}
}
Component Properties
ROfferBannerDynamic
| Property | Type | Description |
|---|---|---|
onNavigateToStore | (() -> Void)? | Optional callback for in-app navigation to product store (takes priority over deeplinks) |
ROfferBannerContainer (Deprecated)
Note:
ROfferBannerContaineris deprecated. UseROfferBannerDynamicinstead.
| Property | Type | Description |
|---|---|---|
init() | Void | No parameters needed - uses global singleton (deprecated, use ROfferBannerDynamic) |
ROfferBanner
| Property | Type | Description |
|---|---|---|
config | OfferBannerConfig | Banner configuration object |
onNavigateToStore | (() -> Void)? | Optional callback for in-app navigation to product store (takes priority over deeplinks) |
deeplink | String? | Optional custom deeplink URL override |
height | CGFloat? | Optional custom banner height |
titleFontSize | CGFloat? | Optional custom title font size |
subtitleFontSize | CGFloat? | Optional custom subtitle font size |
badgeFontSize | CGFloat? | Optional custom badge font size |
buttonFontSize | CGFloat? | Optional custom button font size |
OfferBannerConfig
| Property | Type | Description |
|---|---|---|
logoUrl | String? | Optional URL for the logo image |
title | String | Main banner title |
subtitle | String? | Optional subtitle |
backgroundImageUrl | String? | Optional background image URL |
backgroundColor | String? | Optional background color (hex format) - used as fallback if image fails |
countdownEndDate | String | ISO 8601 timestamp for countdown end |
discountBadgeText | String? | Optional text for discount badge |
ctaText | String? | Optional call-to-action button text |
ctaLink | String? | Optional CTA button link |
deeplink | String? | Optional deeplink URL for navigation |
overlayOpacity | Double? | Background overlay opacity (0.0-1.0) |
buttonColor | String? | Hex color for CTA button |
Configuration
Global Singleton Configuration
The ComponentManager automatically reads the campaignId from your reachu-config.json file and creates a global singleton instance:
{
"liveShow": {
"campaignId": 3,
"tipio": {
"apiKey": "your-api-key",
"baseUrl": "https://your-backend.com"
}
}
}
Benefits: - Zero configuration - No parameters needed anywhere
- Global access - Available from any view with
ComponentManager.shared - Centralized configuration - All settings in one file
- Consistent across app - Same campaign ID everywhere
- Easy to change - Update campaign ID in config file
- Auto-connection - Automatically connects on app startup
Backend Integration
API Endpoints
The component automatically connects to these endpoints:
Get Active Components
GET https://event-streamer-angelo100.replit.app/api/campaigns/{campaignId}/active-components
Response:
[
{
"id": "abc123",
"type": "offer_banner",
"config": {
"logoUrl": "https://storage.url/xxl-logo.png",
"title": "Ukens tilbud",
"subtitle": "Se denne ukes beste tilbud",
"backgroundImageUrl": "https://storage.url/football-grass.jpg",
"countdownEndDate": "2025-10-30T23:59:59Z",
"discountBadgeText": "Opp til 30%",
"ctaText": "Se alle tilbud →",
"ctaLink": "https://xxlsports.no/offers",
"overlayOpacity": 0.4,
"buttonColor": "#FF6B6B"
}
}
]
WebSocket Connection
wss://event-streamer-angelo100.replit.app/ws/{campaignId}
WebSocket Messages
The component listens for these WebSocket messages:
Component Status Changed
{
"type": "component_status_changed",
"campaignId": 3,
"componentId": "abc123-xyz789",
"status": "active",
"component": {
"id": "abc123-xyz789",
"type": "offer_banner",
"config": {
"logoUrl": "https://storage.url/xxl-logo.png",
"title": "Ukens tilbud",
"subtitle": "Se denne ukes beste tilbud",
"backgroundImageUrl": "https://storage.url/football-grass.jpg",
"countdownEndDate": "2025-10-30T23:59:59Z",
"discountBadgeText": "Opp til 30%",
"ctaText": "Se alle tilbud →",
"ctaLink": "https://xxlsports.no/offers",
"overlayOpacity": 0.4,
"buttonColor": "#FF6B6B"
}
}
}
Campaign Ended
{
"type": "campaign_ended",
"campaignId": 3
}
Implementation Examples
Complete App Integration
import SwiftUI
import ReachuUI
import ReachuCore
struct AppView: View {
@StateObject private var cartManager = CartManager()
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// Header
Text("My App")
.font(.largeTitle)
.fontWeight(.bold)
// Dynamic offer banner - no parameters needed!
ROfferBannerDynamic()
.padding(.horizontal)
// Or with custom navigation callback
ROfferBannerDynamic(
onNavigateToStore: {
// Navigate to product store view
showProductStore = true
}
)
.padding(.horizontal)
// Products section
LazyVGrid(columns: gridColumns) {
ForEach(products) { product in
ProductCard(product: product)
.environmentObject(cartManager)
}
}
}
.padding()
}
}
.environmentObject(cartManager)
}
}
Custom Banner Management
struct CustomBannerView: View {
@StateObject private var componentManager = ComponentManager.shared
@State private var showConnectionStatus = false
var body: some View {
VStack(spacing: 16) {
// Connection status
if showConnectionStatus {
HStack {
Circle()
.fill(componentManager.isConnected ? .green : .red)
.frame(width: 8, height: 8)
Text(componentManager.isConnected ? "Connected" : "Disconnected")
.font(.caption)
.foregroundColor(.secondary)
}
}
// Banner content
if let bannerConfig = componentManager.activeBanner {
ROfferBanner(
config: bannerConfig,
onNavigateToStore: {
// Handle navigation to store
showProductStore = true
}
)
} else {
Text("No active banner")
.foregroundColor(.secondary)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
// No need for onAppear/onDisappear - singleton handles lifecycle automatically
}
}
Styling
The component uses a fixed design with these characteristics:
- Height: 160 points
- Corner radius: 12 points
- Padding: 16 points horizontal, 12 points vertical
- Shadow: Black with 0.3 opacity, 8 point radius, 4 point Y offset
- Typography: System fonts with specific weights and sizes
- Colors: Dynamic button color from configuration
Custom Styling
The component automatically adapts to your app's color scheme and supports:
- Dynamic button colors via
buttonColorin configuration - Automatic dark/light mode adaptation
- Proper contrast ratios for accessibility
Lifecycle Management
Global Singleton Pattern
The ComponentManager.shared singleton automatically handles:
- Auto-initialization: Connects automatically when first accessed
- API Fetch: Fetches initial components from REST API
- WebSocket Updates: Listens for real-time updates
- Global State: Maintains state across all views
- Resource Management: Automatically manages connections and cleanup
Using ComponentManager.shared
When using ComponentManager.shared directly:
struct MyView: View {
@StateObject private var componentManager = ComponentManager.shared
var body: some View {
VStack {
// Access global state
if let bannerConfig = componentManager.activeBanner {
ROfferBanner(config: bannerConfig)
}
// Check connection status
Text("Status: \(componentManager.isConnected ? "Connected" : "Disconnected")")
}
// No manual lifecycle management needed!
}
}
Benefits: - Zero configuration - No parameters or setup needed
- Global state - Same instance across all views
- Automatic lifecycle - No manual connect/disconnect calls
- Consistent behavior - Same campaign ID everywhere
Error Handling
The component includes comprehensive error handling:
- Invalid URLs: Fallback to placeholder images
- Invalid countdown dates: Logged and ignored
- WebSocket failures: Graceful degradation
- API failures: Don't prevent app functionality
- Network issues: Automatic retry with exponential backoff
Performance Considerations
- Lazy loading: Images load asynchronously
- Skeleton states: Prevent layout jumps during loading
- Memory management: Automatic cleanup of resources
- Efficient updates: Only re-renders when configuration changes
Dependencies
import ReachuUI // For ROfferBanner components
import ReachuCore // For ComponentManager and data models
Troubleshooting
Banner Not Showing
- Check campaign ID: Ensure the campaign ID exists in your backend and
reachu-config.json - Verify connection: Check if
ComponentManager.shared.isConnectedis true - Check API response: Verify the REST API returns active components
- WebSocket connection: Ensure WebSocket can connect to your server
- Global state: Ensure you're using
ComponentManager.sharedconsistently
Images Not Loading
- URL validation: Ensure image URLs are valid and accessible
- Network connectivity: Check internet connection
- CORS issues: Ensure your image server allows cross-origin requests
Countdown Not Working
- Date format: Ensure
countdownEndDateis in ISO 8601 format - Future date: Ensure the date is in the future
- Timezone: Consider timezone differences
Related Components
- Product Slider - For displaying products
- Floating Cart Indicator - For cart management
- Checkout System - For checkout flow