White label apps

How to white-label your application using codemagic.yaml

These are the steps you need to get started white labeling your application using Codemagic.

  1. Add your base app to Codemagic
  2. Storing client’s assets somewhere Codemagic can access
  3. Create a unique environment variable group for each client (via UI or API)
  4. Setup your codemagic.yaml workflows to dynamically build for all clients
  5. Start new builds via API, passing the client Id, and the environment variable group name

1. Add your base app to Codemagic

You don’t have to create a Codemagic application for each client you want to white-label for, only one application linked with your base code is required.

The apps you have available on Codemagic are listed on the Applications page. Click Add application to add a new app.

  1. If you have more than one team configured in Codemagic, select the team you wish to add the app to.
  2. Connect the repository where the source code is hosted. Detailed instructions that cover some advanced options are available here.
  3. Select the repository from the list of available repositories. Select the appropriate project type.
  4. Click Finish: Add application

2. Storing client assets

Instead of adding each customer’s assets to the base app repository, you should store the assets required for rebranding the app somewhere accessible by Codemagic. This could be an encrypted S3 or GCP storage bucket or a headless CMS. Each customer should be assigned a unique identifier which can be passed to the white-label workflow when the build is started.

You should create a zip archive for each client that uses their unique identifier in the file name, e.g. assets_001.zip for client 001. When a build is started for a specific customer’s app, this unique identifier will be sent in the API request payload and the correct asset archive will be downloaded and used to rebrand the app.

💡 The zip archive typically contains these folders:

  • android_assets/. This folder contains the Android icons from /android/app/src/main/res/.
  • ios_assets/. This folder contains the iOS icons from /ios/Runner/Assets.xcassets/AppIcon.appiconset/.

Other assets such as fonts, images, etc. can also be added to this zip archive.

Avoid adding any sensitive files such as certificates, profiles, key stores, or other sensitive data in this archive.

3. Create a unique environment variable group for each client

During the white-label build, Codemagic uses client-specific data to set or replace various values in the base code and to sign and publish the app to the stores.

You should create a uniquely named environment variable group for each of your clients that contains secure environment variables for items such as certificates, profiles, API keys, or other client-specific credentials.

This group might contain the following environment variables:

  • Android package name. PACKAGE_NAME.
  • Android Keystore information. CM_KEYSTORE (base64 encoded), CM_KEY_ALIAS, CM_KEY_PASSWORD, CM_KEYSTORE_PASSWORD, CM_KEYSTORE_PATH.
  • The content of the Google Cloud service JSON file to publish to Play Store. GCLOUD_SERVICE_ACCOUNT_CREDENTIALS, learn how to get it here.
  • iOS app details. APP_STORE_ID, BUNDLE_ID.
  • App Store Connect API key. APP_STORE_CONNECT_KEY_IDENTIFIER, APP_STORE_CONNECT_ISSUER_ID, APP_STORE_CONNECT_PRIVATE_KEY, learn how to create create a new key here.
  • iOS Distribution certificate private key. CERTIFICATE_PRIVATE_KEY, learn how to obtain it here.
  • .env file if your app uses some secrets at runtime. DOTENV_FILE (base64 encoded).

To add these values you can either use the Codemagic UI or use the Codemagic REST API to add these groups and values programmatically, which could be advantageous if you have a large number of clients or wish to add these values from your own backend system or client dashboard.

To add an environment variable using the Codemagic REST API, you need your API access token, the application id, the client group unique name, and the variable name and value.

  • The access token is available in the Codemagic UI under Teams > Personal Account > Integrations > Codemagic API > Show. You can then store this as an environment variable if you are calling the REST API from other workflows.
  • Once you have added your app in Codemagic, open its settings and copy the application id from the browser address bar - https://codemagic.io/app/<APP_ID>/settings
  • The client group name, is the group that holds all variables for this client e.g. WL_001, WL_002.

An example of adding a secure variable to an application group looks like this:

