UTA DevHub
UI Development/Splash Screen Guide

White-Label Support

Implementing splash screens for multi-brand applications with build-time and runtime configurations

White-Label Support

Overview

White-label applications require different branding assets and configurations based on the brand or client. This guide provides comprehensive strategies for implementing splash screens that support multiple brands with minimal code duplication.

White-Label Considerations

When building multi-brand apps, splash screens need to:

  • Display brand-specific logos and colors
  • Support different configurations per brand
  • Maintain a single codebase for easier maintenance
  • Allow for both build-time and runtime brand selection

Configuration Strategies

Build-Time vs Runtime

Build-Time Configuration

Each brand gets its own build with embedded assets:

Advantages:

  • Native splash screens work perfectly
  • Smaller app size per brand
  • Better performance (no runtime decisions)
  • More secure (assets are embedded)

Disadvantages:

  • Multiple builds to maintain
  • Longer deployment process
  • App store submissions per brand

When to use:

  • Distributing through app stores
  • Brands have very different features
  • Security is paramount

Build-Time Implementation

Asset Organization

assets/
├── brands/
│   ├── brand-a/
│   │   ├── splash-logo.png
│   │   ├── splash-logo@2x.png
│   │   ├── splash-logo@3x.png
│   │   └── config.json
│   ├── brand-b/
│   │   ├── splash-logo.png
│   │   ├── splash-logo@2x.png
│   │   ├── splash-logo@3x.png
│   │   └── config.json
│   └── brand-c/
│       └── ...
└── splash/
    └── ... (generated assets)

Generating Brand Assets

Create a script to generate assets for each brand:

#!/bin/bash
# scripts/generate-splash-screens.sh
 
BRANDS=("brand-a" "brand-b" "brand-c")
 
for BRAND in "${BRANDS[@]}"; do
  echo "Generating splash screen for $BRAND..."
  
  # Read brand config
  CONFIG_FILE="assets/brands/$BRAND/config.json"
  BG_COLOR=$(jq -r '.splashBackgroundColor' $CONFIG_FILE)
  LOGO_WIDTH=$(jq -r '.splashLogoWidth' $CONFIG_FILE)
  
  # Generate assets
  npx react-native-bootsplash generate \
    --assets-path="assets/splash/$BRAND" \
    --background-color="$BG_COLOR" \
    --logo-width="$LOGO_WIDTH" \
    --flavor="$BRAND" \
    "assets/brands/$BRAND/splash-logo.png"
done

Android Build Flavors

Configure product flavors in android/app/build.gradle:

android {
  flavorDimensions "brand"
  
  productFlavors {
    brandA {
      dimension "brand"
      applicationId "com.company.branda"
      resValue "string", "app_name", "Brand A"
      buildConfigField "String", "BRAND_ID", '"brand-a"'
    }
    
    brandB {
      dimension "brand"
      applicationId "com.company.brandb"
      resValue "string", "app_name", "Brand B"
      buildConfigField "String", "BRAND_ID", '"brand-b"'
    }
    
    brandC {
      dimension "brand"
      applicationId "com.company.brandc"
      resValue "string", "app_name", "Brand C"
      buildConfigField "String", "BRAND_ID", '"brand-c"'
    }
  }
}

iOS Build Schemes

Create Build Configurations

In Xcode:

  1. Select your project
  2. Go to Info tab
  3. Duplicate configurations for each brand
  4. Name them: Debug-BrandA, Release-BrandA, etc.

Create Schemes

  1. Go to Product > Scheme > Manage Schemes
  2. Duplicate the default scheme for each brand
  3. Edit each scheme to use appropriate build configuration
  4. Set environment variables or preprocessor macros

Configure Info.plist

Use build settings to vary values:

<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

JavaScript Configuration

Use react-native-config or similar to load brand config:

// config/brand.ts
import Config from 'react-native-config';
 
export interface BrandConfig {
  id: string;
  name: string;
  colors: {
    primary: string;
    secondary: string;
    background: string;
  };
  api: {
    baseUrl: string;
    apiKey: string;
  };
}
 
