@enhancers/react-native-whitelabel
v1.1.3
Published
React Native White Label (RNWL) - A production-ready library for white-label React Native applications with feature flags support
Maintainers
Readme
RNWL - React Native White Label Library
RNWL is a production-ready library for building white-label React Native applications with advanced feature flags support.
🎯 What is RNWL?
RNWL enables you to build a single React Native app that can be branded and configured for multiple clients/brands, with the ability to dynamically enable/disable features per brand.
✨ Features
- 🎯 Feature Flags Management: Enable/disable features dynamically per brand
- ⚛️ React Hooks:
useRNWLFeaturesanduseRNWLColorsfor easy component integration - 🚪 Component Gating:
RNWLGatefor conditional rendering - 🎨 Brand Colors: Per-brand color tokens accessible at runtime via
useRNWLColors() - 🔗 Deep Link Configuration: Custom URL schemes and universal links, configured natively per brand
- 🏷️ Xcode Scheme Renaming: Scheme files and
PRODUCT_NAMEinproject.pbxprojare updated to matchdisplayName, so the correct brand name appears in Xcode's scheme dropdown immediately after running the CLI - 📱 Multi-Platform: iOS, Android, and Web support
- 💪 TypeScript: Fully typed API
- 🔧 Zero Config: Works with sensible defaults
- 🌐 Remote Feature Flags: Load flags from your backend via custom async loader
📦 Installation
npm install @enhancers/react-native-whitelabel
# or
yarn add @enhancers/react-native-whitelabel🚀 Quick Start
1. Wrap your app with RNWLProvider
import { RNWLProvider } from '@enhancers/react-native-whitelabel';
export default function App() {
return (
<RNWLProvider>
<YourApp />
</RNWLProvider>
);
}The provider initializes the feature flags manager once and makes the state available to the entire component tree.
On iOS and Android, features are loaded synchronously from the bundled rnwl.json via require() at module-evaluation time, so isFeatureEnabled is ready before the first component render — no loading flash, no spurious warnings in Metro.
On web and when using a custom featureLoader, loading is asynchronous. Use the isLoading flag from useRNWLFeatures() to guard against rendering before flags are available.
You can pass initialization options via the options prop:
<RNWLProvider options={{ featureLoader: fetchFeaturesFromBackend }}>
<YourApp />
</RNWLProvider>2. Configure Metro
Add the rnwl-config virtual module to your metro.config.js. This tells Metro where to find the rnwl.json generated by the CLI:
const path = require('path');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const config = {
resolver: {
extraNodeModules: {
'rnwl-config': path.resolve(__dirname, 'rnwl.json'),
},
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);This is a one-time setup.
rnwl.jsonis generated bynpx rnwl apply-white-label <brand>at the project root.
3. Use Features in Components
import { useRNWLFeatures, useRNWLColors, RNWLGate } from '@enhancers/react-native-whitelabel';
function MyComponent() {
const { isFeatureEnabled, isLoading, error, brand, displayName, packageName, deeplinkScheme } = useRNWLFeatures();
const { primary, background } = useRNWLColors();
if (isLoading) return <LoadingScreen />;
if (error) return <ErrorScreen />;
return (
<View style={{ backgroundColor: background }}>
{isFeatureEnabled('darkMode') && <DarkModeUI />}
<RNWLGate feature="analytics">
<AnalyticsPanel />
</RNWLGate>
{deeplinkScheme && (
<Text>Share link: {deeplinkScheme}://profile/123</Text>
)}
<Button color={primary} title="Continue" />
</View>
);
}📚 Documentation
- Example Project - Complete example with sample configurations
📁 Project Structure
react-native-whitelabel/
├── src/ # 📦 Main library source
│ ├── config/ # Feature flags manager
│ │ └── rnwlFeatureFlagsManager.ts
│ ├── hooks/ # React hooks & components
│ │ ├── RNWLGate.tsx
│ │ ├── useRNWLColors.ts
│ │ └── useRNWLFeatures.ts
│ └── index.ts # Main entry point
│
├── bin/
│ └── rnwl.js # 🖥️ CLI entry point
│
├── dist/ # 📤 Compiled output (generated)
│
├── example/ # 📋 Example React Native app
│ ├── src/
│ │ ├── App.tsx
│ │ └── screens/
│ │ └── HomeScreen.tsx
│ ├── rnwl-configs/ # Brand configurations
│ │ ├── blue/
│ │ │ ├── config.yml
│ │ │ ├── icons/
│ │ │ ├── ios/
│ │ │ ├── android/
│ │ │ └── assets/
│ │ ├── green/ # (same structure as blue)
│ │ └── red/ # (same structure as blue)
│ └── rnwl.json # Generated by apply-white-label.js
│
├── package.json
├── tsconfig.json
└── README.md🔄 How It Works
- Configure Brands: Create YAML files for each brand in
rnwl-configs/ - Run Build Script: Execute
npx rnwl apply-white-label <brand>to apply the brand configuration - Wrap your app: Mount
<RNWLProvider>at the root — it initializes the manager and provides state to the tree - Use Features: Use
useRNWLFeatures()orRNWLGatein any component
📖 Example Configuration
See example/configs/ for complete examples. Full configuration structure:
# Metadata
brand: my-brand
displayName: "My Brand App"
version: "1.0.0"
# Brand colors and styling
colors:
primary: "#3366FF"
secondary: "#FF6B6B"
# Feature flags (enabled/disabled per brand)
features:
authentication: true
pushNotifications: true
darkMode: false
analytics: true
videoStreaming: false
paymentGateway: true
# Asset handling
ignoreAssets: false # Set to true to skip asset processing (default: false)
# Deep linking — both fields are optional, use one or both
deeplinkScheme: myapp # custom URL scheme → myapp:// (shorthand for a single scheme)
# deeplinkSchemes: # array form — use when multiple schemes are needed (e.g. SDK requirements)
# - myapp
# - sdkscheme
universalLinkDomain: myapp.com # universal/app link → https://myapp.com
# iOS-only entitlements (optional) — any key/value pair written to the .entitlements file
# iosEntitlements:
# com.apple.developer.networking.HotspotConfiguration: true
# com.apple.developer.networking.wifi-info: true🔧 Custom Parameters
Each brand can define arbitrary key/value parameters in config.yml under the params key. Values can be a single scalar or an array.
params:
apiBaseUrl: "https://api.blue.example.com"
supportedLocales:
- "en"
- "it"
- "fr"
maxRetries: 3
debugMode: falseThey are written to rnwl.json and exposed at runtime via useRNWLFeatures():
const { params } = useRNWLFeatures();
const url = params.apiBaseUrl; // "https://api.blue.example.com"
const locales = params.supportedLocales; // ["en", "it", "fr"]Or directly from the manager singleton:
import { rnwlFeatureFlagsManager } from '@enhancers/react-native-whitelabel';
const params = rnwlFeatureFlagsManager.getParams();Supported value types: string, number, boolean, or an array of those.
📖 Asset Handling with ignoreAssets
The ignoreAssets flag controls whether the CLI auto-detects and processes brand assets:
ignoreAssets: false(default): CLI detects and reports on:- Generic icons in
icons/ - iOS-specific icons in
ios/ - Android density-specific icons in
android/ - Additional assets in
assets/
- Generic icons in
ignoreAssets: true: CLI skips all asset detection and processing- Use when managing assets separately in your build pipeline
- Use when assets are handled by native build tools
- Useful for design teams that generate assets independently
🤖 Android Icons
Place icon files in rnwl-configs/<brand>/android/icons/:
| Source file | Destination |
|-------------|-------------|
| ic_launcher-{density}.png | android/app/src/main/res/mipmap-{density}/ic_launcher.png |
| ic_launcher_round-{density}.png | android/app/src/main/res/mipmap-{density}/ic_launcher_round.png |
| ic_launcher_foreground-{density}.png | android/app/src/main/res/mipmap-{density}/ic_launcher_foreground.png |
Supported densities: mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi.
ic_launcher_foreground files are optional and intended for adaptive icons (Android 8.0+, API 26+). If not provided, no foreground file is written. To use adaptive icons correctly:
- Provide
ic_launcher_foreground-{density}.pngfiles designed with the adaptive icon safe zone (108×108dp canvas, 72×72dp safe zone) - Add
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xmlmanually to your project referencing@mipmap/ic_launcher_foregroundand a background layer
Why not auto-generate
ic_launcher_foregroundfromic_launcher? On Android 8.0+ launchers (Samsung, Pixel, etc.) that find aic_launcher_foregroundin the APK, they apply a circular mask to it. A square icon not designed for the safe zone would appear visually cropped.
🤖 Android Splash Screen
Place source files in rnwl-configs/<brand>/android/splash/. No YAML key is required. The ignoreAssets: true flag also skips splash processing.
| Source file | Destination |
|-------------|-------------|
| splashscreen-ldpi.png | android/app/src/main/res/mipmap-ldpi/splashscreen.png |
| splashscreen-mdpi.png | android/app/src/main/res/mipmap-mdpi/splashscreen.png |
| splashscreen-hdpi.png | android/app/src/main/res/mipmap-hdpi/splashscreen.png |
| splashscreen-xhdpi.png | android/app/src/main/res/mipmap-xhdpi/splashscreen.png |
| splashscreen-xxhdpi.png | android/app/src/main/res/mipmap-xxhdpi/splashscreen.png |
| splashscreen-xxxhdpi.png | android/app/src/main/res/mipmap-xxxhdpi/splashscreen.png |
Landscape variants are optional — add splashscreen_land-{density}.png files and they will be copied to mipmap-{density}/splashscreen_land.png.
📄 Android strings.xml
Place a strings.xml file in rnwl-configs/<brand>/android/strings.xml to fully control the Android string resources for that brand. The CLI copies it directly to android/app/src/main/res/values/strings.xml when present.
| Source file | Destination |
|-------------|-------------|
| rnwl-configs/<brand>/android/strings.xml | android/app/src/main/res/values/strings.xml |
If the file is absent in the brand folder it is silently skipped — no error is thrown.
Example
<resources>
<string name="app_name">Blue App</string>
<string name="splash_name">Blue App</string>
</resources>This replaces the entire destination file, so make sure to include all string keys your app references.
🍎 iOS Splash Screen
All iOS splash screen assets are placed in rnwl-configs/<brand>/ios/splash/. No YAML key is required. The ignoreAssets: true flag also skips splash processing.
Imageset assets
These files are copied into the corresponding .imageset directories in Images.xcassets and Contents.json is regenerated automatically based on which files are present:
| Source file | Destination |
|-------------|-------------|
| Splashscreen.png | ios/<App>/Images.xcassets/Splashscreen.imageset/ |
| [email protected] | ios/<App>/Images.xcassets/Splashscreen.imageset/ |
| [email protected] | ios/<App>/Images.xcassets/Splashscreen.imageset/ |
| Splashscreen~landscape.png | ios/<App>/Images.xcassets/Splashscreen~landscape.imageset/ |
| [email protected] | ios/<App>/Images.xcassets/Splashscreen~landscape.imageset/ |
| [email protected] | ios/<App>/Images.xcassets/Splashscreen~landscape.imageset/ |
Landscape variants are optional — if no Splashscreen~landscape[@Nx].png files are found, Splashscreen~landscape.imageset is left untouched.
Note: the CLI copies images into existing imagesets — it does not create
Splashscreen.imagesetorSplashscreen~landscape.imagesetif they don't already exist in your Xcode project.
Launch Storyboard
The CLI copies the launch storyboard and its referenced images from the brand config automatically, using UILaunchStoryboardName from Info.plist to resolve the correct destination.
How it works
- The CLI reads
UILaunchStoryboardNamefromios/<App>/Info.plistand normalises the value by appending.storyboardif absent (e.g.Launch→Launch.storyboard). Falls back toLaunchScreen.storyboardif the key is missing. - The CLI looks for the source storyboard in
rnwl-configs/<brand>/ios/:- First tries an exact name match (
<UILaunchStoryboardName>) - If not found, picks any
.storyboardfile present in that folder
- First tries an exact name match (
- The destination is resolved in this order:
ios/<App>/<UILaunchStoryboardName>— classic location inside the app folderios/<UILaunchStoryboardName>— root of theios/directory
- The storyboard is copied to the first path that already exists in the project.
- The CLI parses the storyboard XML and extracts all referenced image names (
<imageView image="...">and<image name="...">). For each name it looks for<name>.png,<name>@2x.png,<name>@3x.pnginrnwl-configs/<brand>/ios/splash/and copies any found files to the same directory as the storyboard.
Warnings
The CLI emits a warning (non-fatal) in these cases:
| Situation | Warning |
|-----------|---------|
| No .storyboard file found in brand config ios/ | ⚠ No .storyboard found in brand config ios/ — launch screen not updated |
| Storyboard references an image with no matching file in ios/splash/ | ⚠ Storyboard references image(s) not found in ios/splash: <name> — launch screen may appear broken |
Brand config structure
rnwl-configs/<brand>/ios/
<any>.storyboard ← any name — copied to the path declared in UILaunchStoryboardName
splash/
icon.png ← images referenced by the storyboard (all variants optional)
[email protected]
[email protected]Examples
| UILaunchStoryboardName | Brand config file | Destination |
|--------------------------|-------------------|-------------|
| LaunchScreen | ios/LaunchScreen.storyboard | ios/<App>/LaunchScreen.storyboard |
| Launch | ios/LaunchScreen.storyboard | ios/Launch.storyboard (resolved from root) |
| Launch | ios/Launch.storyboard | ios/Launch.storyboard (resolved from root) |
| MyCustomSplash | ios/anything.storyboard | ios/<App>/MyCustomSplash.storyboard |
🏷️ Xcode Scheme Renaming
After applying a brand, opening the Xcode project shows the correct brand name in the scheme dropdown — no manual rename required.
What gets updated
| File / location | Change |
|-----------------|--------|
| ios/<App>.xcodeproj/xcshareddata/xcschemes/<ProjectName>[suffix].xcscheme | File renamed to <displayName>[suffix].xcscheme |
| Inside each renamed scheme file | BlueprintName attribute updated to displayName |
| ios/<App>.xcodeproj/project.pbxproj | Hardcoded PRODUCT_NAME = <ProjectName>; entries replaced with displayName |
The .xcodeproj folder itself and all ios/<AppDir>/ paths are never renamed — they are referenced throughout the project and in Fastlane.
How scheme detection works
Schemes are identified by looking for ReferencedContainer = "container:<ProjectName>.xcodeproj" inside the scheme XML. Because the .xcodeproj folder name never changes, this works regardless of what the scheme file is currently called — so the rename is fully idempotent:
- Applying
connect(displayNameConnect) renameshOn.xcscheme→Connect.xcscheme - Applying
honafterwards (displayNamehOn) findsConnect.xcschemevia the container reference and renames it back tohOn.xcscheme - Applying the same brand twice is a no-op
Schemes that do not reference the project's .xcodeproj (e.g. CocoaPods schemes) are left untouched.
Suffix preservation
Suffixed variants are handled automatically:
Use the optional schemeName field in config.yml to set an explicit scheme identifier. If omitted, displayName is used as-is — set schemeName whenever displayName contains spaces to avoid issues with xcodebuild and CI scripts. PRODUCT_NAME in project.pbxproj is always set to displayName (spaces included) since that field supports it.
displayName: "Blue Theme"
schemeName: BlueTheme # recommended when displayName has spaces| Original file | After schemeName: Connect | After schemeName: BlueTheme |
|---------------|-----------------------------|-------------------------------|
| hOn.xcscheme | Connect.xcscheme | BlueTheme.xcscheme |
| hOn-release.xcscheme | Connect-release.xcscheme | BlueTheme-release.xcscheme |
| hOn-tvOS.xcscheme | Connect-tvOS.xcscheme | BlueTheme-tvOS.xcscheme |
The suffix is derived by stripping the current BlueprintName value from the front of the filename — the remainder is kept as-is.
If no matching .xcscheme files are found the step is skipped silently with a warning log.
🔥 Google Services Config
Brand-specific Firebase / Google Services config files are copied automatically when present in the brand folder. No YAML key is required. Processed independently of ignoreAssets.
| Source file | Destination |
|-------------|-------------|
| rnwl-configs/<brand>/android/google-services.json | android/app/google-services.json |
| rnwl-configs/<brand>/ios/GoogleService-Info.plist | ios/<App>/GoogleService-Info.plist |
If either file is absent in the brand folder it is silently skipped — no error is thrown.
🔗 Deep Link Configuration
RNWL supports two deep link strategies, configured independently per brand in config.yml:
| Field | Type | Description |
|-------|------|-------------|
| deeplinkScheme | string | Custom URL scheme — shorthand for a single scheme (e.g. myapp → handles myapp:// links) |
| deeplinkSchemes | string[] | Array of URL schemes — use when multiple schemes are needed (e.g. alongside schemes required by third-party SDKs). deeplinkScheme is equivalent to a one-element array. |
| universalLinkDomain | string | Domain for universal links / App Links (e.g. myapp.com → handles https://myapp.com/…) |
| webcredentialsDomain | string | Domain for Shared Web Credentials / Password AutoFill on iOS (e.g. myapp.com) |
All fields are optional. universalLinkDomain and webcredentialsDomain can share the same domain or use different ones — they are written as separate entries in the com.apple.developer.associated-domains array.
config.yml example
deeplinkScheme: myapp
universalLinkDomain: myapp.com
webcredentialsDomain: myapp.com # optional, can match universalLinkDomain or differTo register multiple iOS URL schemes (e.g. when a third-party SDK requires its own scheme):
deeplinkSchemes:
- myapp
- sdkscheme # additional scheme required by a third-party SDK
universalLinkDomain: myapp.comWhat the CLI configures natively
deeplinkScheme / deeplinkSchemes
Both forms are equivalent — deeplinkScheme: myapp is shorthand for deeplinkSchemes: [myapp]. When both are present, deeplinkSchemes takes precedence.
| Platform | File modified | Change |
|----------|--------------|--------|
| Android | android/app/src/main/AndroidManifest.xml | Adds an <intent-filter> with <data android:scheme="..."/> for the first scheme inside MainActivity |
| iOS | ios/<App>/Info.plist | Adds/updates CFBundleURLTypes with all schemes in a single <array> entry |
universalLinkDomain
| Platform | File modified | Change |
|----------|--------------|--------|
| Android | android/app/src/main/AndroidManifest.xml | Adds an <intent-filter android:autoVerify="true"> with android:scheme="https" and android:host |
| iOS | ios/<App>/<App>.entitlements | Creates or updates the file with com.apple.developer.associated-domains → applinks:<domain> |
| iOS | ios/<App>.xcodeproj/project.pbxproj | Sets CODE_SIGN_ENTITLEMENTS to point to the entitlements file |
webcredentialsDomain
| Platform | File modified | Change |
|----------|--------------|--------|
| iOS | ios/<App>/<App>.entitlements | Adds webcredentials:<domain> to the com.apple.developer.associated-domains array (alongside applinks: if universalLinkDomain is also set) |
| iOS | ios/<App>.xcodeproj/project.pbxproj | Sets CODE_SIGN_ENTITLEMENTS if not already pointing to the entitlements file |
| Android | android/app/src/main/AndroidManifest.xml | Adds a separate <intent-filter android:autoVerify="true"> for the domain — only if it differs from universalLinkDomain (if they match, the existing App Links filter already covers it) |
Note on universal links (iOS): the entitlements alone are not sufficient — you also need to host an
apple-app-site-association(AASA) file athttps://<domain>/.well-known/apple-app-site-association. This is a server-side requirement outside the scope of the CLI.
iOS Entitlements (iosEntitlements)
Beyond universal links, you can inject any additional iOS entitlement directly from config.yml using the iosEntitlements key:
iosEntitlements:
com.apple.developer.networking.HotspotConfiguration: true
com.apple.developer.networking.wifi-info: trueSupported value types:
| YAML type | Plist output |
|-----------|-------------|
| true / false | <true/> / <false/> |
| "string" | <string>string</string> |
| ["a", "b"] | <array><string>a</string>…</array> |
The CLI writes all keys into <AppName>.entitlements, creating the file if it does not exist, and sets CODE_SIGN_ENTITLEMENTS in project.pbxproj automatically. Re-running the command updates keys in-place; keys not present in iosEntitlements are left untouched.
Note:
com.apple.developer.associated-domainsis managed exclusively byuniversalLinkDomain— do not set it viaiosEntitlements.
The CLI uses HTML comment markers (<!-- RNWL:deeplinkScheme --> / <!-- RNWL:universalLinkDomain -->) around the injected blocks in AndroidManifest.xml so that re-running the command cleanly replaces the previous values instead of appending duplicates. If an equivalent <intent-filter> already exists without markers, the CLI wraps it in-place on the first run.
Accessing brand identity in the app
brand, displayName and packageName are exposed via useRNWLFeatures():
const { brand, displayName, packageName } = useRNWLFeatures();
return <Text>{displayName}</Text>; // "Blue App"
// brand === "blue" (matches the folder name in rnwl-configs/ and the brand: field in config.yml)Accessing deep link config in the app
Both values are exposed via useRNWLFeatures():
import { useRNWLFeatures } from '@enhancers/react-native-whitelabel';
function ShareButton() {
const { deeplinkScheme, universalLinkDomain } = useRNWLFeatures();
const shareUrl = universalLinkDomain
? `https://${universalLinkDomain}/profile/123`
: `${deeplinkScheme}://profile/123`;
return <Button title="Share" onPress={() => Share.share({ url: shareUrl })} />;
}🖥️ CLI: apply-white-label
The apply-white-label command applies a brand configuration to your React Native project in one step.
Usage
npx rnwl apply-white-label <brand> [options]Options:
| Flag | Description |
|------|-------------|
| --androidVersionCode <code> | Override versionCode in android/app/build.gradle |
| --androidVersionName <name> | Override versionName in android/app/build.gradle |
| --iosCurrentProjectVersion <ver> | Override CURRENT_PROJECT_VERSION in project.pbxproj |
| --iosMarketingVersion <ver> | Override MARKETING_VERSION in project.pbxproj |
Project structure
Create an rnwl-configs/ folder in your project root with one subfolder per brand:
my-app/
├── rnwl-configs/
│ ├── blue/
│ │ ├── config.yml
│ │ ├── android/
│ │ │ ├── icons/
│ │ │ │ ├── ic_launcher-mdpi.png
│ │ │ │ ├── ic_launcher-hdpi.png
│ │ │ │ ├── ...
│ │ │ │ ├── ic_launcher_foreground-mdpi.png (optional, adaptive icons)
│ │ │ │ ├── ic_launcher_foreground-hdpi.png
│ │ │ │ └── ...
│ │ │ ├── splash/
│ │ │ │ ├── splashscreen-mdpi.png
│ │ │ │ ├── splashscreen-hdpi.png
│ │ │ │ ├── ...
│ │ │ │ ├── splashscreen_land-mdpi.png (optional landscape)
│ │ │ │ └── ...
│ │ │ ├── strings.xml (optional)
│ │ │ └── google-services.json (optional)
│ │ └── ios/
│ │ ├── icons/
│ │ │ ├── AppIcon.png
│ │ │ ├── AppIcon-120.png
│ │ │ └── ...
│ │ ├── splash/
│ │ │ ├── Splashscreen.png
│ │ │ ├── [email protected]
│ │ │ ├── [email protected]
│ │ │ ├── Splashscreen~landscape.png (optional landscape)
│ │ │ ├── [email protected]
│ │ │ ├── [email protected]
│ │ │ └── icon.png, [email protected] ... (optional — storyboard-referenced images)
│ │ ├── <any>.storyboard (optional — any name; destination resolved from UILaunchStoryboardName in Info.plist)
│ │ └── GoogleService-Info.plist (optional)
│ └── red/
│ └── config.yml
├── android/
├── ios/
└── app.jsonExample config.yml
brand: blue
displayName: "Blue App"
schemeName: BlueApp # optional — Xcode scheme identifier (no spaces); defaults to displayName with spaces stripped
bundleId: com.myapp.blue
packageName: com.myapp.blue
version: "1.0.0"
colors:
primary: "#0066FF"
secondary: "#3366FF"
background: "#FFFFFF"
# Deep links (both optional)
deeplinkScheme: blueapp # registers blueapp:// on Android & iOS
# deeplinkSchemes: # alternative array form (iOS supports multiple schemes)
# - blueapp
# - sdkscheme
# universalLinkDomain: blue.myapp.com # registers https://blue.myapp.com App Links
features:
authentication: true
pushNotifications: true
darkMode: false
analytics: true
paymentGateway: false
ignoreAssets: falseExamples
# Apply the "blue" brand
npx rnwl apply-white-label blue
# Apply the "red" brand
npx rnwl apply-white-label red
# Override Android version
npx rnwl apply-white-label blue --androidVersionCode 42 --androidVersionName 2.1.0
# Override iOS version
npx rnwl apply-white-label blue --iosCurrentProjectVersion 42 --iosMarketingVersion 2.1.0
# Override both platforms at once
npx rnwl apply-white-label blue --androidVersionCode 42 --androidVersionName 2.1.0 --iosCurrentProjectVersion 42 --iosMarketingVersion 2.1.0
# List available brands (shown when an unknown brand is passed)
npx rnwl apply-white-label unknown-brandYou can also add convenience scripts to your package.json:
{
"scripts": {
"brand:blue": "npx rnwl apply-white-label blue",
"brand:red": "npx rnwl apply-white-label red"
}
}What it does
When executed, the CLI:
- Reads
rnwl-configs/<brand>/config.yml - Updates
app.jsonwithdisplayNameandversion - Updates
android/app/build.gradlewithapplicationId, and optionallyversionCode/versionNameif the flags are provided - Copies
android/strings.xmlfrom the brand config folder toandroid/app/src/main/res/values/strings.xml(if present) - Updates
ios/.../Info.plistwithCFBundleDisplayName - Copies the launch storyboard from the brand config using
UILaunchStoryboardNamefromInfo.plistto resolve the correct filename and destination, and copies storyboard-referenced images fromios/splash/next to the storyboard (see iOS Splash Screen) - Updates
ios/.../project.pbxprojwithPRODUCT_BUNDLE_IDENTIFIERandPRODUCT_NAME, and optionallyCURRENT_PROJECT_VERSION/MARKETING_VERSIONif the flags are provided - Renames
.xcschemefiles insideios/<App>.xcodeproj/xcshareddata/xcschemes/to matchdisplayNameand updates theBlueprintNameattribute inside each file (see Xcode Scheme Renaming) - Copies Android icons into
mipmap-*directories - Copies iOS icons into
AppIcon.appiconsetand regeneratesContents.json - Copies Android splash images into
mipmap-*directories (ifandroid/splash/exists in the brand folder) - Copies iOS splash images into
Splashscreen.imagesetand/orSplashscreen~landscape.imagesetand regeneratesContents.json(ifios/splash/exists — see iOS Splash Screen) - Copies
google-services.jsontoandroid/app/(if present in the brand folder) - Copies
GoogleService-Info.plisttoios/<App>/(if present in the brand folder) - Generates
rnwl.jsonin the project root with the active feature flags and brand colors - Configures native deep links (if
deeplinkSchemeoruniversalLinkDomainare set — see Deep Link Configuration) - Writes additional iOS entitlements (if
iosEntitlementsis set — see iOS Entitlements)
Full example with version flags
npx rnwl apply-white-label blue \
--androidVersionCode 42 \
--androidVersionName 2.1.0 \
--iosCurrentProjectVersion 42 \
--iosMarketingVersion 2.1.0This will apply the blue brand config and additionally set:
| File | Field | Value |
|------|-------|-------|
| android/app/build.gradle | versionCode | 42 |
| android/app/build.gradle | versionName | "2.1.0" |
| ios/.../project.pbxproj | CURRENT_PROJECT_VERSION | 42 |
| ios/.../project.pbxproj | MARKETING_VERSION | 2.1.0 |
The version flags are independent of the
versionfield inconfig.yml. Use them when you need to bump versions at build time (e.g. from a CI pipeline) without editing the config file.
🎨 Brand Colors
Each brand can define color tokens in config.yml under the colors key:
colors:
primary: "#0066FF"
secondary: "#3366FF"
background: "#FFFFFF"Any key name is supported — primary, secondary, background, accent, error, etc. The CLI writes them into rnwl.json and they are available at runtime via the useRNWLColors() hook.
useRNWLColors()
import { useRNWLColors } from '@enhancers/react-native-whitelabel';
function ThemedButton() {
const { primary } = useRNWLColors();
return <Button color={primary} title="Continue" />;
}Returns an RNWLColors object. Any key not defined in the brand config will be undefined.
RNWLColors type
interface RNWLColors {
primary?: string;
secondary?: string;
background?: string;
[key: string]: string | undefined; // any additional custom color keys
}Colors are also accessible directly via useRNWLFeatures() through the colors field on the context value, for components that already destructure the full context.
🎯 Feature Flag API
isFeatureEnabled(featureName)
Returns true if the feature is enabled, false otherwise. Never throws. Logs a console.warn in two cases:
- The feature key does not exist in the loaded configuration (useful for catching typos or stale references at development time)
- Features have not been loaded yet (only possible on web or when using a custom
featureLoader, since native initialization is synchronous)
import { useRNWLFeatures } from '@enhancers/react-native-whitelabel';
function MyComponent() {
const { isFeatureEnabled } = useRNWLFeatures();
// ✅ feature exists and is enabled → true
// ✅ feature exists and is disabled → false
// ⚠️ feature does not exist → false + console.warn
// ⚠️ not yet loaded (web / featureLoader) → false + console.warn
return isFeatureEnabled('darkMode') ? <DarkUI /> : <LightUI />;
}Or directly via the manager singleton:
import { rnwlFeatureFlagsManager } from '@enhancers/react-native-whitelabel';
const enabled = rnwlFeatureFlagsManager.isFeatureEnabled('analytics');💡 Use Cases
- Multi-tenant SaaS apps: Different features for different customers
- A/B Testing: Enable features for specific user segments
- Gradual Rollouts: Enable new features for specific brands first
- White-label Solutions: Build once, brand many times
- Feature Management: Control feature availability without rebuilding
🌐 Remote Feature Flags
Feature flags can be loaded from a remote backend via RNWLInitializeOptions, passed to <RNWLProvider>. This lets you control feature availability server-side without rebuilding the app.
featureLoader — custom async loader
Replaces the built-in file loading entirely. Return an RNWLFeatures object from any async source.
<RNWLProvider
options={{
featureLoader: async () => {
const res = await fetch('https://api.myapp.com/feature-flags');
const data = await res.json();
return data.features; // { darkMode: true, ads: false, ... }
},
}}
>
<YourApp />
</RNWLProvider>overrides — static partial overrides
Merged on top of whatever was loaded (file or featureLoader). Keys present here always win.
<RNWLProvider options={{ overrides: { ads: false } }}>
<YourApp />
</RNWLProvider>You can combine both: featureLoader fetches the base config, overrides applies local adjustments on top.
<RNWLProvider
options={{
featureLoader: fetchFeaturesFromBackend,
overrides: { ads: false }, // always disable ads, regardless of backend response
}}
>
<YourApp />
</RNWLProvider>reinitialize() — refresh at runtime
Call rnwlFeatureFlagsManager.reinitialize() to discard current state and re-run initialization. Useful for refreshing flags after login or when the user changes their plan.
import { rnwlFeatureFlagsManager } from '@enhancers/react-native-whitelabel';
// After user logs in, reload flags from backend
await rnwlFeatureFlagsManager.reinitialize({
featureLoader: fetchFeaturesFromBackend,
});