UTA DevHub
UI Development/Theme Management

White-Label Themes

Implementation guide for white-label theming, brand-specific configurations, and dynamic theme switching.

White-Label Themes

Overview

This guide explains how to implement white-label theming in our React Native application, enabling the app to dynamically adapt its visual identity based on different brand configurations. It covers brand configuration structure, runtime theme switching, and integration with the app initialization flow.

Quick Start

Create a brand configuration file:

// core/domains/white-label/configs/brandA.ts
import { BrandConfig } from '../types';
 
export const brandAConfig: BrandConfig = {
  id: 'brandA',
  name: 'Brand A',
  theme: {
    colors: {
      primary: '#00AA5B',
      primaryLight: '#4DC491',
      primaryDark: '#007A40',
      secondary: '#F9A826',
      secondaryLight: '#FFC670',
      secondaryDark: '#C78000',
    }
  },
  apiBaseUrl: 'https://api.brand-a.com',
  features: {
    enablePayments: true,
    enableSubscriptions: true,
    enableChat: false,
  },
  assets: {
    logoUrl: require('@/assets/brands/brandA/logo.png'),
    splashBackground: require('@/assets/brands/brandA/splash.png'),
  }
};

Purpose & Scope

This document provides implementation guidance for:

  • Creating and organizing brand configurations
  • Loading brand-specific themes during app initialization
  • Switching brands at runtime
  • Integrating white-label themes with the core theme system
  • Handling brand-specific assets and feature flags

The guide is intended for developers implementing multi-brand or white-label solutions in our React Native application.

Prerequisites

Before implementing white-label themes, ensure you are familiar with:

Implementation Details

Brand Configuration Structure

The white-label system is built around a standardized BrandConfig type that defines all customizable aspects of a brand:

// core/domains/white-label/types.ts
import { ImageSourcePropType } from 'react-native';
import { ThemeOverrides } from '@/core/shared/styles/types';
 
export interface BrandConfig {
  id: string;                  // Unique identifier for the brand
  name: string;                // Display name
  theme: ThemeOverrides;       // Brand-specific theme overrides
  apiBaseUrl?: string;         // Optional custom API endpoint
  features: {                  // Feature flags for this brand
    [key: string]: boolean;
  };
  assets: {                    // Brand-specific assets
    logoUrl: ImageSourcePropType;
    splashBackground: ImageSourcePropType;
    [key: string]: ImageSourcePropType;
  };
  [key: string]: any;          // Additional brand-specific properties
}
 
// Define which theme properties can be overridden by brands
export interface ThemeOverrides {
  colors?: {
    [key: string]: string;     // Color overrides
  };
  radius?: {
    [key: string]: number;     // Border radius overrides
  };
  spacing?: {
    [key: string]: number;     // Spacing overrides
  };
}

White-Label Store

The white-label configuration is managed through a central store:

// core/domains/white-label/store.ts
import { create } from 'zustand';
import { defaultBrandConfig } from './configs/default';
import type { BrandConfig } from './types';
 
interface WhiteLabelState {
  brandConfig: BrandConfig;
  setBrandConfig: (config: BrandConfig) => void;
}
 
export const whiteLabelStore = create<WhiteLabelState>((set) => ({
  brandConfig: defaultBrandConfig,
  setBrandConfig: (config) => set({ brandConfig: config }),
}));

White-Label Service

A service layer handles brand switching and persistence:

// core/domains/white-label/service.ts
import { storage } from '@/core/shared/utils/storage';
import { whiteLabelStore } from './store';
import { ThemeLoader } from './ThemeLoader';
import type { BrandConfig } from './types';
 
export class WhiteLabelService {
  /**
   * Switch to a different brand configuration
   */
  static async switchBrand(brandId: string): Promise<boolean> {
    try {
      // Load brand configuration
      const brandConfig = await ThemeLoader.loadBrandConfig(brandId);
      
      if (!brandConfig) {
        console.error(`Brand config not found for ID: ${brandId}`);
        return false;
      }
      
      // Update store with new brand config
      whiteLabelStore.setBrandConfig(brandConfig);
      
      // Persist selection
      await storage.setString('selectedBrandId', brandId);
      
      return true;
    } catch (error) {
      console.error('Failed to switch brand:', error);
      return false;
    }
  }
  
