Skip to main content

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 onNavigateToStore callback
  • WebSocket integrationfor live updates
  • Skeleton loadingstates
  • Automatic lifecycle management - Support for both countdown and offer_banner component types - Automatic campaign state handling - Component automatically hides when campaign is inactive or paused

Quick Start

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.

import SwiftUI
import ReachuUI
import ReachuCore

struct ContentView: View {
var body: some View {
VStack {
// Global singleton - no parameters needed
ROfferBannerDynamic()

// Rest of your content...
}
}
}

Component Properties

ROfferBannerDynamic

PropertyTypeDescription
onNavigateToStore(() -> Void)?Optional callback for in-app navigation to product store (takes priority over deeplinks)

ROfferBannerContainer (Deprecated)

Note:ROfferBannerContainer is deprecated. Use ROfferBannerDynamic instead.

PropertyTypeDescription
init()VoidNo parameters needed - uses global singleton (deprecated, use ROfferBannerDynamic)

ROfferBanner

PropertyTypeDescription
configOfferBannerConfigBanner configuration object
onNavigateToStore(() -> Void)?Optional callback for in-app navigation to product store (takes priority over deeplinks)
deeplinkString?Optional custom deeplink URL override
heightCGFloat?Optional custom banner height
titleFontSizeCGFloat?Optional custom title font size
subtitleFontSizeCGFloat?Optional custom subtitle font size
badgeFontSizeCGFloat?Optional custom badge font size
buttonFontSizeCGFloat?Optional custom button font size

OfferBannerConfig

PropertyTypeDescription
logoUrlString?Optional URL for the logo image
titleStringMain banner title
subtitleString?Optional subtitle
backgroundImageUrlString?Optional background image URL
backgroundColorString?Optional background color (hex format) - used as fallback if image fails
countdownEndDateStringISO 8601 timestamp for countdown end
discountBadgeTextString?Optional text for discount badge
ctaTextString?Optional call-to-action button text
ctaLinkString?Optional CTA button link
deeplinkString?Optional deeplink URL for navigation
overlayOpacityDouble?Background overlay opacity (0.0-1.0)
buttonColorString?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 buttonColor in configuration
  • Automatic dark/light mode adaptation
  • Proper contrast ratios for accessibility

Lifecycle Management

Global Singleton Pattern

The ComponentManager.shared singleton automatically handles:

  1. Auto-initialization: Connects automatically when first accessed
  2. API Fetch: Fetches initial components from REST API
  3. WebSocket Updates: Listens for real-time updates
  4. Global State: Maintains state across all views
  5. 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

  1. Check campaign ID: Ensure the campaign ID exists in your backend and reachu-config.json
  2. Verify connection: Check if ComponentManager.shared.isConnected is true
  3. Check API response: Verify the REST API returns active components
  4. WebSocket connection: Ensure WebSocket can connect to your server
  5. Global state: Ensure you're using ComponentManager.shared consistently

Images Not Loading

  1. URL validation: Ensure image URLs are valid and accessible
  2. Network connectivity: Check internet connection
  3. CORS issues: Ensure your image server allows cross-origin requests

Countdown Not Working

  1. Date format: Ensure countdownEndDate is in ISO 8601 format
  2. Future date: Ensure the date is in the future
  3. Timezone: Consider timezone differences