curl -XPOST -H 'x-auth-token: <your-auth-token>' \
       -H 'Content-Type: application/json;charset=utf-8' \
       -d '{
       "key": "<variable-name>",
       "value": "<variable-value>"
       "group":"<client-unique-group-name>",
       "secure": true
       }' \
       'https://api.codemagic.io/apps/<app-id>/variables'

💡 Files such as Android keystores, or .env files should be base64 encoded and can be passed like this:

-d '{ "key": "<variable-name>", "value":$(cat fileName | base64) ...

And then decode it during the build like this:

echo $VAR | base64 --decode > /path

4. Setup your codemagic.yaml workflows to dynamically build for all clients

Your codemagic.yaml file contains various workflows for building your app for all clients e.g. android-qa-workflow, ios-release-workflow.

In most cases, white label automation is done using shell scripts to perform tasks such as downloading assets, copying files such as logos, images, fonts, etc. to a new location, or changing string values in projects. Here you will find some common script samples we are using in our final sample project.

Downloading assets from Amazon S3

The Amazon CLI tools are pre-installed on Codemagic’s machines which makes it easy to store assets such as images, fonts, logos, etc. in an encrypted S3 bucket and then download these to the build machine when building each white label version.

The following is an example of downloading a zip archive from Amazon S3 during the build where the CLIENT_ID variable is provided when the build is triggered using the Codemagic REST API:

environment:
  groups:
    - aws_credentials
  vars:
    S3_BUCKET_NAME: cmwhitelabel 
    CLIENT_ASSETS_FOLDER: client_assets
  ...    
scripts:
  - name: Get assets from AWS S3 bucket
    script: | 
      aws s3 cp s3://$S3_BUCKET_NAME/assets_${CLIENT_ID}.zip assets.zip
      unzip assets.zip -d $CLIENT_ASSETS_FOLDER

Use this script if you’re using a headless CMS instead:

scripts:
  - name: Get assets from Contentful CMS
    script: | 
      FILE_URL=$(curl --request GET --header "Authorization: Bearer $CONTENTFUL_API_TOKEN" "https://cdn.contentful.com/spaces/${CONTENTFUL_SPACE_ID}/environments/master/assets" | jq '.items[].fields' | jq -r --arg id "assets_$CLIENT_ID" '. | select (.title==$id) | .file.url' | cut -c 3-) 
      curl -H "Authorization: Bearer $CONTENTFUL_API_TOKEN" $FILE_URL --output assets.zip

Changing the Android package name

To natively change the Android package name, we can utilize a build automation tool like Gradle to modify the necessary files.

In this example, we’re first creating a file called changePackage.gradle in the root directory of the Android project, which updates the applicationId property of the defaultConfig block in your app’s build configuration, then adding this line to the app/build.gradle file to apply the changePackage.gradle script during the build process.

- name: Change Android package name
  script: | 
    echo "android {  defaultConfig { applicationId '${PACKAGE_NAME}' }  }" > android/changePackage.gradle
    echo "apply from: rootProject.file('changePackage.gradle')" >> android/app/build.gradle

Changing the iOS bundle ID

The automation scripts used in a white label workflow will often need to modify the content of a configuration file. This can be achieved using the sed stream editor utility, which can perform basic text transformations such as replacing or adding text in a file.

For example, if you want to change the bundle identifier used in the Xcode project by modifying the project.pbxproj file, the following script will set the PRODUCT_BUNDLE_IDENTIFIER with a value stored in the environment variable called $BUNDLE_ID.

- name: Set bundle id
  script: sed -i '' -e 's/PRODUCT_BUNDLE_IDENTIFIER \= [^\;]*\;/PRODUCT_BUNDLE_IDENTIFIER = '${BUNDLE_ID}';/' ios/Runner.xcodeproj/project.pbxproj

Changing app name

Using sed, the following script will replace the line in the AndroidManifest.xml that starts with android:label= with a new line contains the new app name $APP_NAME.