  /**
   * Check if a feature is enabled for the current brand
   */
  static isFeatureEnabled(featureKey: string): boolean {
    const { brandConfig } = whiteLabelStore.getState();
    return !!brandConfig.features[featureKey];
  }
  
  /**
   * Get asset for the current brand
   */
  static getAsset(assetKey: keyof BrandConfig['assets']) {
    const { brandConfig } = whiteLabelStore.getState();
    return brandConfig.assets[assetKey];
  }
}

Theme Loader

A specialized loader handles loading brand configurations from different sources:

// core/domains/white-label/ThemeLoader.ts
import { storage } from '@/core/shared/utils/storage';
import { defaultBrandConfig } from './configs/default';
import type { BrandConfig } from './types';
 
export class ThemeLoader {
  // Cache for loaded brand configurations
  private static brandCache: Record<string, BrandConfig> = {};
  
  /**
   * Load a brand configuration by ID
   */
  static async loadBrandConfig(brandId?: string): Promise<BrandConfig> {
    const id = brandId || 'default';
    
    // Check cache first
    if (this.brandCache[id]) {
      return this.brandCache[id];
    }
    
    try {
      // Try to load from bundled configs first
      const bundledConfig = await this.loadBundledConfig(id);
      if (bundledConfig) {
        this.brandCache[id] = bundledConfig;
        return bundledConfig;
      }
      
      // Try to load from remote source
      const remoteConfig = await this.loadRemoteConfig(id);
      if (remoteConfig) {
        this.brandCache[id] = remoteConfig;
        return remoteConfig;
      }
      
      // Fallback to default
      return defaultBrandConfig;
    } catch (error) {
      console.error(`Error loading brand config for ${id}:`, error);
      return defaultBrandConfig;
    }
  }
  
  /**
   * Load bundled configuration
   */
  private static async loadBundledConfig(brandId: string): Promise<BrandConfig | null> {
    try {
      // Dynamic import of bundled configuration
      const { default: config } = await import(`./configs/${brandId}`);
      return config || null;
    } catch (error) {
      return null;
    }
  }
  
  /**
   * Load configuration from remote source
   */
  private static async loadRemoteConfig(brandId: string): Promise<BrandConfig | null> {
    try {
      // Check for cached remote config
      const cachedConfig = await storage.getObject(`brand_config_${brandId}`);
      if (cachedConfig) {
        return cachedConfig as BrandConfig;
      }
      
      // Fetch from remote API
      const response = await fetch(`https://api.example.com/brands/${brandId}/config`);
      if (!response.ok) {
        return null;
      }
      
      const config = await response.json();
      
      // Cache for future use
      await storage.setObject(`brand_config_${brandId}`, config);
      
      return config;
    } catch (error) {
      return null;
    }
  }
}

Integration with Theme Provider

To integrate white-label themes with the theme system, modify the ThemeProvider:

// core/shared/styles/ThemeProvider.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { lightTheme, darkTheme } from './theme';
import { textStyles } from './typography';
import { useWhiteLabelConfig } from '@/core/domains/white-label/hooks';
 
// ... existing theme context code ...
 
export function ThemeProvider({ children }: { children: React.ReactNode }) {
  // Get device color scheme
  const deviceColorScheme = useColorScheme();
  
  // Get white-label configuration
  const { brandTheme } = useWhiteLabelConfig();
  
  // Theme mode preference (light, dark, or system)
  const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>('system');
  
  // Determine if dark mode is active
  const isDark = React.useMemo(() => {
    if (themeMode === 'system') {
      return deviceColorScheme === 'dark';
    }
    return themeMode === 'dark';
  }, [themeMode, deviceColorScheme]);
  
  // Get the base color theme based on dark/light
  const baseColorTheme = React.useMemo(() => {
    return isDark ? darkTheme : lightTheme;
  }, [isDark]);
  
  // Merge with brand theme
  const theme = React.useMemo(() => {
    // Start with base theme
    const mergedTheme = {
      ...baseColorTheme,
      textStyles,
    };
    
    // Apply brand overrides if available
    if (brandTheme) {
      if (brandTheme.colors) {
        mergedTheme.colors = {
          ...mergedTheme.colors,
          ...brandTheme.colors,
        };
      }
      
      if (brandTheme.radius) {
        mergedTheme.radius = {
          ...mergedTheme.radius,
          ...brandTheme.radius,
        };
      }
      
      if (brandTheme.spacing) {
        mergedTheme.spacing = {
          ...mergedTheme.spacing,
          ...brandTheme.spacing,
        };
      }
    }
    
    return mergedTheme;
  }, [baseColorTheme, brandTheme]);
  
  // ... rest of the ThemeProvider code ...
}