// Load config based on build flavor
export const BRAND_CONFIG: BrandConfig = {
  id: Config.BRAND_ID,
  name: Config.BRAND_NAME,
  colors: {
    primary: Config.PRIMARY_COLOR,
    secondary: Config.SECONDARY_COLOR,
    background: Config.BACKGROUND_COLOR,
  },
  api: {
    baseUrl: Config.API_BASE_URL,
    apiKey: Config.API_KEY,
  },
};

Runtime Implementation

Brand Manager

Create a centralized brand management system:

// core/shared/brands/BrandManager.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
 
export type BrandId = 'brand-a' | 'brand-b' | 'brand-c';
 
interface BrandAssets {
  splashLogo: any; // Using require() returns number
  appLogo: any;
  primaryColor: string;
  secondaryColor: string;
}
 
export class BrandManager {
  private static currentBrand: BrandId | null = null;
  
  private static brands: Record<BrandId, BrandAssets> = {
    'brand-a': {
      splashLogo: require('@/assets/brands/brand-a/splash-logo.png'),
      appLogo: require('@/assets/brands/brand-a/logo.png'),
      primaryColor: '#FF5500',
      secondaryColor: '#FF8800',
    },
    'brand-b': {
      splashLogo: require('@/assets/brands/brand-b/splash-logo.png'),
      appLogo: require('@/assets/brands/brand-b/logo.png'),
      primaryColor: '#0055FF',
      secondaryColor: '#0088FF',
    },
    'brand-c': {
      splashLogo: require('@/assets/brands/brand-c/splash-logo.png'),
      appLogo: require('@/assets/brands/brand-c/logo.png'),
      primaryColor: '#00AA55',
      secondaryColor: '#00DD88',
    },
  };
  
  /**
   * Initialize brand manager
   */
  static async initialize(): Promise<void> {
    // Try to load saved brand
    const savedBrand = await AsyncStorage.getItem('selectedBrand');
    if (savedBrand && this.isValidBrand(savedBrand)) {
      this.currentBrand = savedBrand as BrandId;
    }
  }
  
  /**
   * Set the current brand
   */
  static async setBrand(brandId: BrandId): Promise<void> {
    if (!this.isValidBrand(brandId)) {
      throw new Error(`Invalid brand ID: ${brandId}`);
    }
    
    this.currentBrand = brandId;
    await AsyncStorage.setItem('selectedBrand', brandId);
  }
  
  /**
   * Get current brand assets
   */
  static getAssets(): BrandAssets {
    if (!this.currentBrand) {
      throw new Error('Brand not initialized');
    }
    
    return this.brands[this.currentBrand];
  }
  
  /**
   * Get specific brand assets
   */
  static getBrandAssets(brandId: BrandId): BrandAssets {
    return this.brands[brandId];
  }
  
  /**
   * Check if brand ID is valid
   */
  private static isValidBrand(brandId: string): boolean {
    return Object.keys(this.brands).includes(brandId);
  }
}

Branded Splash Screen Component

// core/shared/splash-screen/BrandedSplashScreen.tsx
import React, { useEffect, useState } from 'react';
import { 
  View, 
  Image, 
  StyleSheet, 
  Animated,
  StatusBar,
  ActivityIndicator 
} from 'react-native';
import { BrandManager } from '@/core/shared/brands/BrandManager';
import { SplashScreen } from './SplashScreen';
 
interface Props {
  onReady: () => void;
}
 
