Table of Contents
1. Introduction
In today's mobile app development landscape, businesses often need to serve multiple user segments with different requirements, features, and configurations—all from a single codebase. This is where multi-persona apps come into play.
This comprehensive guide will walk you through building a multi-persona React Native application that supports five distinct personas, each with unique bundle identifiers, features, and conditional dependencies. We'll cover both iOS and Android implementations, demonstrating how to maintain a single codebase while delivering tailored experiences.
- ✅ Single codebase for easier maintenance
- ✅ Smaller app sizes (only include what's needed)
- ✅ Better performance (no unused libraries)
- ✅ Clear separation of features per persona
- ✅ Flexible dependency management
2. What is a Multi-Persona App?
A multi-persona app is a single application codebase that can be built into multiple distinct apps, each targeting different user segments or markets. Each persona has:
- Unique Bundle Identifier: Allows multiple variants to be installed simultaneously
- Custom Configuration: Different app names, icons, and settings
- Conditional Features: Some personas may have features others don't
- Selective Dependencies: Only include libraries needed for that persona
Example: Our Five Personas
| Persona | Bundle ID | Display Name | Special Features |
|---|---|---|---|
| US Commercial | com.example.myapp | MyApp | Standard features |
| VA Veterans | com.example.myapp.va | MyApp VA | Terra SDK integration |
| EU Commercial | com.example.myapp.eu | MyApp | EU compliance |
| Clinical | com.example.myapp.clinical | MyApp Clinical | Terra SDK integration |
| White Label | com.example.myapp.partner | MyApp | Partner customization |
3. Architecture Overview
Our multi-persona architecture operates at three levels:
- Native Layer (iOS/Android): Build configurations, bundle IDs, and conditional native dependencies
- React Native Layer: Runtime persona detection and conditional feature loading
- Build System: Scripts and automation for building different personas
Implementation Strategy
┌─────────────────────────────────────────┐
│ React Native Layer │
│ - Persona detection utility │
│ - Conditional feature loading │
│ - Dynamic imports │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ iOS Native Layer │
│ - Xcode Schemes (5 schemes) │
│ - Conditional Pod dependencies │
│ - Build settings per scheme │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Android Native Layer │
│ - Product Flavors (5 flavors) │
│ - Flavor-specific dependencies │
│ - BuildConfig fields │
└─────────────────────────────────────────┘
4. iOS Implementation
iOS uses Xcode Schemes to create different build configurations for each persona. Each scheme has its own bundle identifier and can include or exclude specific dependencies.
Step 1: Create Xcode Schemes
For each persona, create a separate scheme in Xcode:
MyApp-US-CommercialMyApp-VA-VeteransMyApp-EUMyApp-ClinicalMyApp-WhiteLabel
Step 2: Configure Build Settings
For each scheme, set user-defined build settings:
// Example: MyApp-VA-Veterans Scheme
PERSONA_BUNDLE_IDENTIFIER = com.example.myapp.va
PERSONA_APP_VERSION_TYPE = va_veterans
PERSONA_DISPLAY_NAME = MyApp VA
Step 3: Conditional Pod Dependencies
Update your Podfile to conditionally include dependencies based on the persona:
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
# Check if Terra should be included
enable_terra = ENV['ENABLE_TERRA'] == 'YES' ||
ENV['PERSONA_APP_VERSION_TYPE'] == 'va_veterans' ||
ENV['PERSONA_APP_VERSION_TYPE'] == 'clinical'
platform :ios, '15.1'
target 'MyApp' do
use_expo_modules!
# ... standard React Native pods ...
# Conditionally include Terra SDK
if enable_terra
pod 'terra-react', :path => '../node_modules/terra-react'
end
post_install do |installer|
react_native_post_install(installer, config[:reactNativePath])
end
end
Step 4: Native Module for Persona Detection
Create a native module to expose the persona type to React Native:
// AppVersionTypeModule.swift
import Foundation
import React
@objc(AppVersionTypeModule)
class AppVersionTypeModule: NSObject {
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
@objc
func getVersionType(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
if let versionType = Bundle.main.object(forInfoDictionaryKey: "APP_VERSION_TYPE") as? String {
resolve(versionType)
} else {
reject("VERSION_TYPE_ERROR", "Failed to get version type", nil)
}
}
}
Step 5: Setup Script
Create a script to automate iOS setup for each persona:
#!/bin/bash
# setup-ios-terra.sh
SCHEME_NAME="${1:-MyApp-US-Commercial}"
cd ios
# Determine if Terra should be enabled
case "$SCHEME_NAME" in
"MyApp-VA-Veterans"|"MyApp-Clinical")
export ENABLE_TERRA=YES
echo "✓ Terra SDK will be ENABLED"
;;
*)
export ENABLE_TERRA=NO
echo "✗ Terra SDK will be DISABLED"
;;
esac
pod install
echo "Setup complete! Open Xcode and select scheme: $SCHEME_NAME"
./scripts/setup-ios-terra.sh MyApp-VA-Veterans
./scripts/setup-ios-terra.sh MyApp-US-Commercial
5. Android Implementation
Android uses Product Flavors to create different build variants. Each flavor has its own application ID and can include flavor-specific dependencies.
Step 1: Configure Product Flavors
Update android/app/build.gradle:
android {
flavorDimensions "version"
productFlavors {
usCommercial {
dimension "version"
applicationId "com.example.myapp"
buildConfigField "String", "APP_VERSION_TYPE", "\"us_commercial\""
resValue "string", "app_name", "MyApp"
}
vaVeterans {
dimension "version"
applicationId "com.example.myapp.va"
buildConfigField "String", "APP_VERSION_TYPE", "\"va_veterans\""
resValue "string", "app_name", "MyApp VA"
}
euCommercial {
dimension "version"
applicationId "com.example.myapp.eu"
buildConfigField "String", "APP_VERSION_TYPE", "\"eu_commercial\""
resValue "string", "app_name", "MyApp"
}
clinical {
dimension "version"
applicationId "com.example.myapp.clinical"
buildConfigField "String", "APP_VERSION_TYPE", "\"clinical\""
resValue "string", "app_name", "MyApp Clinical"
}
whiteLabel {
dimension "version"
applicationId "com.example.myapp.partner"
buildConfigField "String", "APP_VERSION_TYPE", "\"white_label\""
resValue "string", "app_name", "MyApp"
}
}
}
Step 2: Flavor-Specific Dependencies
Add conditional dependencies in the same build.gradle file:
dependencies {
// Standard dependencies for all flavors
implementation("com.facebook.react:react-android")
// ... other common dependencies ...
// Conditional Terra SDK - only for VA and Clinical flavors
vaVeteransImplementation(project(':terra-react')) {
exclude group: 'com.facebook.react', module: 'react-native'
}
clinicalImplementation(project(':terra-react')) {
exclude group: 'com.facebook.react', module: 'react-native'
}
}
Step 3: Native Module for Persona Detection
Create a Kotlin module to expose the persona type:
// AppVersionTypeModule.kt
package com.example.myapp
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.facebook.react.module.annotations.ReactModule
@ReactModule(name = "AppVersionTypeModule")
class AppVersionTypeModule(reactContext: ReactApplicationContext)
: ReactContextBaseJavaModule(reactContext) {
override fun getName(): String {
return "AppVersionTypeModule"
}
@ReactMethod
fun getVersionType(promise: Promise) {
try {
val versionType = BuildConfig.APP_VERSION_TYPE
promise.resolve(versionType)
} catch (e: Exception) {
promise.reject("VERSION_TYPE_ERROR", "Failed to get version type", e)
}
}
}
Step 4: Register the Module
Register the module in MainApplication.kt:
override fun getPackages(): List =
PackageList(this).packages.apply {
add(BleConnectionPackage())
add(AppVersionTypePackage())
}
Step 5: Build Commands
# Debug builds
./gradlew assembleVaVeteransDebug
./gradlew assembleClinicalDebug
./gradlew assembleUsCommercialDebug
# Release builds
./gradlew assembleVaVeteransRelease
./gradlew assembleClinicalRelease
./gradlew assembleUsCommercialRelease
6. React Native Layer
The React Native layer provides runtime persona detection and conditional feature loading.
Step 1: Create Persona Utility Module
// src/utils/personaModules.ts
import AppVersionTypeModule from '@/native/AppVersionTypeModule';
let appVersionType: string | null = null;
/**
* Get the app version type (persona) for the current build
*/
export const getAppVersionType = async (): Promise => {
if (!appVersionType) {
appVersionType = await AppVersionTypeModule.getVersionType();
}
return appVersionType;
};
/**
* Check if the current persona supports Terra SDK
* Terra SDK is only available for VA Veterans and Clinical personas
*/
export const supportsTerra = async (): Promise => {
const versionType = await getAppVersionType();
return versionType === 'va_veterans' || versionType === 'clinical';
};
/**
* Check if the current persona supports a specific feature
*/
export const supportsFeature = async (featureName: string): Promise => {
const versionType = await getAppVersionType();
const featureMatrix: Record = {
terra: ['va_veterans', 'clinical'],
analytics: ['us_commercial', 'eu_commercial'],
// Add more features as needed
};
const supportedPersonas = featureMatrix[featureName];
return supportedPersonas ? supportedPersonas.includes(versionType) : false;
};
Step 2: Conditional Feature Loading
Use the persona utility to conditionally load features:
// src/context/appProvider/AppProvider.tsx
import { useEffect, useState } from 'react';
import { supportsTerra } from '@/utils/personaModules';
export const AppProvider = ({ children }) => {
const [enableTerra, setEnableTerra] = useState(false);
useEffect(() => {
supportsTerra().then(setEnableTerra);
}, []);
if (enableTerra) {
// Dynamically import Terra only when needed
const { TerraProvider } = require('src/context/appContexts/terraContext');
return (
{children}
);
}
return (
{children}
);
};
Step 3: TypeScript Interface
// src/native/AppVersionTypeModule.ts
import { requireNativeModule } from 'expo-modules-core';
interface AppVersionTypeModuleInterface {
getVersionType(): Promise;
}
const AppVersionTypeModule = requireNativeModule(
'AppVersionTypeModule',
);
export default AppVersionTypeModule as AppVersionTypeModuleInterface;
7. Conditional Dependencies
One of the key benefits of multi-persona apps is the ability to include dependencies only where needed. This reduces app size and improves performance.
Example: Terra SDK
In our example, Terra SDK is only needed for VA Veterans and Clinical personas. Here's how we handle it:
iOS Conditional Dependencies
# Podfile
enable_terra = ENV['ENABLE_TERRA'] == 'YES' ||
ENV['PERSONA_APP_VERSION_TYPE'] == 'va_veterans' ||
ENV['PERSONA_APP_VERSION_TYPE'] == 'clinical'
target 'MyApp' do
# ... other pods ...
if enable_terra
pod 'terra-react', :path => '../node_modules/terra-react'
end
end
Android Conditional Dependencies
// build.gradle
dependencies {
// ... common dependencies ...
// Terra SDK only for specific flavors
vaVeteransImplementation(project(':terra-react'))
clinicalImplementation(project(':terra-react'))
}
React Native Conditional Loading
// Use dynamic imports to avoid bundling unused code
const loadTerraFeature = async () => {
if (await supportsTerra()) {
const TerraModule = await import('terra-react');
return TerraModule;
}
return null;
};
8. Best Practices
1. Centralize Persona Configuration
Keep all persona-specific configurations in one place. Use constants or configuration files to manage persona settings.
2. Use Build Scripts
Automate the build process with scripts. This reduces errors and makes it easier for team members to build different personas.
#!/bin/bash
# build-all-personas.sh
PERSONAS=(
"usCommercial:MyApp-US-Commercial"
"vaVeterans:MyApp-VA-Veterans"
"euCommercial:MyApp-EU"
"clinical:MyApp-Clinical"
"whiteLabel:MyApp-WhiteLabel"
)
for persona in "${PERSONAS[@]}"; do
IFS=':' read -r android_flavor ios_scheme <<< "$persona"
echo "Building: $android_flavor / $ios_scheme"
# Build commands...
done
3. Verify Configuration
Create verification scripts to ensure configurations are correct:
#!/bin/bash
# verify-conditional-deps.sh
echo "Verifying conditional dependencies..."
# Check iOS Podfile
if grep -q "if enable_terra" ios/Podfile; then
echo "✓ iOS Podfile configured correctly"
else
echo "✗ iOS Podfile needs configuration"
fi
# Check Android build.gradle
if grep -q "vaVeteransImplementation.*terra" android/app/build.gradle; then
echo "✓ Android build.gradle configured correctly"
else
echo "✗ Android build.gradle needs configuration"
fi
4. Document Persona Differences
Maintain clear documentation about which features and dependencies belong to which persona. This helps prevent confusion and errors.
5. Test Each Persona Independently
Each persona should be tested as a separate app. Don't assume that testing one persona covers all scenarios.
6. Use Feature Flags
Combine persona detection with feature flags for even more flexibility:
const shouldShowFeature = async (featureName: string) => {
const persona = await getAppVersionType();
const featureFlag = await getFeatureFlag(featureName);
return supportsFeature(featureName) && featureFlag;
};
7. Monitor App Sizes
Regularly check the app size for each persona. Conditional dependencies should result in smaller apps for personas that don't need heavy libraries.
9. Conclusion
Building a multi-persona React Native app requires careful planning and implementation across multiple layers:
- Native Layer: Configure build settings, bundle IDs, and conditional dependencies
- React Native Layer: Implement runtime persona detection and conditional feature loading
- Build System: Create scripts to automate building different personas
The approach we've outlined provides:
- ✅ Single codebase for easier maintenance
- ✅ Smaller app sizes through conditional dependencies
- ✅ Better performance by excluding unused libraries
- ✅ Clear separation of features per persona
- ✅ Flexibility to add new personas or features
By following this guide, you can successfully build and maintain a multi-persona React Native application that serves different user segments while keeping your codebase manageable and your apps optimized.
Next Steps
- Set up your iOS schemes and Android product flavors
- Create the persona detection native modules
- Implement the React Native persona utility
- Configure conditional dependencies
- Create build and verification scripts
- Test each persona thoroughly
About the Author: This guide is based on real-world implementation of a multi-persona React Native app supporting five distinct personas with conditional dependencies and feature sets.
Tags: React Native, iOS, Android, Multi-Persona Apps, Mobile Development, Build Configuration, Product Flavors, Xcode Schemes