White-Label Hooks

Create hooks for accessing white-label configuration in components:

// core/domains/white-label/hooks.ts
import { useEffect, useState } from 'react';
import { whiteLabelStore } from './store';
import { WhiteLabelService } from './service';
import type { BrandConfig } from './types';
 
/**
 * Hook to access the current brand configuration
 */
export function useWhiteLabelConfig() {
  // Get current state from store
  const brandConfig = whiteLabelStore((state) => state.brandConfig);
  
  return {
    brandId: brandConfig.id,
    brandName: brandConfig.name,
    brandTheme: brandConfig.theme,
    apiBaseUrl: brandConfig.apiBaseUrl,
    assets: brandConfig.assets,
    isFeatureEnabled: (feature: string) => {
      return !!brandConfig.features[feature];
    },
    switchBrand: WhiteLabelService.switchBrand,
  };
}
 
/**
 * Hook to access available brand configurations for selection
 */
export function useAvailableBrands() {
  const [brands, setBrands] = useState<{ id: string; name: string }[]>([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    async function loadBrands() {
      try {
        // In a real implementation, this would fetch from an API or config file
        const availableBrands = [
          { id: 'default', name: 'Default Brand' },
          { id: 'brandA', name: 'Brand A' },
          { id: 'brandB', name: 'Brand B' },
        ];
        
        setBrands(availableBrands);
        setLoading(false);
      } catch (error) {
        console.error('Failed to load available brands:', error);
        setLoading(false);
      }
    }
    
    loadBrands();
  }, []);
  
  return { brands, loading };
}

Usage Patterns

Brand-Specific Components

Components can consume the brand configuration to render brand-specific UI:

// ui/components/BrandLogo.tsx
import React from 'react';
import { Image, StyleSheet, ImageStyle } from 'react-native';
import { useWhiteLabelConfig } from '@/core/domains/white-label/hooks';
 
interface BrandLogoProps {
  size?: 'small' | 'medium' | 'large';
  style?: ImageStyle;
}
 
export function BrandLogo({ size = 'medium', style }: BrandLogoProps) {
  const { assets } = useWhiteLabelConfig();
  
  // Determine logo size
  const getSize = () => {
    switch (size) {
      case 'small':
        return { width: 80, height: 40 };
      case 'large':
        return { width: 240, height: 120 };
      default:
        return { width: 160, height: 80 };
    }
  };
  
  return (
    <Image
      source={assets.logoUrl}
      style={[getSize(), styles.logo, style]}
      resizeMode="contain"
    />
  );
}
 
const styles = StyleSheet.create({
  logo: {
    // Any default styles
  },
});

Brand Switcher UI

Create a UI component to allow users to switch between brands:

// features/settings/BrandSwitcher.tsx
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Image } from 'react-native';
import { useWhiteLabelConfig, useAvailableBrands } from '@/core/domains/white-label/hooks';
 
export function BrandSwitcher() {
  const { brandId, switchBrand } = useWhiteLabelConfig();
  const { brands, loading } = useAvailableBrands();
  const [switching, setSwitching] = useState(false);
  
  const handleBrandSwitch = async (id: string) => {
    if (id === brandId || switching) return;
    
    setSwitching(true);
    await switchBrand(id);
    setSwitching(false);
  };
  
  if (loading) {
    return <Text>Loading brands...</Text>;
  }
  
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Select Brand</Text>
      
      {brands.map((brand) => (
        <TouchableOpacity
          key={brand.id}
          style={[styles.brandItem, brand.id === brandId && styles.selectedBrand]}
          onPress={() => handleBrandSwitch(brand.id)}
          disabled={switching}
        >
          <Text style={styles.brandName}>{brand.name}</Text>
          {brand.id === brandId && (
            <Text style={styles.activeText}>Active</Text>
          )}
        </TouchableOpacity>
      ))}
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    padding: 16,
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  brandItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 12,
    borderRadius: 8,
    marginBottom: 8,
    backgroundColor: '#f0f0f0',
  },
  selectedBrand: {
    backgroundColor: '#e0f7e0',
    borderWidth: 1,
    borderColor: '#00AA5B',
  },
  brandName: {
    fontSize: 16,
  },
  activeText: {
    color: '#00AA5B',
    fontWeight: 'bold',
  },
});