export function BrandedSplashScreen({ onReady }: Props) {
  const [fadeAnim] = useState(new Animated.Value(1));
  const [assets, setAssets] = useState<any>(null);
  
  useEffect(() => {
    async function loadBrand() {
      try {
        // Initialize brand manager
        await BrandManager.initialize();
        
        // Get brand assets
        const brandAssets = BrandManager.getAssets();
        setAssets(brandAssets);
        
        // Hide native splash quickly
        await SplashScreen.hide({ fade: true, duration: 100 });
      } catch (error) {
        console.error('Failed to load brand:', error);
        // Use default brand or handle error
      }
    }
    
    loadBrand();
  }, []);
  
  useEffect(() => {
    if (assets) {
      // Show branded splash for minimum time
      setTimeout(() => {
        Animated.timing(fadeAnim, {
          toValue: 0,
          duration: 300,
          useNativeDriver: true,
        }).start(() => {
          onReady();
        });
      }, 1500);
    }
  }, [assets, fadeAnim, onReady]);
  
  if (!assets) {
    // Show loading while determining brand
    return (
      <View style={[styles.container, styles.loading]}>
        <ActivityIndicator size="large" color="#999" />
      </View>
    );
  }
  
  return (
    <Animated.View 
      style={[
        styles.container, 
        { 
          opacity: fadeAnim,
          backgroundColor: assets.primaryColor 
        }
      ]}
    >
      <StatusBar hidden />
      <Image 
        source={assets.splashLogo} 
        style={styles.logo} 
        resizeMode="contain" 
      />
    </Animated.View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
  },
  loading: {
    backgroundColor: '#FFFFFF',
  },
  logo: {
    width: 200,
    height: 200,
  },
});

Brand Selection Screen

For apps where users select their brand:

// screens/BrandSelectionScreen.tsx
import React from 'react';
import { View, Text, TouchableOpacity, Image, StyleSheet } from 'react-native';
import { BrandManager, BrandId } from '@/core/shared/brands/BrandManager';
 
const AVAILABLE_BRANDS: Array<{ id: BrandId; name: string }> = [
  { id: 'brand-a', name: 'Brand A' },
  { id: 'brand-b', name: 'Brand B' },
  { id: 'brand-c', name: 'Brand C' },
];
 
export function BrandSelectionScreen({ navigation }) {
  const selectBrand = async (brandId: BrandId) => {
    await BrandManager.setBrand(brandId);
    // Restart app or navigate to main screen
    navigation.replace('Main');
  };
  
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Select Your Brand</Text>
      
      {AVAILABLE_BRANDS.map(brand => {
        const assets = BrandManager.getBrandAssets(brand.id);
        
        return (
          <TouchableOpacity
            key={brand.id}
            style={[
              styles.brandCard,
              { borderColor: assets.primaryColor }
            ]}
            onPress={() => selectBrand(brand.id)}
          >
            <Image 
              source={assets.appLogo} 
              style={styles.brandLogo}
              resizeMode="contain"
            />
            <Text style={styles.brandName}>{brand.name}</Text>
          </TouchableOpacity>
        );
      })}
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
    justifyContent: 'center',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 32,
  },
  brandCard: {
    padding: 16,
    marginVertical: 8,
    borderWidth: 2,
    borderRadius: 12,
    alignItems: 'center',
  },
  brandLogo: {
    width: 80,
    height: 80,
    marginBottom: 8,
  },
  brandName: {
    fontSize: 18,
    fontWeight: '600',
  },
});

Hybrid Implementation

Combine native and React splash screens for optimal experience:

// App.tsx
import React, { useEffect, useState } from 'react';
import { SplashScreen } from '@/core/shared/splash-screen/SplashScreen';
import { BrandedSplashScreen } from '@/core/shared/splash-screen/BrandedSplashScreen';
import { BrandManager } from '@/core/shared/brands/BrandManager';
import { initializer } from '@/core/shared/app/initialization';
 
export default function App() {
  const [showBrandedSplash, setShowBrandedSplash] = useState(true);
  const [appReady, setAppReady] = useState(false);
  
  useEffect(() => {
    async function initialize() {
      try {
        // Quick native splash hides automatically when React loads
        
        // Initialize brand
        await BrandManager.initialize();
        
        // Run app initialization
        await initializer.executeStage(InitStage.PRE_UI);
        
        setAppReady(true);
      } catch (error) {
        console.error('Initialization failed:', error);
        setAppReady(true); // Show app anyway
      }
    }
    
    initialize();
  }, []);
  
  if (showBrandedSplash) {
    return (
      <BrandedSplashScreen 
        onReady={() => setShowBrandedSplash(false)}
      />
    );
  }
  
  if (!appReady) {
    return null; // Keep showing nothing while initializing
  }
  
  return <MainApp />;
}