- name: Change Android app name
  script: sed -i.bak "s/android:label=.*/android:label=\"$APP_NAME\"/g" android/app/src/main/AndroidManifest.xml

PlistBuddy is a utility on macOS that can be used to perform operations on plist files. This approach can be used with native Swift/Objective-C apps, but please note that setting values directly in a Flutter project may cause problems with your project and you should consider using sed instead.

- name: Change iOS app name
  script: /usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" -c "Set :CFBundleDisplayName $APP_NAME" ios/${XCODE_SCHEME}/Info.plist

Changing app icons

For Android apps, you should run a script to update the icons located in android/app/src/main/res where you will find a number of directories that contain an icon for specific resolutions such as drawable-hdpi, drawable-mdpi, drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi. Your script to update the icons in your Android project might look something like this:

name: Change Android app icons
script: cp -r ./$CLIENT_ASSETS_FOLDER/android_assets/* ./android/app/src/main/res

For iOS apps, if you look at an Xcode project using Finder, you will see that the icons added in Xcode are located in <project-name>/<scheme-name>/Assets.xcassets/AppIcon.appiconset. This means that after downloading icon assets for a specific client’s build, you can change them on disk by simply deleting the existing AppIcon.appiconset directory, and then copying the assets into the Assets.xcassets directory.

name: Change iOS app icons
script: cp -r ./$CLIENT_ASSETS_FOLDER/ios_assets ios/Runner/Assets.xcassets/

💡 You can also use the flutter_launcher_icons package to generate the icons.

Automatic build versioning

Each new app version that is published to Google Play or the Apple App Store needs to have a unique build number. You can use Codemagic’s CLI tools to retrieve the previous build number and then increment this for each new build. For example, the following shows how to increment the build number when building Flutter apps:

name: Flutter build aab with automatic versioning
script: | 
  flutter build appbundle --release \
    --build-name=1.0.0 \
    --build-number=$(($(google-play get-latest-build-number --package-name "$PACKAGE_NAME" --tracks="$GOOGLE_PLAY_TRACK") + 1))
name: Flutter build ipa with automatic versioning
script: | 
  flutter build ipa --release \
    --build-name=1.0.0 \
    --build-number=$(($(app-store-connect get-latest-testflight-build-number "$APP_STORE_ID") + 1)) \
    --export-options-plist=/Users/builder/export_options.plist

Publishing to customer stores

You can automate the process of publishing each client’s app to their store account with Codemagic.

Make sure you add each client’s service account JSON key file to their environment variables group so Codemagic can authenticate and publish the app to their store.

publishing:
  google_play:
    credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
    track: <your-track>
Read more on this here.

If you’re using the automatic iOS code signing method, then each client’s environment group already has their App Store Connect account credentials, and they’ll be used to publish the app to this account.

publishing:      
  app_store_connect:
    api_key: $APP_STORE_CONNECT_PRIVATE_KEY
    key_id: $APP_STORE_CONNECT_KEY_IDENTIFIER
    issuer_id: $APP_STORE_CONNECT_ISSUER_ID
Read more on this here.

⚠️ Neither Apple nor Google provides APIs that programmatically allow an app to be created. Therefore, you will need to create and upload the first version of each app manually. After that Codemagic can fully automate the white-label process.

Full YAML sample

Having followed all of the above steps, you now have a working codemagic.yaml file that allows you to download client assets from AWS S3 bucket, chaning app name and icons, replacing exciting package name and bundle Id, build, code sign, automatically version and publish to each customer stores accounts.

Your final codemagic.yaml file should look something like this:

workflows:
  android-client-release:
    name: Android client release
    instance_type: mac_mini_m1
    labels:
      - ${CLIENT_ID} # Helpful when you open your Codemagic's builds page 
    environment:
      groups:
        - aws_credentials # Includes (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION)
      vars:
        S3_BUCKET_NAME: cmwhitelabel # The name of your S3 bucket that have all of your clients assets.
        CLIENT_ASSETS_FOLDER: client_assets # The name of unzipped folder on the build machine that have the client assets.
        ANDROID_ASSETS_FOLDER: android_assets # The name of your folder in S3 bucket that have the client's Android assets from (/android/app/src/main/res/).
        IOS_ASSETS_FOLDER: ios_assets # The name of your folder in S3 bucket that have the client's iOS assets from (/ios/Runner/Assets.xcassets/).
        GOOGLE_PLAY_TRACK: internal
    scripts:
      - name: Get assets from AWS S3 bucket
        script: | 
          aws s3 cp s3://$S3_BUCKET_NAME/assets_${CLIENT_ID}.zip assets.zip
          unzip assets.zip -d $CLIENT_ASSETS_FOLDER
      - name: Set Package name
        script: | 
          flutter pub add change_app_package_name
          flutter pub run change_app_package_name:main $PACKAGE_NAME
      - name: Change Android icons
        script: cp -r ./$CLIENT_ASSETS_FOLDER/$ANDROID_ASSETS_FOLDER/* ./android/app/src/main/res
      - name: Set up keystore
        script: echo $CM_KEYSTORE | base64 --decode > $CM_KEYSTORE_PATH
      - name: Install dependencies
        script: flutter packages pub get
      - name: Flutter build aab and automatic versioning
        script: | 
          BUILD_NUMBER=$(($(google-play get-latest-build-number --package-name "$PACKAGE_NAME" --tracks="$GOOGLE_PLAY_TRACK") + 1))
          flutter build appbundle --release \
          --build-name=1.0.$BUILD_NUMBER \
          --build-number=$BUILD_NUMBER
    artifacts: 
      - build/**/outputs/**/*.aab
    publishing:
      google_play:
        credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
        track: $GOOGLE_PLAY_TRACK 