Feature Flag Usage

Use white-label feature flags to conditionally enable functionality:

// features/payments/screens/PaymentsScreen.tsx
import React from 'react';
import { View, Text } from 'react-native';
import { useWhiteLabelConfig } from '@/core/domains/white-label/hooks';
import { PaymentForm } from '../components/PaymentForm';
import { FeatureDisabled } from '@/ui/components/FeatureDisabled';
 
export function PaymentsScreen() {
  const { isFeatureEnabled } = useWhiteLabelConfig();
  
  // Check if payments are enabled for this brand
  if (!isFeatureEnabled('enablePayments')) {
    return (
      <FeatureDisabled 
        title="Payments Unavailable"
        message="Payment processing is not available for your account."
      />
    );
  }
  
  return (
    <View>
      <PaymentForm />
    </View>
  );
}

Animation Strategies

Implement smooth animations when switching between brands:

// core/shared/styles/AnimatedThemeProvider.tsx
import React, { useEffect, useState } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { ThemeProvider } from './ThemeProvider';
import { useWhiteLabelConfig } from '@/core/domains/white-label/hooks';
 
interface Props {
  children: React.ReactNode;
}
 
export function AnimatedThemeProvider({ children }: Props) {
  const { brandId } = useWhiteLabelConfig();
  const [prevBrandId, setPrevBrandId] = useState(brandId);
  const [isTransitioning, setIsTransitioning] = useState(false);
  const fadeAnim = useState(new Animated.Value(1))[0];
  
  useEffect(() => {
    // Brand has changed, animate transition
    if (brandId !== prevBrandId) {
      setIsTransitioning(true);
      
      // Fade out
      Animated.timing(fadeAnim, {
        toValue: 0,
        duration: 200,
        useNativeDriver: true,
      }).start(() => {
        // Update previous brand ID after fade out
        setPrevBrandId(brandId);
        
        // Fade in with new theme
        Animated.timing(fadeAnim, {
          toValue: 1,
          duration: 300,
          useNativeDriver: true,
        }).start(() => {
          setIsTransitioning(false);
        });
      });
    }
  }, [brandId, prevBrandId, fadeAnim]);
  
  return (
    <ThemeProvider>
      <Animated.View style={[styles.container, { opacity: fadeAnim }]}>
        {children}
      </Animated.View>
    </ThemeProvider>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

Best Practices & Security

Performance Considerations

  • (Do ✅) Cache brand configurations

    • Store loaded brand configs in memory cache
    • Persist to device storage for faster startup
  • (Do ✅) Lazy load brand-specific assets

    • Only load assets for the current brand
    • Consider implementing asset preloading for faster switching
  • (Do ✅) Optimize theme merging

    • Use memoization to prevent unnecessary re-renders
    • Only merge the specific properties that differ
  • (Don't ❌) Load all brand assets at startup

    • This increases bundle size and startup time
    • Instead, load dynamically as needed

Troubleshooting

Common Issues and Solutions

IssuePossible CauseSolution
Brand styles not applyingIncorrect theme mergingVerify theme merging logic in ThemeProvider
Missing brand assetsIncorrect path or bundling issueCheck asset paths and bundle configuration
Brand selection not persistingStorage issueVerify AsyncStorage implementation and permissions
Feature flags not workingIncorrect flag namingEnsure feature flag names match between code and config
Slow brand switchingLarge asset filesOptimize asset sizes and implement progressive loading

Core References

Architecture Documents

UI & Assets

Summary

Implementing white-label themes enables your application to dynamically adapt its visual identity based on different brand configurations. Key implementation points include:

  1. Creating a standardized brand configuration structure
  2. Loading brand configurations during the Pre-UI initialization stage
  3. Integrating with the core theme system through theme merging
  4. Providing hooks and utilities for accessing brand-specific data
  5. Implementing smooth animations for brand switching

By following these patterns, you can create a flexible, maintainable white-label solution that supports multiple brands within a single codebase.