Theming Integration

Integrate splash screen branding with app theming:

// core/shared/theme/ThemeProvider.tsx
import React, { createContext, useContext } from 'react';
import { BrandManager } from '@/core/shared/brands/BrandManager';
 
interface Theme {
  colors: {
    primary: string;
    secondary: string;
    background: string;
    text: string;
  };
  // ... other theme properties
}
 
const ThemeContext = createContext<Theme | null>(null);
 
export function ThemeProvider({ children }) {
  const assets = BrandManager.getAssets();
  
  const theme: Theme = {
    colors: {
      primary: assets.primaryColor,
      secondary: assets.secondaryColor,
      background: '#FFFFFF',
      text: '#000000',
    },
    // ... other theme properties
  };
  
  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
}
 
export const useTheme = () => {
  const theme = useContext(ThemeContext);
  if (!theme) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return theme;
};

Testing White-Label Apps

Automated Testing

// __tests__/white-label.test.ts
import { BrandManager } from '@/core/shared/brands/BrandManager';
import { render } from '@testing-library/react-native';
import { BrandedSplashScreen } from '@/core/shared/splash-screen/BrandedSplashScreen';
 
describe('White-Label Support', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });
  
  it('loads correct assets for each brand', async () => {
    const brands: BrandId[] = ['brand-a', 'brand-b', 'brand-c'];
    
    for (const brandId of brands) {
      await BrandManager.setBrand(brandId);
      const assets = BrandManager.getAssets();
      
      expect(assets).toBeDefined();
      expect(assets.splashLogo).toBeDefined();
      expect(assets.primaryColor).toMatch(/^#[0-9A-F]{6}$/i);
    }
  });
  
  it('renders branded splash screen', async () => {
    await BrandManager.setBrand('brand-a');
    
    const { getByTestId } = render(
      <BrandedSplashScreen onReady={jest.fn()} />
    );
    
    // Add testID to component for testing
    expect(getByTestId('branded-splash')).toBeTruthy();
  });
});

Manual Testing Checklist

  • Test each brand configuration
  • Verify correct assets display
  • Check app store builds
  • Test brand switching (if applicable)
  • Verify theme consistency
  • Test on various devices
  • Check memory usage with all assets

Best Practices

White-Label Best Practices

  1. Consistent Asset Naming: Use the same filenames across brands
  2. Validate Assets: Ensure all brands have required assets
  3. Automate Generation: Script asset generation and testing
  4. Document Brand Configs: Maintain clear documentation
  5. Test Thoroughly: Test each brand configuration

Asset Guidelines

  • (Do ✅) Standardize dimensions across all brand assets
  • (Do ✅) Use consistent file formats (PNG for all)
  • (Do ✅) Optimize file sizes for each brand
  • (Do ✅) Include all required densities/resolutions
  • (Don't ❌) Hardcode brand-specific values in shared code
  • (Consider 🤔) Using SVG for scalable brand assets

Code Organization

src/
├── core/
│   ├── shared/
│   │   ├── brands/        # Brand management
│   │   └── splash-screen/ # Splash screen components
│   └── brand-specific/    # Brand-specific overrides
│       ├── brand-a/
│       ├── brand-b/
│       └── brand-c/
└── features/              # Shared features

Summary

White-label splash screen support requires careful planning:

  • Choose the right strategy: Build-time, runtime, or hybrid
  • Organize assets properly: Consistent structure across brands
  • Automate processes: Asset generation and testing
  • Test thoroughly: Each brand on multiple devices
  • Maintain flexibility: Easy to add new brands

The approach depends on your specific requirements for distribution, brand selection, and maintenance.

Next Steps