A/B testing examples
Last updated
Last updated
Our analytics data show that most of the people make a purchase during their first session. However, only 70% of users open our in-app store during the first session. We hypothesize that if we add a purchase screen to the tutorial then 100% of users will face it and the number of purchases during the first session will increase.
The criteria for test inclusion is starting the tutorial. The DTDAnalytics.tutorial(step: Int)
event with step = -1 is the trigger.
Control group: a 10 step tutorial, no purchase screens
Group А: an 11 step tutorial. At step number 5, we will offer a special offer.
// Timer constants
struct Constants {
static let waitGroupConst = 10.0
}
// Value keys
enum ValueKey: String {
case showStore
}
class AppConfig {
static func loadDefaults() {
let appDefaults: [String: Any] = [
ValueKey.showStore.rawValue: "false"
]
// Set default values
DTDRemoteConfig.defaults = appDefaults
}
static func bool(forKey key: ValueKey) -> Bool {
return DTDRemoteConfig.config[key.rawValue].boolValue
}
}
class AppLogic {
var timer: Timer
// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
func startTutorial() {
// Send a trigger event that indicates tutorial start
DTDAnalytics.tutorial(step: -1)
}
func nextTutotrialStep(_ currentStep: Int) {
DTDAnalytics.tutorial(step: currentStep)
if AppConfig.bool(forKey: .showStore) && currentStep == 5 {
// Offer purchasing of a special offer
}
}
}
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Set the maximum time of waiting for an A/B test group
DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
// Set default values
AppConfig.loadDefaults()
// Initialize the SDK for working with A/B testing
DTDAnalytics.initializeWithAbTest(applicationKey: "appKey",
configuration: config,
abConfigListener: self)
}
}
extension AppLogic: DTDRemoteConfigListener {
// Process the result of waiting for A/B test configuration
func onReceived(result: DTDRemoteConfigReceiveResult) {
// It is not used in current example
}
// Prepare the app UI for changing the remote configuration
func onPrepareToChange() {
// Use the main app thread because you are getting ready for working with the interface
DispatchQueue.main.async { [weak self] in
// Display the download progress indicator
self?.showActivityIndicator()
// Add a timer that will forcibly remove the download progress indicator
self?.timer = Timer.scheduledTimer(withTimeInterval: Constants.waitGroupConst, repeats: false) { [weak self] _ in
self?.hideActivityIndicator()
}
}
}
// Apply the values of the assigned group
func onChanged(result: DTDRemoteConfigChangeResult, error: Error?) {
defer {
// Hide the download progress indicator
DispatchQueue.main.async { [weak self] in
self?.timer.invalidate();
self?.hideActivityIndicator()
}
}
switch result {
case .success:
// Apply new values
DTDRemoteConfig.applyConfig()
case .failure:
// Error processing
if let error = error {
print(error.localizedDescription)
}
@unknown default:
break
}
}
}
// Constants .h + .m
@interface Constants : NSObject
// Timer constants
extern double const waitGroupConst;
// Value keys
extern NSString * const showStore;
@end
@implementation Constants
double const waitGroupConst = 10.0;
NSString * const showStore = @"showStore";
@end
// AppConfig .h + .m
@interface AppConfig: NSObject
+(void) loadDefaults;
+(BOOL) getBoolForKey:(NSString *) key;
@end
@implementation AppConfig
+(void)loadDefaults {
NSDictionary *appDefaults = @{
showStore: @NO
};
// Set default values
DTDRemoteConfig.defaults = appDefaults;
}
+(BOOL) getBoolForKey:(NSString *) key {
return DTDRemoteConfig.config[key].boolValue;
}
@end
// AppLogic .h + .m
@interface AppLogic : UIViewController <DTDRemoteConfigListener>
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation AppLogic
- (void)viewDidLoad {
[super viewDidLoad];
[self startTutorial];
}
// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
- (void) startTutorial {
// Send a trigger event that indicates tutorial start
[DTDAnalytics tutorialStep: -1];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self nextTutotrialStep:5];
});
}
- (void) nextTutotrialStep:(NSInteger) currentStep {
[DTDAnalytics tutorialStep: currentStep];
if ([AppConfig getBoolForKey:showStore] && currentStep == 5) {
// Offer purchasing of a special offer
}
}
// Process the result of waiting for A/B test configuration
- (void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result {
// It is not used in current example
}
// Prepare the app UI for changing the remote configuration
- (void)onPrepareToChange {
// Use the main app thread because you are getting ready for working with the interface
__weak AppLogic *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
// Display the progress indicator
[weakSelf showActivityIndicator];
// Add a timer that will forcibly remove the progress indicator
self.timer = [NSTimer scheduledTimerWithTimeInterval:waitGroupConst repeats:false block:^(NSTimer *timer){
[weakSelf hideActivityIndicator];
}];
});
}
// Apply the values of the assigned group
- (void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error {
switch (result) {
case DTDRemoteConfigChangeResultSuccess:
// Apply new values
[DTDRemoteConfig applyConfig];
break;
case DTDRemoteConfigChangeResultFailure:
// Error processing
if (error) {
NSLog(@"DTDRemoteConfigError: %@", error.localizedDescription);
}
default:
break;
}
// Hide the progress indicator
__weak AppLogic *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self.timer invalidate];
[weakSelf hideActivityIndicator];
});
}
- (void)showActivityIndicator {
// Display the progress indicator
}
- (void)hideActivityIndicator {
// Hide the progress indicator
}
@end
// AppDelegate .h + .m
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
AppLogic *appLogicController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"AppLogic"];
UINavigationController *navController = [[UINavigationController alloc]initWithRootViewController:appLogicController];
self.window.rootViewController = navController;
[self.window makeKeyAndVisible];
// Group timeout, optional
DTDRemoteConfig.groupDefinitionWaiting = waitGroupConst;
// Implementation defaults params
[AppConfig loadDefaults];
[DTDAnalytics applicationKey:appKey abConfigListener:appLogicController];
return YES;
}
@end
class Constants {
// Timer constants
companion object {
const val waitGroupConst = 10.0
const val waitGroupConstInMilliseconds = 10000L
}
}
// Value keys
enum class ValueKey(val value: String) {
ShowStore("showStore")
}
class AppConfig {
companion object {
fun loadDefaults() {
val appDefaults = mapOf<String, Any>(ValueKey.ShowStore.value to "false")
DTDRemoteConfig.defaults = appDefaults
}
fun bool(key: ValueKey): Boolean {
return DTDRemoteConfig.config[key.value].booleanValue
}
}
}
class AppLogic {
// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
fun startTutorial() {
// Send a trigger event that indicates tutorial start
DTDAnalytics.tutorial(step = -1)
}
fun nextTutorialStep(currentStep: Int) {
DTDAnalytics.tutorial(step = currentStep)
if (AppConfig.bool(ValueKey.ShowStore) && currentStep == 5) {
// Offer purchasing of a special offer
}
}
}
class MainActivity : AppCompatActivity(), DTDRemoteConfigListener {
//activityIndicator control timer
var timer: TimerTask? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState, persistentState)
setContentView(R.layout.activity_main)
// Set the maximum time of waiting for an A/B test group
DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
// Set default values
AppConfig.loadDefaults()
// Initialize the SDK for working with A/B testing
DTDAnalytics.initializeWithAbTest(
appKey = "appKey",
context = this,
abConfigListener = this
)
}
// Process the result of waiting for A/B test configuration
override fun onReceived(result: DTDRemoteConfigReceiveResult) {
// It is not used in current example
}
// Prepare the app UI for changing the remote configuration
override fun onPrepareToChange() {
// Use the main app thread because you are getting ready for working with the interface
runOnUiThread {
// Display the download progress indicator
this.showActivityIndicator()
}
// Add a timer that will forcibly remove the download progress indicator
timer = Timer("ActivityIndicator").schedule(Constants.waitGroupConstInMilliseconds) {
runOnUiThread {
this.hideActivityIndicator()
}
}
}
override fun onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?) {
when (result) {
DTDRemoteConfigChangeResult.Success -> {
// Apply new values
DTDRemoteConfig.applyConfig()
}
DTDRemoteConfigChangeResult.Failure -> {
// Error processing
ex?.let { Log.e("TAG", ex.toString()) }
}
}
this.timer?.cancel()
this.timer = null
runOnUiThread {
this.hideActivityIndicator()
}
class Constants {
// Timer constants
final static double waitGroupConst = 10.0;
final static long waitGroupConstInMilliseconds = 10000L;
}
enum ValueKey {
ShowStore("showStore");
private final String stringValue;
ValueKey(String toString) {
stringValue = toString;
}
@Override
public String toString() {
return stringValue;
}
}
class AppConfig {
static void loadDefaults() {
HashMap<String, Object> map = new HashMap<>();
map.put(ValueKey.ShowStore.toString(), "false");
DTDRemoteConfig.INSTANCE.setDefaults(map);
}
static Boolean bool(ValueKey key) {
return DTDRemoteConfig.INSTANCE.getConfig().get(key.toString()).getBooleanValue();
}
}
class AppLogic {
// Tutorial open event (e.g. by clicking the 'start tutorial' button)
void startTutorial() {
// Send a trigger event that indicates tutorial start
DTDAnalytics.INSTANCE.tutorial( -1);
}
void nextTutorialStep(int currentStep) {
DTDAnalytics.INSTANCE.tutorial(currentStep);
if (AppConfig.bool(ValueKey.ShowStore) && currentStep == 5) {
// Offer purchasing of a special offer
}
}
}
class MainActivity extends AppCompatActivity implements DTDRemoteConfigListener {
//activityIndicator control timer
Timer timer = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Set the maximum time of waiting for an A/B test group
DTDRemoteConfig.INSTANCE.setGroupDefinitionWaiting(Constants.waitGroupConst);
// Set default values
AppConfig.loadDefaults();
// Initialize the SDK for working with A/B testing
DTDAnalytics.INSTANCE.initializeWithAbTest("appKey", this, this);
}
// Process the result of waiting for A/B test configuration
@Override
public void onReceived(@NonNull DTDRemoteConfigReceiveResult result) {
// It is not used in current example
}
// Prepare the app UI for changing the remote configuration
@Override
public void onPrepareToChange() {
// Use the main app thread because you are getting ready for working with the interface
runOnUiThread(() -> {
// Display the download progress indicator
showActivityIndicator();
});
timer = new Timer().schedule(new TimerTask() {
@Override
public void run() {
runOnUiThread(() -> hideActivityIndicator());
}
}, Constants.waitGroupConstInMilliseconds);
}
@Override
public void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex) {
if (result == DTDRemoteConfigChangeResult.Success) {
// Apply new values
DTDRemoteConfig.INSTANCE.applyConfig();
}
if (result == DTDRemoteConfigChangeResult.Failure) {
// Apply new values
DTDRemoteConfig.INSTANCE.applyConfig();
}
if (timer != null) {
timer.cancel();
timer = null;
}
runOnUiThread(() -> hideActivityIndicator());
}
}
// Timer constants
public static class Constants
{
public const float WAIT_GROUP_TIME = 10.0f;
}
// Value keys
public enum ValueKey
{
showStore
}
public class AppConfig
{
public void LoadDefaults()
{
var appDefaults = new Dictionary<string, object>
{
{ValueKey.showStore.ToString(), false}
};
DTDRemoteConfig.Defaults = appDefaults;
}
public bool GetBool(ValueKey key) => DTDRemoteConfig.Config[key.ToString()].BoolValue();
}
public class AppLogic : MonoBehaviour
{
private readonly AppConfig _appConfig = new AppConfig();
private const string APP_KEY = "appKey";
private SimpleUI _simpleUI;
private void Start()
{
DontDestroyOnLoad(this);
_simpleUI = FindObjectOfType<SimpleUI>();
if (_simpleUI == null) throw new NullReferenceException("UIManager not found.");
// Set the maximum time of waiting for an A/B test group
DTDRemoteConfig.GroupDefinitionWaiting = Constants.WAIT_GROUP_TIME;
// Set default values
_appConfig.LoadDefaults();
// Initialize the SDK for working with A/B testing
DTDAnalytics.InitializeWithAbTests(
appKey: APP_KEY,
configListener: this);
}
// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
// Send a trigger event that indicates tutorial start
public void StartTutorial() => DTDAnalytics.Tutorial(-1);
public void NextTutorialStep(int step)
{
DTDAnalytics.Tutorial(step);
if (_appConfig.GetBool(ValueKey.showStore) && step == 5)
{
// Offer purchasing of a special offer
Store.Instance.ShowSpecialOffer();
}
}
// Process the result of waiting for A/B test configuration
public void OnReceived(DTDRemoteConfigReceiveResult result)
{
// It is not used in current example
}
// Prepare the app UI for changing the remote configuration
public void OnPrepareToChange()
{
// Display the download progress indicator
_simpleUI.ShowLoadingIndicator(Constants.WAIT_GROUP_TIME);
}
// Apply the values of the assigned group
public void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)
{
// Hide the download progress indicator
_simpleUI.HideLoadingIndicator();
switch (result)
{
case DTDRemoteConfigChangeResult.Failure:
// Error processing
Debug.LogError(exceptionText);
break;
case DTDRemoteConfigChangeResult.Success:
// Apply new values
DTDRemoteConfig.ApplyConfig();
break;
}
}
}
// Timer constants
static class Constants
{
public const float WaitGroupConst = 10.0f;
}
// Value keys
static class ValueKeys
{
public const string ShowStore = "showStore";
}
static class AppConfg
{
public static void LoadDafaults()
{
DTDRemoteConfig.Defaults = new Dictionary<string, object>
{
[ValueKeys.ShowStore] = false
};
}
public static bool GetBoolValue(string key)
{
return DTDRemoteConfig.Config[key].BoolValue;
}
}
class Application : IDTDRemoteConfigListener
{
public void Run()
{
// Set the maximum time of waiting for an A/B test group
DTDRemoteConfig.GroupDefinitionWaiting = Constants.WaitGroupConst;
// Set default values
AppConfg.LoadDafaults();
// Initialize the SDK for working with A/B testing
DTDAnalytics.InitializeWithAbTest("appKey", this);
// Your code to show UI
}
// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
public void StartTutorial()
{
// Send a trigger event that indicates tutorial start
DTDAnalytics.Tutorial(-1);
}
public void NextTutotrialStep(int step)
{
DTDAnalytics.Tutorial(step);
if (AppConfg.GetBoolValue(ValueKeys.ShowStore) && step == 5)
{
// Offer purchasing of a special offer (method do a job in UI thread)
UI.ShowSpecialOffer();
}
}
// Apply the values of the assigned group
public void OnChanged(DTDRemoteConfigChangeResult result, string error)
{
Debug.WriteLine($"[App-ABTests] OnChanged({result}, {error})");
switch (result)
{
case DTDRemoteConfigChangeResult.Failure:
// Error processing
break;
case DTDRemoteConfigChangeResult.Success:
// Apply new values
DTDRemoteConfig.ApplyConfig();
break;
default:
break;
}
// Hide the download progress indicator(method do a job in UI thread)
UI.HideLoadingIndicator();
}
// Prepare the app UI for changing the remote configuration
public void OnPrepareToChange()
{
Debug.WriteLine($"[App-ABTests] OnPrepareToChange()");
// Display the download progress indicator (method do a job in UI thread)
UI.ShowLoadingIndicator();
// Add a timer that will forcibly remove the download progress indicator
TimerUtil.CallWithDelay(Constants.WaitGroupConst, () =>
{
// Hide the download progress indicator(method do a job in UI thread)
UI.HideLoadingIndicator();
});
}
// Process the result of waiting for A/B test configuration
public void OnReceived(DTDRemoteConfigReceiveResult result)
{
// It is not used in current example
Debug.WriteLine($"[App-ABTests] OnReceived({result})");
}
}
// Timer constant
float WAIT_GROUP_TIME = 15.0f;
void SomeLogicClass::Start() {
// Set the maximum time of waiting for an A/B test group
UDTDRemoteConfigBPLibrary::SetGroupDefinitionWaiting(WAIT_GROUP_TIME);
// Set default values
FDTDRemoteConfigDefaults Defaults;
Defaults.BoolDefaults.Add("showStore", false);
UDTDRemoteConfigBPLibrary::SetDefaults(Defaults);
const auto onConfigReceive = new FDTDRemoteConfigReceiveResultDelegate();
onConfigReceive->BindUObject(this, &SomeLogicClass::OnConfigReceive);
const auto onPrepareToChange = new FDTDRemoteConfigPrepareToChangeDelegate();
onPrepareToChange->BindUObject(this, &SomeLogicClass::OnPrepareToChange);
const auto onConfigChange = new FDTDRemoteConfigChangeResultDelegate();
onConfigChange->BindUObject(this, &SomeLogicClass::OnConfigChange);
// Initialize the SDK for working with A/B testing
UDTDAnalyticsBPLibrary::InitializeWithAbTest("AppKey", *onConfigChange, *onPrepareToChange, *onConfigReceive);
}
// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
void SomeLogicClass::StartTutorial() {
// Send a trigger event that indicates tutorial start
UDTDAnalyticsBPLibrary::Tutorial(-1);
}
void SomeLogicClass::NextTutorialStep(int32 step)
{
UDTDAnalyticsBPLibrary::Tutorial(step);
bool isNeedShowStore = UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("showStore").BoolValue;
if (isNeedShowStore && step == 5)
{
// Offer purchasing of a special offer
SomeStoreUI::ShowSpecialOffer();
}
}
// Process the result of waiting for A/B test configuration
void SomeLogicClass::OnConfigReceive(EDTDRemoteConfigReceiveResult result) {
// It is not used in current example
}
// Prepare the app UI for changing the remote configuration
void SomeLogicClass::OnPrepareToChange() {
// Display the download progress indicator
SomeUI::ShowLoadingIndicator(WAIT_GROUP_TIME);
}
// Apply the values of the assigned group
void SomeLogicClass::OnConfigChange(EDTDRemoteConfigChangeResult result, const FString& error) {
// Hide the download progress indicator
SomeUI::HideLoadingIndicator();
switch (result)
{
case EDTDRemoteConfigChangeResult::Success:
// Apply new values
UDTDRemoteConfigBPLibrary::ApplyConfig();
break;
case EDTDRemoteConfigChangeResult::Failure:
// Error processing
UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigError: %s"), *error);
break;
default:
break;
}
}
# Control timer
var timer = Timer.new()
# Timer constant
var waitGroupConst = 10.0
var showStore = "showStore"
func loadDefaults():
var appDefaults = GDDTDRemoteConfigDefaults.new()
# Value key
appDefaults.AddBoolValue(showStore, false)
DTDRemoteConfig.SetDefaults(appDefaults)
func getBool(key: String) -> bool:
return DTDRemoteConfig.GetRemoteConfigValue(key).GetBoolValue()
# Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
func startTutorial():
# Send a trigger event that indicates tutorial start
DTDAnalytics.Tutorial(-1)
func nextTutorialStep(currentStep: int):
DTDAnalytics.Tutorial(currentStep)
if (getBool(showStore) && currentStep == 5):
#Offer purchasing of a special offer
pass
func _ready():
# Set the maximum time of waiting for an A/B test group
DTDRemoteConfig.SetGroupDefinitionWaiting(waitGroupConst)
# Set default values
loadDefaults()
#Initialize the SDK for working with A/B testing
DTDAnalytics.InitializeWithAbTest("appKey",
onRemoteConfigChange,
onRemoteConfigPrepareToChange,
onRemoteConfigReceive)
DTDAnalytics.SetLogLevel(GDDTDLogLevel.Debug)
func onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String):
match result:
GDDTDRemoteConfigChangeResult.Success:
# Apply new values
DTDRemoteConfig.ApplyConfig()
GDDTDRemoteConfigChangeResult.Failure:
# Error processing
print(error)
timer.stop()
hideActivityIndicator()
# Prepare the app UI for changing the remote configuration
func onRemoteConfigPrepareToChange():
# Display the download progress indicator
showActivityIndicator()
# Add a timer that will forcibly remove the download progress indicator
timer.connect("timeout", hideActivityIndicator)
timer.one_shot = true
timer.wait_time = waitGroupConstInMilliseconds
add_child(timer)
timer.start()
# Process the result of waiting for A/B test configuration
func onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):
#It is not used in current example
pass
Our analytics data show that N users installed the app more than half a year ago but still did not make a purchase. We hypothesize that if we offer a huge discount then we can get additional income.
The criteria for test inclusion is the app install date and the “payer” status.
Control group: current version with usual prices
Group А: it has a discount badge on the main page. After clicking on the badge, a purchase window pops up
// Timer constants
struct Constants {
static let waitConfigConst = 10.0
static let waitGroupConst = 15.0
}
// Value keys
enum ValueKey: String {
case maximumDiscount
}
class AppConfig {
static func loadDefaults() {
let appDefaults: [String: Any] = [
ValueKey.maximumDiscount.rawValue: "false"
]
// Set default values
DTDRemoteConfig.defaults = appDefaults
}
static func bool(forKey key: ValueKey) -> Bool {
return DTDRemoteConfig.config[key.rawValue].boolValue
}
}
class AppLogic {
override func viewDidLoad() {
super.viewDidLoad()
// Display the launch screen
showLaunchScreen()
}
// Launching the main logic of the application
func startAppForReal() {
// Update app UI
updateAppUI()
// Hide launch screen
hideLaunchScreen()
}
func updateAppUI() {
if bool(forKey: .maximumDiscount) {
// Display a discount badge clicking on which invokes a purchase window popup
}
}
}
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Set the maximum time of waiting for the A/B test configuration
DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
// Set the maximum time of waiting for an A/B test group
DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
// Set default values
AppConfig.loadDefaults()
// Initialize the SDK for working with A/B testing
DTDAnalytics.initializeWithAbTest(applicationKey: "appKey",
configuration: config,
abConfigListener: self)
}
}
extension AppLogic: DTDRemoteConfigListener {
// Process the result of waiting for A/B test configuration
func onReceived(result: DTDRemoteConfigReceiveResult) {
// If the attempt fails, launch the main logic of the application
if result == .failure {
DispatchQueue.main.async { [weak self] in
self?.startAppForReal()
}
}
}
// Prepare the app UI for changing the remote configuration
func onPrepareToChange() {