workflows:
  ios-client-release:
    name: iOS client release
    instance_type: mac_mini_m1
    labels:
      - ${CLIENT_ID} # Helpful when you open your Codemagic's builds page  
    environment:
      groups:
        - aws_credentials # Includes (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION)
      vars:
        S3_BUCKET_NAME: cmwhitelabel # The name of your S3 bucket that have all of your clients assets.
        CLIENT_ASSETS_FOLDER: client_assets # The name of unzipped folder on the build machine that have the client assets.
        ANDROID_ASSETS_FOLDER: android_assets # The name of your folder in S3 bucket that have the client's Android assets from (/android/app/src/main/res/).
        IOS_ASSETS_FOLDER: ios_assets # The name of your folder in S3 bucket that have the client's iOS assets from (/ios/Runner/Assets.xcassets/).
        XCODE_WORKSPACE: "ios/Runner.xcworkspace"
        XCODE_SCHEME: "Runner"
        BASE_BUNDLE_ID: io.codemagic.whitelabel.dev # <-- Put the bundle ID that exists in the code, it will be replaced with the client's.
    scripts:
      - name: Get assets from AWS S3 bucket
        script: | 
          aws s3 cp s3://$S3_BUCKET_NAME/assets_${CLIENT_ID}.zip assets.zip
          unzip assets.zip -d $CLIENT_ASSETS_FOLDER
      - name: Set bundle id # Replace the base bundle Id with the client's
        script: | 
          PBXPROJ=$CM_BUILD_DIR/ios/Runner.xcodeproj/project.pbxproj
          sed -i.bak "s/\$BASE_BUNDLE_ID/$BUNDLE_ID/g" $PBXPROJ
      - name: Change iOS icons
        script: cp -r ./$CLIENT_ASSETS_FOLDER/$IOS_ASSETS_FOLDER ios/Runner/Assets.xcassets/
      - name: Install pods
        script: find . -name "Podfile" -execdir pod install \;
      - name: iOS code signing
        script: | 
          keychain initialize
          app-store-connect fetch-signing-files "$BUNDLE_ID" --type IOS_APP_STORE --create
          keychain add-certificates
          xcode-project use-profiles
      - name: Install dependencies
        script: flutter packages pub get      
      - name: Flutter build ipa and automatic versioning
        script: | 
          BUILD_NUMBER=$(($(app-store-connect get-latest-app-store-build-number "$APP_STORE_ID") + 1))
          flutter build ipa --release \
          --build-name=1.0.$BUILD_NUMBER \
          --build-number=$BUILD_NUMBER\
          --export-options-plist=/Users/builder/export_options.plist
    artifacts: 
      - build/ios/ipa/*.ipa
    publishing:      
      app_store_connect:
        api_key: $APP_STORE_CONNECT_PRIVATE_KEY
        key_id: $APP_STORE_CONNECT_KEY_IDENTIFIER
        issuer_id: $APP_STORE_CONNECT_ISSUER_ID

5. Start new builds via API

The Codemagic REST API is used in a white-label workflow to trigger builds for each unique client version you need to build. When triggering a build, you can pass environment variables in the API request’s payload that identify a specific client so their unique assets can be downloaded and used for the build, and the unique client environment group name that holds all the client secrets.

To trigger a build using the Codemagic REST API, you need your API access token, the application id, and the workflow id.

  • The access token is available in the Codemagic UI under Teams > Personal Account > Integrations > Codemagic API > Show. You can then store this as an environment variable if you are calling the REST API from other workflows.
  • Once you have added your app in Codemagic, open its settings and copy the application id from the browser address bar - https://codemagic.io/app/<APP_ID>/settings
  • The workflow id is the string value you assigned to the name property e.g “ios-qa-build”

An example of triggering a single build and passing an environment variable to specify the client id and a group to read the variables from might look like this:

curl -H "Content-Type: application/json" -H "x-auth-token: <auth-token>" \
  --data '{
      "appId": "<app-id>", 
      "workflowId": "<workflow-id>",
      "branch": "<branch-name>",
      "labels": ["<client-id>"],
      "environment": { 
          "variables": { 
              "CLIENT_ID": "<client-id>"
          },
          "groups": [
              "<client-group-name>"
          ]
      }
    }' \
  https://api.codemagic.io/builds

Instead of triggering a single build for each client on your local machine manually, you can create a Codemagic workflow that loops through all of your client and trigger a new build for each one of them.

In the following example, you can trigger this workflow by merging a PR into this branch.

workflows:
  trigger:
    name: trigger builds for all clients
    environment:
      groups:
        - cm_credentials # Includes (CM_API_KEY)
    triggering:
      events:
        - pull_request
      branch_patterns:
        - pattern: '<current-branch-name>'
          include: true
          source: true      
    scripts:
      - name: Trigger multiple client builds
        script: | 
            CLIENTS=("001" "002" "003") # put your clients IDs here
            for CLIENT in ${CLIENTS[@]}; do
                echo "CLIENT: $CLIENT"  
                curl -H "Content-Type: application/json" -H "x-auth-token: ${CM_API_KEY}" \
                    --data '{
                        "appId": "<app-id>", 
                        "workflowId": "<workflow-id>",
                        "branch": "<branch-name>",
                        "environment": { 
                            "variables": { 
                                "CLIENT_ID": "'${CLIENT}'"
                            },
                            "groups": [
                                "WL_${CLIENT}"
                            ]                      
                        }
                    }' \
                https://api.codemagic.io/builds
            done

The Codemagic REST API can also be used for white label solutions where a dashboard is made available to your clients so they can customize an app themselves. This means they could upload their own icons, images, etc. to brand their app and then create a new build of their app. It could also be more advanced and allow clients to add their own distribution certificates, provisioning profiles and API keys.

You can find out more about the Codemagic REST API here

Check out the white label sample project here.