From 3ea64843b56120ece479b468ed6f938fc51649e1 Mon Sep 17 00:00:00 2001 From: AyaMahmoud148 Date: Thu, 10 Jul 2025 11:40:06 +0300 Subject: [PATCH 1/7] feat: support advanced UI customization --- .../instabug/flutter/modules/InstabugApi.java | 162 ++++++++++++++++++ .../com/instabug/flutter/InstabugApiTest.java | 73 ++++++++ .../InstabugExampleMethodCallHandler.kt | 28 +++ example/ios/InstabugTests/InstabugApiTests.m | 28 +++ ...stabug_flutter_example_method_channel.dart | 17 ++ example/pubspec.yaml | 5 + ios/Classes/Modules/InstabugApi.m | 127 +++++++++++++- lib/instabug_flutter.dart | 1 + lib/src/models/theme_config.dart | 121 +++++++++++++ lib/src/modules/instabug.dart | 39 +++++ pigeons/instabug.api.dart | 2 + 11 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 lib/src/models/theme_config.dart diff --git a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java index edfde055a..27331152a 100644 --- a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java @@ -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; @@ -509,4 +510,165 @@ public void setNetworkLogBodyEnabled(@NonNull Boolean isEnabled) { e.printStackTrace(); } } + + @Override + public void setTheme(@NonNull Map 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(); + } + } + + public void setFullscreen(@NonNull Boolean isEnabled) { + try { + Instabug.setFullscreen(isEnabled); + } catch (Exception e) { + 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 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 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 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 map, String fileKey, String assetKey) { + try { + 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) { + return Typeface.create(fontName, Typeface.NORMAL); + } + + } catch (Exception e) { + return Typeface.DEFAULT; + } + } + + } diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index 97b9cdf7b..478570c9a 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -80,6 +80,8 @@ import org.mockito.verification.VerificationMode; import org.mockito.verification.VerificationMode; +import android.graphics.Typeface; + public class InstabugApiTest { private final Callable screenshotProvider = () -> mock(Bitmap.class); private final Application mContext = mock(Application.class); @@ -658,4 +660,75 @@ public void testSetNetworkLogBodyDisabled() { mInstabug.verify(() -> Instabug.setNetworkLogBodyEnabled(false)); } + + @Test + public void testSetThemeWithAllProperties() { + Map 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 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 color setters were called + verify(builder).setPrimaryColor(android.graphics.Color.parseColor("#FF6B6B")); + verify(builder).setBackgroundColor(android.graphics.Color.parseColor("#FFFFFF")); + verify(builder).setTitleTextColor(android.graphics.Color.parseColor("#000000")); + verify(builder).setPrimaryTextColor(android.graphics.Color.parseColor("#333333")); + verify(builder).setSecondaryTextColor(android.graphics.Color.parseColor("#666666")); + + // Verify text style setters were called + verify(builder).setPrimaryTextStyle(Typeface.BOLD); + verify(builder).setSecondaryTextStyle(Typeface.ITALIC); + verify(builder).setCtaTextStyle(Typeface.BOLD_ITALIC); + + // Verify theme was set on Instabug + mInstabug.verify(() -> Instabug.setTheme(any(com.instabug.library.model.IBGTheme.class))); + + } + + + @Test + public void testSetFullscreenEnabled() { + boolean isEnabled = true; + + api.setFullscreen(isEnabled); + + mInstabug.verify(() -> Instabug.setFullscreen(true)); + } + + @Test + public void testSetFullscreenDisabled() { + boolean isEnabled = false; + + api.setFullscreen(isEnabled); + + mInstabug.verify(() -> Instabug.setFullscreen(false)); + } } diff --git a/example/android/app/src/main/kotlin/com/example/InstabugSample/InstabugExampleMethodCallHandler.kt b/example/android/app/src/main/kotlin/com/example/InstabugSample/InstabugExampleMethodCallHandler.kt index 17a7d35c6..f6fb0fc03 100644 --- a/example/android/app/src/main/kotlin/com/example/InstabugSample/InstabugExampleMethodCallHandler.kt +++ b/example/android/app/src/main/kotlin/com/example/InstabugSample/InstabugExampleMethodCallHandler.kt @@ -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() @@ -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?) { @@ -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() + + } + } + } diff --git a/example/ios/InstabugTests/InstabugApiTests.m b/example/ios/InstabugTests/InstabugApiTests.m index 9f2c04373..d778f830e 100644 --- a/example/ios/InstabugTests/InstabugApiTests.m +++ b/example/ios/InstabugTests/InstabugApiTests.m @@ -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.any]); +} + @end diff --git a/example/lib/src/native/instabug_flutter_example_method_channel.dart b/example/lib/src/native/instabug_flutter_example_method_channel.dart index 9507cc403..118097dc3 100644 --- a/example/lib/src/native/instabug_flutter_example_method_channel.dart +++ b/example/lib/src/native/instabug_flutter_example_method_channel.dart @@ -54,6 +54,22 @@ class InstabugFlutterExampleMethodChannel { log("Failed to send out of memory: '${e.message}'.", name: _tag); } } + + static Future 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 { @@ -65,4 +81,5 @@ class Constants { static const sendNativeFatalHangMethodName = "sendNativeFatalHang"; static const sendAnrMethodName = "sendAnr"; static const sendOomMethodName = "sendOom"; + static const setFullscreenMethodName = "setFullscreen"; } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fe72aaa2d..4e2cd44b0 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -51,6 +51,8 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: + # - assets/fonts/ + # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg @@ -66,6 +68,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 diff --git a/ios/Classes/Modules/InstabugApi.m b/ios/Classes/Modules/InstabugApi.m index 3bdc465f0..fce1dbc2b 100644 --- a/ios/Classes/Modules/InstabugApi.m +++ b/ios/Classes/Modules/InstabugApi.m @@ -14,7 +14,9 @@ extern void InitInstabugApi(id messenger) { InstabugHostApiSetup(messenger, api); } -@implementation InstabugApi +@implementation InstabugApi { + NSMutableSet *_registeredFonts; +} - (void)setEnabledIsEnabled:(NSNumber *)isEnabled error:(FlutterError *_Nullable *_Nonnull)error { Instabug.enabled = [isEnabled boolValue]; @@ -396,4 +398,127 @@ - (void)setNetworkLogBodyEnabledIsEnabled:(NSNumber *)isEnabled IBGNetworkLogger.logBodyEnabled = [isEnabled boolValue]; } + + +- (void)setThemeThemeConfig:(NSDictionary *)themeConfig error:(FlutterError *_Nullable *_Nonnull)error { + IBGTheme *theme = [[IBGTheme alloc] init]; + + NSDictionary *colorMapping = @{ + @"primaryColor": ^(UIColor *color) { theme.primaryColor = color; }, + @"backgroundColor": ^(UIColor *color) { theme.backgroundColor = color; }, + @"titleTextColor": ^(UIColor *color) { theme.titleTextColor = color; }, + @"subtitleTextColor": ^(UIColor *color) { theme.subtitleTextColor = color; }, + @"primaryTextColor": ^(UIColor *color) { theme.primaryTextColor = color; }, + @"secondaryTextColor": ^(UIColor *color) { theme.secondaryTextColor = color; }, + @"callToActionTextColor": ^(UIColor *color) { theme.callToActionTextColor = color; }, + @"headerBackgroundColor": ^(UIColor *color) { theme.headerBackgroundColor = color; }, + @"footerBackgroundColor": ^(UIColor *color) { theme.footerBackgroundColor = color; }, + @"rowBackgroundColor": ^(UIColor *color) { theme.rowBackgroundColor = color; }, + @"selectedRowBackgroundColor": ^(UIColor *color) { theme.selectedRowBackgroundColor = color; }, + @"rowSeparatorColor": ^(UIColor *color) { theme.rowSeparatorColor = color; } + }; + + for (NSString *key in colorMapping) { + if (themeConfig[key]) { + NSString *colorString = themeConfig[key]; + UIColor *color = [self colorFromHexString:colorString]; + if (color) { + void (^setter)(UIColor *) = colorMapping[key]; + setter(color); + } + } + } + + [self setFontIfPresent:themeConfig[@"primaryFontPath"] ?: themeConfig[@"primaryFontAsset"] forTheme:theme type:@"primary"]; + [self setFontIfPresent:themeConfig[@"secondaryFontPath"] ?: themeConfig[@"secondaryFontAsset"] forTheme:theme type:@"secondary"]; + [self setFontIfPresent:themeConfig[@"ctaFontPath"] ?: themeConfig[@"ctaFontAsset"] forTheme:theme type:@"cta"]; + + Instabug.theme = theme; +} + +- (void)setFontIfPresent:(NSString *)fontPath forTheme:(IBGTheme *)theme type:(NSString *)type { + if (!fontPath || fontPath.length == 0 || !theme || !type) return; + + if (!_registeredFonts) { + _registeredFonts = [NSMutableSet set]; + } + + UIFont *font = [UIFont fontWithName:fontPath size:UIFont.systemFontSize]; + + if (!font && ![_registeredFonts containsObject:fontPath]) { + NSString *fontFileName = [fontPath stringByDeletingPathExtension]; + NSArray *fontExtensions = @[@"ttf", @"otf", @"woff", @"woff2"]; + NSString *fontFilePath = nil; + + for (NSString *extension in fontExtensions) { + fontFilePath = [[NSBundle mainBundle] pathForResource:fontFileName ofType:extension]; + if (fontFilePath) break; + } + + if (fontFilePath) { + NSData *fontData = [NSData dataWithContentsOfFile:fontFilePath]; + if (fontData) { + CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)fontData); + if (provider) { + CGFontRef cgFont = CGFontCreateWithDataProvider(provider); + if (cgFont) { + CFErrorRef error = NULL; + if (CTFontManagerRegisterGraphicsFont(cgFont, &error)) { + NSString *postScriptName = (__bridge_transfer NSString *)CGFontCopyPostScriptName(cgFont); + if (postScriptName) { + font = [UIFont fontWithName:postScriptName size:UIFont.systemFontSize]; + [_registeredFonts addObject:fontPath]; + } + } else if (error) { + CFStringRef desc = CFErrorCopyDescription(error); + CFRelease(desc); + CFRelease(error); + } + CGFontRelease(cgFont); + } + CGDataProviderRelease(provider); + } + } + } + } else if (!font && [_registeredFonts containsObject:fontPath]) { + font = [UIFont fontWithName:fontPath size:UIFont.systemFontSize]; + } + + if (font) { + if ([type isEqualToString:@"primary"]) { + theme.primaryTextFont = font; + } else if ([type isEqualToString:@"secondary"]) { + theme.secondaryTextFont = font; + } else if ([type isEqualToString:@"cta"]) { + theme.callToActionTextFont = font; + } + } +} + +- (UIColor *)colorFromHexString:(NSString *)hexString { + NSString *cleanString = [hexString stringByReplacingOccurrencesOfString:@"#" withString:@""]; + + if (cleanString.length == 6) { + unsigned int rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:cleanString]; + [scanner scanHexInt:&rgbValue]; + + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 + green:((rgbValue & 0xFF00) >> 8) / 255.0 + blue:(rgbValue & 0xFF) / 255.0 + alpha:1.0]; + } else if (cleanString.length == 8) { + unsigned int rgbaValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:cleanString]; + [scanner scanHexInt:&rgbaValue]; + + return [UIColor colorWithRed:((rgbaValue & 0xFF000000) >> 24) / 255.0 + green:((rgbaValue & 0xFF0000) >> 16) / 255.0 + blue:((rgbaValue & 0xFF00) >> 8) / 255.0 + alpha:(rgbaValue & 0xFF) / 255.0]; + } + + return [UIColor blackColor]; +} + @end diff --git a/lib/instabug_flutter.dart b/lib/instabug_flutter.dart index e38545897..66f74c2fb 100644 --- a/lib/instabug_flutter.dart +++ b/lib/instabug_flutter.dart @@ -3,6 +3,7 @@ export 'src/models/crash_data.dart'; export 'src/models/exception_data.dart'; export 'src/models/feature_flag.dart'; export 'src/models/network_data.dart'; +export 'src/models/theme_config.dart'; export 'src/models/trace.dart'; export 'src/models/w3c_header.dart'; diff --git a/lib/src/models/theme_config.dart b/lib/src/models/theme_config.dart new file mode 100644 index 000000000..ea3a6754e --- /dev/null +++ b/lib/src/models/theme_config.dart @@ -0,0 +1,121 @@ +class ThemeConfig { + /// Primary color for UI elements indicating interactivity or call to action. + final String? primaryColor; + + /// Background color for the main UI. + final String? backgroundColor; + + /// Color for title text elements. + final String? titleTextColor; + + /// Color for subtitle text elements. + final String? subtitleTextColor; + + /// Color for primary text elements. + final String? primaryTextColor; + + /// Color for secondary text elements. + final String? secondaryTextColor; + + /// Color for call-to-action text elements. + final String? callToActionTextColor; + + /// Background color for header elements. + final String? headerBackgroundColor; + + /// Background color for footer elements. + final String? footerBackgroundColor; + + /// Background color for row elements. + final String? rowBackgroundColor; + + /// Background color for selected row elements. + final String? selectedRowBackgroundColor; + + /// Color for row separator lines. + final String? rowSeparatorColor; + + /// Text style for primary text (Android only). + final String? primaryTextStyle; + + /// Text style for secondary text (Android only). + final String? secondaryTextStyle; + + /// Text style for title text (Android only). + final String? titleTextStyle; + + /// Text style for call-to-action text (Android only). + final String? ctaTextStyle; + + /// Path to primary font file. + final String? primaryFontPath; + + /// Asset path to primary font file. + final String? primaryFontAsset; + + /// Path to secondary font file. + final String? secondaryFontPath; + + /// Asset path to secondary font file. + final String? secondaryFontAsset; + + /// Path to call-to-action font file. + final String? ctaFontPath; + + /// Asset path to call-to-action font file. + final String? ctaFontAsset; + + const ThemeConfig({ + this.primaryColor, + this.backgroundColor, + this.titleTextColor, + this.subtitleTextColor, + this.primaryTextColor, + this.secondaryTextColor, + this.callToActionTextColor, + this.headerBackgroundColor, + this.footerBackgroundColor, + this.rowBackgroundColor, + this.selectedRowBackgroundColor, + this.rowSeparatorColor, + this.primaryTextStyle, + this.secondaryTextStyle, + this.titleTextStyle, + this.ctaTextStyle, + this.primaryFontPath, + this.primaryFontAsset, + this.secondaryFontPath, + this.secondaryFontAsset, + this.ctaFontPath, + this.ctaFontAsset, + }); + + Map toMap() { + return Map.fromEntries( + [ + MapEntry('primaryColor', primaryColor), + MapEntry('backgroundColor', backgroundColor), + MapEntry('titleTextColor', titleTextColor), + MapEntry('subtitleTextColor', subtitleTextColor), + MapEntry('primaryTextColor', primaryTextColor), + MapEntry('secondaryTextColor', secondaryTextColor), + MapEntry('callToActionTextColor', callToActionTextColor), + MapEntry('headerBackgroundColor', headerBackgroundColor), + MapEntry('footerBackgroundColor', footerBackgroundColor), + MapEntry('rowBackgroundColor', rowBackgroundColor), + MapEntry('selectedRowBackgroundColor', selectedRowBackgroundColor), + MapEntry('rowSeparatorColor', rowSeparatorColor), + MapEntry('primaryTextStyle', primaryTextStyle), + MapEntry('secondaryTextStyle', secondaryTextStyle), + MapEntry('titleTextStyle', titleTextStyle), + MapEntry('ctaTextStyle', ctaTextStyle), + MapEntry('primaryFontPath', primaryFontPath), + MapEntry('primaryFontAsset', primaryFontAsset), + MapEntry('secondaryFontPath', secondaryFontPath), + MapEntry('secondaryFontAsset', secondaryFontAsset), + MapEntry('ctaFontPath', ctaFontPath), + MapEntry('ctaFontAsset', ctaFontAsset), + ].where((entry) => entry.value != null), + ); + } +} diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index 6bba8ed1f..81f2baa4d 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_classes_with_only_static_members import 'dart:async'; +import 'dart:io'; // to maintain supported versions prior to Flutter 3.3 // ignore: unnecessary_import @@ -482,4 +483,42 @@ class Instabug { static Future willRedirectToStore() async { return _host.willRedirectToStore(); } + + /// Sets a custom theme for Instabug UI elements. + /// + /// @param theme - Configuration object containing theme properties + /// + /// Example: + /// ```dart + /// + /// Instabug.setTheme(ThemeConfig( + /// primaryColor: '#FF6B6B', + /// secondaryTextColor: '#666666', + /// primaryTextColor: '#333333', + /// titleTextColor: '#000000', + /// backgroundColor: '#FFFFFF', + /// primaryTextStyle: 'bold', + /// secondaryTextStyle: 'normal', + /// titleTextStyle: 'bold', + /// ctaTextStyle: 'bold', + /// primaryFontPath: '/data/user/0/com.yourapp/files/fonts/YourFont.ttf', + /// secondaryFontPath: '/data/user/0/com.yourapp/files/fonts/YourFont.ttf', + /// ctaFontPath: '/data/user/0/com.yourapp/files/fonts/YourFont.ttf', + /// primaryFontAsset: 'fonts/YourFont.ttf', + /// secondaryFontAsset: 'fonts/YourFont.ttf' + /// )); + /// ``` + static Future setTheme(ThemeConfig themeConfig) async { + return _host.setTheme(themeConfig.toMap()); + } + + /// Enables or disables displaying in full-screen mode, hiding the status and navigation bars. + /// This method is only available on Android platform. + /// @param isEnabled A boolean to enable/disable setFullscreen. + static Future setFullscreen(bool isEnabled) async { + if (Platform.isAndroid) { + const MethodChannel channel = MethodChannel('instabug_flutter'); + await channel.invokeMethod('setFullscreen', {'isEnabled': isEnabled}); + } + } } diff --git a/pigeons/instabug.api.dart b/pigeons/instabug.api.dart index 275306987..898199824 100644 --- a/pigeons/instabug.api.dart +++ b/pigeons/instabug.api.dart @@ -76,4 +76,6 @@ abstract class InstabugHostApi { void willRedirectToStore(); void setNetworkLogBodyEnabled(bool isEnabled); + + void setTheme(Map themeConfig); } From 4081f5bff6b2662bf401dc521156f66c1d2037e6 Mon Sep 17 00:00:00 2001 From: AyaMahmoud148 Date: Thu, 10 Jul 2025 11:42:17 +0300 Subject: [PATCH 2/7] chore: add change log --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb867f5c..c7824fef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 8798555d6f23c63e2bdd925351dea81038b12465 Mon Sep 17 00:00:00 2001 From: AyaMahmoud148 Date: Thu, 10 Jul 2025 11:48:47 +0300 Subject: [PATCH 3/7] fix: delete setFullScreen --- .../instabug/flutter/modules/InstabugApi.java | 8 +------- .../com/instabug/flutter/InstabugApiTest.java | 17 ----------------- lib/src/modules/instabug.dart | 10 ---------- 3 files changed, 1 insertion(+), 34 deletions(-) diff --git a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java index 27331152a..12286decb 100644 --- a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java @@ -558,13 +558,7 @@ public void setTheme(@NonNull Map themeConfig) { } } - public void setFullscreen(@NonNull Boolean isEnabled) { - try { - Instabug.setFullscreen(isEnabled); - } catch (Exception e) { - e.printStackTrace(); - } - } + /** * Retrieves a color value from the Map. diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index 478570c9a..65b36d959 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -713,22 +713,5 @@ public void testSetThemeWithAllProperties() { } - - @Test - public void testSetFullscreenEnabled() { - boolean isEnabled = true; - - api.setFullscreen(isEnabled); - - mInstabug.verify(() -> Instabug.setFullscreen(true)); - } - @Test - public void testSetFullscreenDisabled() { - boolean isEnabled = false; - - api.setFullscreen(isEnabled); - - mInstabug.verify(() -> Instabug.setFullscreen(false)); - } } diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index 81f2baa4d..a074a7f6d 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -511,14 +511,4 @@ class Instabug { static Future setTheme(ThemeConfig themeConfig) async { return _host.setTheme(themeConfig.toMap()); } - - /// Enables or disables displaying in full-screen mode, hiding the status and navigation bars. - /// This method is only available on Android platform. - /// @param isEnabled A boolean to enable/disable setFullscreen. - static Future setFullscreen(bool isEnabled) async { - if (Platform.isAndroid) { - const MethodChannel channel = MethodChannel('instabug_flutter'); - await channel.invokeMethod('setFullscreen', {'isEnabled': isEnabled}); - } - } } From d46e04dc64a9b7c53e07e9ae12e7862ea15d24fe Mon Sep 17 00:00:00 2001 From: AyaMahmoud148 Date: Thu, 10 Jul 2025 11:55:44 +0300 Subject: [PATCH 4/7] fix: linting --- lib/src/modules/instabug.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index a074a7f6d..41b66bc97 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -1,7 +1,6 @@ // ignore_for_file: avoid_classes_with_only_static_members import 'dart:async'; -import 'dart:io'; // to maintain supported versions prior to Flutter 3.3 // ignore: unnecessary_import From 98a2f8cb9429c2f70aac6dad744ab272f766c72e Mon Sep 17 00:00:00 2001 From: AyaMahmoud148 Date: Thu, 10 Jul 2025 12:25:15 +0300 Subject: [PATCH 5/7] fix: unit test --- .../com/instabug/flutter/InstabugApiTest.java | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index 65b36d959..30512a2e4 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -692,25 +692,21 @@ public void testSetThemeWithAllProperties() { 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 color setters were called - verify(builder).setPrimaryColor(android.graphics.Color.parseColor("#FF6B6B")); - verify(builder).setBackgroundColor(android.graphics.Color.parseColor("#FFFFFF")); - verify(builder).setTitleTextColor(android.graphics.Color.parseColor("#000000")); - verify(builder).setPrimaryTextColor(android.graphics.Color.parseColor("#333333")); - verify(builder).setSecondaryTextColor(android.graphics.Color.parseColor("#666666")); - - // Verify text style setters were called - verify(builder).setPrimaryTextStyle(Typeface.BOLD); - verify(builder).setSecondaryTextStyle(Typeface.ITALIC); - verify(builder).setCtaTextStyle(Typeface.BOLD_ITALIC); - - // Verify theme was set on Instabug - mInstabug.verify(() -> Instabug.setTheme(any(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))) } From 20add374041542de55fbe0d419797c22390d94ad Mon Sep 17 00:00:00 2001 From: AyaMahmoud148 Date: Thu, 10 Jul 2025 12:33:07 +0300 Subject: [PATCH 6/7] fix: linting --- android/src/test/java/com/instabug/flutter/InstabugApiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index 30512a2e4..755a9d068 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -706,7 +706,7 @@ public void testSetThemeWithAllProperties() { verify(builder).setSecondaryTextStyle(Typeface.ITALIC); verify(builder).setCtaTextStyle(Typeface.BOLD_ITALIC); - mInstabug.verify(() -> Instabug.setTheme(any(com.instabug.library.model.IBGTheme.class))) + mInstabug.verify(() -> Instabug.setTheme(any(com.instabug.library.model.IBGTheme.class))); } From cd807c4372c8661aa2ccc54f6af2bdd50a841936 Mon Sep 17 00:00:00 2001 From: AyaMahmoud148 Date: Mon, 14 Jul 2025 13:59:26 +0300 Subject: [PATCH 7/7] fix: resolve comments --- .../instabug/flutter/modules/InstabugApi.java | 36 +++-- .../com/instabug/flutter/InstabugApiTest.java | 17 ++- example/ios/InstabugTests/InstabugApiTests.m | 2 +- example/pubspec.yaml | 1 - ios/Classes/Modules/InstabugApi.m | 131 ++++++++++++------ 5 files changed, 114 insertions(+), 73 deletions(-) diff --git a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java index 12286decb..f81cf3cc5 100644 --- a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java @@ -638,29 +638,27 @@ private void setFontIfPresent(Map themeConfig, com.instabug.libr } private Typeface getTypeface(Map 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 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; - } - - + String assetPath = "fonts/" + fontName; + return Typeface.createFromAsset(context.getAssets(), assetPath); + } catch (Exception e) { try { - String assetPath = "fonts/" + fontName; - return Typeface.createFromAsset(context.getAssets(), assetPath); - } catch (Exception e) { return Typeface.create(fontName, Typeface.NORMAL); + } catch (Exception e2) { + return Typeface.DEFAULT; } - - } catch (Exception e) { - return Typeface.DEFAULT; } } diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index 755a9d068..158b67499 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -692,21 +692,20 @@ public void testSetThemeWithAllProperties() { when(mock.build()).thenReturn(mock(com.instabug.library.model.IBGTheme.class)); }); - api.setTheme(themeConfig); + api.setTheme(themeConfig); - com.instabug.library.model.IBGTheme.Builder builder = mThemeBuilder.constructed().get(0); - + 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))); + 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))); } diff --git a/example/ios/InstabugTests/InstabugApiTests.m b/example/ios/InstabugTests/InstabugApiTests.m index d778f830e..176e092a3 100644 --- a/example/ios/InstabugTests/InstabugApiTests.m +++ b/example/ios/InstabugTests/InstabugApiTests.m @@ -636,7 +636,7 @@ - (void)testSetThemeWithAllProperties { [self.api setThemeThemeConfig:themeConfig error:&error]; - OCMVerify([self.mInstabug setTheme:OCMArg.any]); + OCMVerify([self.mInstabug setTheme:[OCMArg isNotNil]]); } @end diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4e2cd44b0..dfd49f2aa 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -52,7 +52,6 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: # - assets/fonts/ - # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/ios/Classes/Modules/InstabugApi.m b/ios/Classes/Modules/InstabugApi.m index fce1dbc2b..01b271987 100644 --- a/ios/Classes/Modules/InstabugApi.m +++ b/ios/Classes/Modules/InstabugApi.m @@ -443,55 +443,100 @@ - (void)setFontIfPresent:(NSString *)fontPath forTheme:(IBGTheme *)theme type:(N _registeredFonts = [NSMutableSet set]; } - UIFont *font = [UIFont fontWithName:fontPath size:UIFont.systemFontSize]; - - if (!font && ![_registeredFonts containsObject:fontPath]) { - NSString *fontFileName = [fontPath stringByDeletingPathExtension]; - NSArray *fontExtensions = @[@"ttf", @"otf", @"woff", @"woff2"]; - NSString *fontFilePath = nil; - - for (NSString *extension in fontExtensions) { - fontFilePath = [[NSBundle mainBundle] pathForResource:fontFileName ofType:extension]; - if (fontFilePath) break; + // Check if font is already registered + if ([_registeredFonts containsObject:fontPath]) { + UIFont *font = [UIFont fontWithName:fontPath size:UIFont.systemFontSize]; + if (font) { + [self setFont:font forTheme:theme type:type]; } + return; + } - if (fontFilePath) { - NSData *fontData = [NSData dataWithContentsOfFile:fontFilePath]; - if (fontData) { - CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)fontData); - if (provider) { - CGFontRef cgFont = CGFontCreateWithDataProvider(provider); - if (cgFont) { - CFErrorRef error = NULL; - if (CTFontManagerRegisterGraphicsFont(cgFont, &error)) { - NSString *postScriptName = (__bridge_transfer NSString *)CGFontCopyPostScriptName(cgFont); - if (postScriptName) { - font = [UIFont fontWithName:postScriptName size:UIFont.systemFontSize]; - [_registeredFonts addObject:fontPath]; - } - } else if (error) { - CFStringRef desc = CFErrorCopyDescription(error); - CFRelease(desc); - CFRelease(error); - } - CGFontRelease(cgFont); - } - CGDataProviderRelease(provider); - } - } - } - } else if (!font && [_registeredFonts containsObject:fontPath]) { - font = [UIFont fontWithName:fontPath size:UIFont.systemFontSize]; + // Try to load font from system fonts first + UIFont *font = [UIFont fontWithName:fontPath size:UIFont.systemFontSize]; + if (font) { + [_registeredFonts addObject:fontPath]; + [self setFont:font forTheme:theme type:type]; + return; } + // Try to load font from bundle + font = [self loadFontFromPath:fontPath]; if (font) { - if ([type isEqualToString:@"primary"]) { - theme.primaryTextFont = font; - } else if ([type isEqualToString:@"secondary"]) { - theme.secondaryTextFont = font; - } else if ([type isEqualToString:@"cta"]) { - theme.callToActionTextFont = font; + [_registeredFonts addObject:fontPath]; + [self setFont:font forTheme:theme type:type]; + } +} + +- (UIFont *)loadFontFromPath:(NSString *)fontPath { + NSString *fontFileName = [fontPath stringByDeletingPathExtension]; + NSArray *fontExtensions = @[@"ttf", @"otf", @"woff", @"woff2"]; + + // Find font file in bundle + NSString *fontFilePath = nil; + for (NSString *extension in fontExtensions) { + fontFilePath = [[NSBundle mainBundle] pathForResource:fontFileName ofType:extension]; + if (fontFilePath) break; + } + + if (!fontFilePath) { + return nil; + } + + // Load font data + NSData *fontData = [NSData dataWithContentsOfFile:fontFilePath]; + if (!fontData) { + return nil; + } + + // Create data provider + CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)fontData); + if (!provider) { + return nil; + } + + // Create CG font + CGFontRef cgFont = CGFontCreateWithDataProvider(provider); + CGDataProviderRelease(provider); + + if (!cgFont) { + return nil; + } + + // Register font + CFErrorRef error = NULL; + BOOL registered = CTFontManagerRegisterGraphicsFont(cgFont, &error); + + if (!registered) { + if (error) { + CFStringRef description = CFErrorCopyDescription(error); + CFRelease(description); + CFRelease(error); } + CGFontRelease(cgFont); + return nil; + } + + // Get PostScript name and create UIFont + NSString *postScriptName = (__bridge_transfer NSString *)CGFontCopyPostScriptName(cgFont); + CGFontRelease(cgFont); + + if (!postScriptName) { + return nil; + } + + return [UIFont fontWithName:postScriptName size:UIFont.systemFontSize]; +} + +- (void)setFont:(UIFont *)font forTheme:(IBGTheme *)theme type:(NSString *)type { + if (!font || !theme || !type) return; + + if ([type isEqualToString:@"primary"]) { + theme.primaryTextFont = font; + } else if ([type isEqualToString:@"secondary"]) { + theme.secondaryTextFont = font; + } else if ([type isEqualToString:@"cta"]) { + theme.callToActionTextFont = font; } }