Skip to content

feat: support advanced UI customization #599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [Unreleased](https://github.com/Instabug/Instabug-Flutter/compare/v15.0.2...dev)

### Added

- Add support for Advanced UI customization with comprehensive theming capabilities ([#599](https://github.com/Instabug/Instabug-Flutter/pull/599))


## [15.0.2](https://github.com/Instabug/Instabug-Flutter/compare/v14.3.0...15.0.2) (Jul 7, 2025)

### Added
Expand Down
154 changes: 154 additions & 0 deletions android/src/main/java/com/instabug/flutter/modules/InstabugApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.graphics.Typeface;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand Down Expand Up @@ -509,4 +510,157 @@ public void setNetworkLogBodyEnabled(@NonNull Boolean isEnabled) {
e.printStackTrace();
}
}

@Override
public void setTheme(@NonNull Map<String, Object> themeConfig) {
try {
Log.d(TAG, "setTheme called with config: " + themeConfig.toString());

com.instabug.library.model.IBGTheme.Builder builder = new com.instabug.library.model.IBGTheme.Builder();

if (themeConfig.containsKey("primaryColor")) {
builder.setPrimaryColor(getColor(themeConfig, "primaryColor"));
}
if (themeConfig.containsKey("secondaryTextColor")) {
builder.setSecondaryTextColor(getColor(themeConfig, "secondaryTextColor"));
}
if (themeConfig.containsKey("primaryTextColor")) {
builder.setPrimaryTextColor(getColor(themeConfig, "primaryTextColor"));
}
if (themeConfig.containsKey("titleTextColor")) {
builder.setTitleTextColor(getColor(themeConfig, "titleTextColor"));
}
if (themeConfig.containsKey("backgroundColor")) {
builder.setBackgroundColor(getColor(themeConfig, "backgroundColor"));
}

if (themeConfig.containsKey("primaryTextStyle")) {
builder.setPrimaryTextStyle(getTextStyle(themeConfig, "primaryTextStyle"));
}
if (themeConfig.containsKey("secondaryTextStyle")) {
builder.setSecondaryTextStyle(getTextStyle(themeConfig, "secondaryTextStyle"));
}
if (themeConfig.containsKey("ctaTextStyle")) {
builder.setCtaTextStyle(getTextStyle(themeConfig, "ctaTextStyle"));
}

setFontIfPresent(themeConfig, builder, "primaryFontPath", "primaryFontAsset", "primary");
setFontIfPresent(themeConfig, builder, "secondaryFontPath", "secondaryFontAsset", "secondary");
setFontIfPresent(themeConfig, builder, "ctaFontPath", "ctaFontAsset", "CTA");

com.instabug.library.model.IBGTheme theme = builder.build();
Instabug.setTheme(theme);
Log.d(TAG, "Theme applied successfully");

} catch (Exception e) {
Log.e(TAG, "Error in setTheme: " + e.getMessage());
e.printStackTrace();
}
}



/**
* Retrieves a color value from the Map.
*
* @param map The Map object.
* @param key The key to look for.
* @return The parsed color as an integer, or black if missing or invalid.
*/
private int getColor(Map<String, Object> map, String key) {
try {
if (map != null && map.containsKey(key) && map.get(key) != null) {
String colorString = (String) map.get(key);
return android.graphics.Color.parseColor(colorString);
}
} catch (Exception e) {
e.printStackTrace();
}
return android.graphics.Color.BLACK;
}

/**
* Retrieves a text style from the Map.
*
* @param map The Map object.
* @param key The key to look for.
* @return The corresponding Typeface style, or Typeface.NORMAL if missing or invalid.
*/
private int getTextStyle(Map<String, Object> map, String key) {
try {
if (map != null && map.containsKey(key) && map.get(key) != null) {
String style = (String) map.get(key);
switch (style.toLowerCase()) {
case "bold":
return Typeface.BOLD;
case "italic":
return Typeface.ITALIC;
case "bold_italic":
return Typeface.BOLD_ITALIC;
case "normal":
default:
return Typeface.NORMAL;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return Typeface.NORMAL;
}

/**
* Sets a font on the theme builder if the font configuration is present in the theme config.
*
* @param themeConfig The theme configuration map
* @param builder The theme builder
* @param fileKey The key for font file path
* @param assetKey The key for font asset path
* @param fontType The type of font (for logging purposes)
*/
private void setFontIfPresent(Map<String, Object> themeConfig, com.instabug.library.model.IBGTheme.Builder builder,
String fileKey, String assetKey, String fontType) {
if (themeConfig.containsKey(fileKey) || themeConfig.containsKey(assetKey)) {
Typeface typeface = getTypeface(themeConfig, fileKey, assetKey);
if (typeface != null) {
switch (fontType) {
case "primary":
builder.setPrimaryTextFont(typeface);
break;
case "secondary":
builder.setSecondaryTextFont(typeface);
break;
case "CTA":
builder.setCtaTextFont(typeface);
break;
}
}
}
}

private Typeface getTypeface(Map<String, Object> map, String fileKey, String assetKey) {
String fontName = null;

if (assetKey != null && map.containsKey(assetKey) && map.get(assetKey) != null) {
fontName = (String) map.get(assetKey);
} else if (fileKey != null && map.containsKey(fileKey) && map.get(fileKey) != null) {
fontName = (String) map.get(fileKey);
}

if (fontName == null) {
return Typeface.DEFAULT;
}

try {
String assetPath = "fonts/" + fontName;
return Typeface.createFromAsset(context.getAssets(), assetPath);
} catch (Exception e) {
try {
return Typeface.create(fontName, Typeface.NORMAL);
} catch (Exception e2) {
return Typeface.DEFAULT;
}
}
}


}
51 changes: 51 additions & 0 deletions android/src/test/java/com/instabug/flutter/InstabugApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
import org.mockito.verification.VerificationMode;
import org.mockito.verification.VerificationMode;

import android.graphics.Typeface;

public class InstabugApiTest {
private final Callable<Bitmap> screenshotProvider = () -> mock(Bitmap.class);
private final Application mContext = mock(Application.class);
Expand Down Expand Up @@ -658,4 +660,53 @@ public void testSetNetworkLogBodyDisabled() {

mInstabug.verify(() -> Instabug.setNetworkLogBodyEnabled(false));
}

@Test
public void testSetThemeWithAllProperties() {
Map<String, Object> themeConfig = new HashMap<>();
themeConfig.put("primaryColor", "#FF6B6B");
themeConfig.put("backgroundColor", "#FFFFFF");
themeConfig.put("titleTextColor", "#000000");
themeConfig.put("primaryTextColor", "#333333");
themeConfig.put("secondaryTextColor", "#666666");
themeConfig.put("primaryTextStyle", "bold");
themeConfig.put("secondaryTextStyle", "italic");
themeConfig.put("ctaTextStyle", "bold_italic");
themeConfig.put("primaryFontAsset", "assets/fonts/CustomFont-Regular.ttf");
themeConfig.put("secondaryFontAsset", "assets/fonts/CustomFont-Bold.ttf");
themeConfig.put("ctaFontAsset", "assets/fonts/CustomFont-Italic.ttf");

MockedConstruction<com.instabug.library.model.IBGTheme.Builder> mThemeBuilder =
mockConstruction(com.instabug.library.model.IBGTheme.Builder.class, (mock, context) -> {
when(mock.setPrimaryColor(anyInt())).thenReturn(mock);
when(mock.setBackgroundColor(anyInt())).thenReturn(mock);
when(mock.setTitleTextColor(anyInt())).thenReturn(mock);
when(mock.setPrimaryTextColor(anyInt())).thenReturn(mock);
when(mock.setSecondaryTextColor(anyInt())).thenReturn(mock);
when(mock.setPrimaryTextStyle(anyInt())).thenReturn(mock);
when(mock.setSecondaryTextStyle(anyInt())).thenReturn(mock);
when(mock.setCtaTextStyle(anyInt())).thenReturn(mock);
when(mock.setPrimaryTextFont(any(Typeface.class))).thenReturn(mock);
when(mock.setSecondaryTextFont(any(Typeface.class))).thenReturn(mock);
when(mock.setCtaTextFont(any(Typeface.class))).thenReturn(mock);
when(mock.build()).thenReturn(mock(com.instabug.library.model.IBGTheme.class));
});

api.setTheme(themeConfig);

com.instabug.library.model.IBGTheme.Builder builder = mThemeBuilder.constructed().get(0);

verify(builder).setPrimaryColor(anyInt());
verify(builder).setBackgroundColor(anyInt());
verify(builder).setTitleTextColor(anyInt());
verify(builder).setPrimaryTextColor(anyInt());
verify(builder).setSecondaryTextColor(anyInt());
verify(builder).setPrimaryTextStyle(Typeface.BOLD);
verify(builder).setSecondaryTextStyle(Typeface.ITALIC);
verify(builder).setCtaTextStyle(Typeface.BOLD_ITALIC);

mInstabug.verify(() -> Instabug.setTheme(any(com.instabug.library.model.IBGTheme.class)));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class InstabugExampleMethodCallHandler : MethodChannel.MethodCallHandler {
sendOOM()
result.success(null)
}
SET_FULLSCREEN -> {
val isEnabled = call.arguments as? Map<*, *>
val enabled = isEnabled?.get("isEnabled") as? Boolean ?: false
setFullscreen(enabled)
result.success(null)
}
else -> {
Log.e(TAG, "onMethodCall for ${call.method} is not implemented")
result.notImplemented()
Expand All @@ -55,6 +61,7 @@ class InstabugExampleMethodCallHandler : MethodChannel.MethodCallHandler {
const val SEND_NATIVE_FATAL_HANG = "sendNativeFatalHang"
const val SEND_ANR = "sendAnr"
const val SEND_OOM = "sendOom"
const val SET_FULLSCREEN = "setFullscreen"
}

private fun sendNativeNonFatal(exceptionObject: String?) {
Expand Down Expand Up @@ -125,4 +132,25 @@ class InstabugExampleMethodCallHandler : MethodChannel.MethodCallHandler {
return randomString.toString()
}

private fun setFullscreen(enabled: Boolean) {
try {

try {
val instabugClass = Class.forName("com.instabug.library.Instabug")
val setFullscreenMethod = instabugClass.getMethod("setFullscreen", Boolean::class.java)
setFullscreenMethod.invoke(null, enabled)
} catch (e: ClassNotFoundException) {
throw e
} catch (e: NoSuchMethodException) {
throw e
} catch (e: Exception) {
throw e
}

} catch (e: Exception) {
e.printStackTrace()

}
}

}
28 changes: 28 additions & 0 deletions example/ios/InstabugTests/InstabugApiTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -611,4 +611,32 @@ - (void)testisW3CFeatureFlagsEnabled {

}

- (void)testSetThemeWithAllProperties {
NSDictionary *themeConfig = @{
@"primaryColor": @"#FF6B6B",
@"backgroundColor": @"#FFFFFF",
@"titleTextColor": @"#000000",
@"primaryTextColor": @"#333333",
@"secondaryTextColor": @"#666666",
@"callToActionTextColor": @"#FF6B6B",
@"primaryFontPath": @"assets/fonts/CustomFont-Regular.ttf",
@"secondaryFontPath": @"assets/fonts/CustomFont-Bold.ttf",
@"ctaFontPath": @"assets/fonts/CustomFont-Italic.ttf"
};

id mockTheme = OCMClassMock([IBGTheme class]);
OCMStub([mockTheme primaryColor]).andReturn([UIColor redColor]);
OCMStub([mockTheme backgroundColor]).andReturn([UIColor whiteColor]);
OCMStub([mockTheme titleTextColor]).andReturn([UIColor blackColor]);
OCMStub([mockTheme primaryTextColor]).andReturn([UIColor darkGrayColor]);
OCMStub([mockTheme secondaryTextColor]).andReturn([UIColor grayColor]);
OCMStub([mockTheme callToActionTextColor]).andReturn([UIColor redColor]);

FlutterError *error;

[self.api setThemeThemeConfig:themeConfig error:&error];

OCMVerify([self.mInstabug setTheme:[OCMArg isNotNil]]);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ class InstabugFlutterExampleMethodChannel {
log("Failed to send out of memory: '${e.message}'.", name: _tag);
}
}

static Future<void> setFullscreen(bool isEnabled) async {
if (!Platform.isAndroid) {
return;
}

try {
await _channel.invokeMethod(Constants.setFullscreenMethodName, {
'isEnabled': isEnabled,
});
} on PlatformException catch (e) {
log("Failed to set fullscreen: '${e.message}'.", name: _tag);
} catch (e) {
log("Unexpected error setting fullscreen: '$e'.", name: _tag);
}
}
}

class Constants {
Expand All @@ -65,4 +81,5 @@ class Constants {
static const sendNativeFatalHangMethodName = "sendNativeFatalHang";
static const sendAnrMethodName = "sendAnr";
static const sendOomMethodName = "sendOom";
static const setFullscreenMethodName = "setFullscreen";
}
4 changes: 4 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ flutter:

# To add assets to your application, add an assets section, like this:
# assets:
# - assets/fonts/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg

Expand All @@ -66,6 +67,9 @@ flutter:
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: ManufacturingConsent
# fonts:
# - asset: assets/fonts/ManufacturingConsent-Regular.ttf
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
Expand Down
Loading