Skip to content

Commit 54cba9b

Browse files
committed
feat: Support Advanced UI customization
1 parent 5c8f2cf commit 54cba9b

File tree

10 files changed

+604
-2
lines changed

10 files changed

+604
-2
lines changed

android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66

77
import android.app.Application;
88
import android.graphics.Bitmap;
9+
import android.graphics.Color;
10+
import android.graphics.Typeface;
911
import android.net.Uri;
1012
import android.util.Log;
1113
import android.view.View;
1214

15+
import com.facebook.react.bridge.ReactApplicationContext;
16+
1317
import androidx.annotation.NonNull;
1418
import androidx.annotation.UiThread;
1519

@@ -45,6 +49,7 @@
4549
import com.instabug.library.internal.module.InstabugLocale;
4650
import com.instabug.library.invocation.InstabugInvocationEvent;
4751
import com.instabug.library.logging.InstabugLog;
52+
import com.instabug.library.model.IBGTheme;
4853
import com.instabug.library.model.NetworkLog;
4954
import com.instabug.library.model.Report;
5055
import com.instabug.library.ui.onboarding.WelcomeMessage;
@@ -1351,4 +1356,203 @@ public void run() {
13511356
}
13521357
});
13531358
}
1359+
/**
1360+
* Sets the theme for Instabug using a configuration object.
1361+
*
1362+
* @param themeConfig A ReadableMap containing theme properties such as colors, fonts, and text styles.
1363+
*/
1364+
@ReactMethod
1365+
public void setTheme(final ReadableMap themeConfig) {
1366+
MainThreadHandler.runOnMainThread(new Runnable() {
1367+
@Override
1368+
public void run() {
1369+
try {
1370+
com.instabug.library.model.IBGTheme.Builder builder = new com.instabug.library.model.IBGTheme.Builder();
1371+
1372+
if (themeConfig.hasKey("primaryColor")) {
1373+
builder.setPrimaryColor(getColor(themeConfig, "primaryColor"));
1374+
}
1375+
if (themeConfig.hasKey("secondaryTextColor")) {
1376+
builder.setSecondaryTextColor(getColor(themeConfig, "secondaryTextColor"));
1377+
}
1378+
if (themeConfig.hasKey("primaryTextColor")) {
1379+
builder.setPrimaryTextColor(getColor(themeConfig, "primaryTextColor"));
1380+
}
1381+
if (themeConfig.hasKey("titleTextColor")) {
1382+
builder.setTitleTextColor(getColor(themeConfig, "titleTextColor"));
1383+
}
1384+
if (themeConfig.hasKey("backgroundColor")) {
1385+
builder.setBackgroundColor(getColor(themeConfig, "backgroundColor"));
1386+
}
1387+
1388+
if (themeConfig.hasKey("primaryTextStyle")) {
1389+
builder.setPrimaryTextStyle(getTextStyle(themeConfig, "primaryTextStyle"));
1390+
}
1391+
if (themeConfig.hasKey("secondaryTextStyle")) {
1392+
builder.setSecondaryTextStyle(getTextStyle(themeConfig, "secondaryTextStyle"));
1393+
}
1394+
if (themeConfig.hasKey("ctaTextStyle")) {
1395+
builder.setCtaTextStyle(getTextStyle(themeConfig, "ctaTextStyle"));
1396+
}
1397+
if (themeConfig.hasKey("primaryFontPath") || themeConfig.hasKey("primaryFontAsset")) {
1398+
Typeface primaryTypeface = getTypeface(themeConfig, "primaryFontPath", "primaryFontAsset");
1399+
if (primaryTypeface != null) {
1400+
builder.setPrimaryTextFont(primaryTypeface);
1401+
} else {
1402+
Log.e("InstabugModule", "Failed to load primary font");
1403+
}
1404+
}
1405+
1406+
if (themeConfig.hasKey("secondaryFontPath") || themeConfig.hasKey("secondaryFontAsset")) {
1407+
Typeface secondaryTypeface = getTypeface(themeConfig, "secondaryFontPath", "secondaryFontAsset");
1408+
if (secondaryTypeface != null) {
1409+
builder.setSecondaryTextFont(secondaryTypeface);
1410+
} else {
1411+
Log.e("InstabugModule", "Failed to load secondary font");
1412+
}
1413+
}
1414+
1415+
if (themeConfig.hasKey("ctaFontPath") || themeConfig.hasKey("ctaFontAsset")) {
1416+
Typeface ctaTypeface = getTypeface(themeConfig, "ctaFontPath", "ctaFontAsset");
1417+
if (ctaTypeface != null) {
1418+
builder.setCtaTextFont(ctaTypeface);
1419+
} else {
1420+
Log.e("InstabugModule", "Failed to load CTA font");
1421+
}
1422+
}
1423+
1424+
IBGTheme theme = builder.build();
1425+
Instabug.setTheme(theme);
1426+
1427+
} catch (Exception e) {
1428+
e.printStackTrace();
1429+
}
1430+
}
1431+
});
1432+
}
1433+
1434+
/**
1435+
* Retrieves a color value from the ReadableMap.
1436+
*
1437+
* @param map The ReadableMap object.
1438+
* @param key The key to look for.
1439+
* @return The parsed color as an integer, or black if missing or invalid.
1440+
*/
1441+
private int getColor(ReadableMap map, String key) {
1442+
try {
1443+
if (map != null && map.hasKey(key) && !map.isNull(key)) {
1444+
String colorString = map.getString(key);
1445+
return Color.parseColor(colorString);
1446+
}
1447+
} catch (Exception e) {
1448+
e.printStackTrace();
1449+
}
1450+
return Color.BLACK;
1451+
}
1452+
1453+
/**
1454+
* Retrieves a text style from the ReadableMap.
1455+
*
1456+
* @param map The ReadableMap object.
1457+
* @param key The key to look for.
1458+
* @return The corresponding Typeface style, or Typeface.NORMAL if missing or invalid.
1459+
*/
1460+
private int getTextStyle(ReadableMap map, String key) {
1461+
try {
1462+
if (map != null && map.hasKey(key) && !map.isNull(key)) {
1463+
String style = map.getString(key);
1464+
switch (style.toLowerCase()) {
1465+
case "bold":
1466+
return Typeface.BOLD;
1467+
case "italic":
1468+
return Typeface.ITALIC;
1469+
case "bold_italic":
1470+
return Typeface.BOLD_ITALIC;
1471+
case "normal":
1472+
default:
1473+
return Typeface.NORMAL;
1474+
}
1475+
}
1476+
} catch (Exception e) {
1477+
e.printStackTrace();
1478+
}
1479+
return Typeface.NORMAL;
1480+
}
1481+
1482+
private Typeface getTypeface(ReadableMap map, String fileKey, String assetKey) {
1483+
try {
1484+
if (fileKey != null && map.hasKey(fileKey) && !map.isNull(fileKey)) {
1485+
String fontPath = map.getString(fileKey);
1486+
String fileName = getFileName(fontPath);
1487+
1488+
try {
1489+
Typeface typeface = Typeface.create(fileName, Typeface.NORMAL);
1490+
if (typeface != null && !typeface.equals(Typeface.DEFAULT)) {
1491+
return typeface;
1492+
}
1493+
} catch (Exception e) {
1494+
e.printStackTrace();
1495+
}
1496+
1497+
try {
1498+
Typeface typeface = Typeface.createFromAsset(getReactApplicationContext().getAssets(), "fonts/" + fileName);
1499+
return typeface;
1500+
} catch (Exception e) {
1501+
e.printStackTrace();
1502+
}
1503+
}
1504+
1505+
if (assetKey != null && map.hasKey(assetKey) && !map.isNull(assetKey)) {
1506+
String assetPath = map.getString(assetKey);
1507+
String fileName = getFileName(assetPath);
1508+
try {
1509+
Typeface typeface = Typeface.createFromAsset(getReactApplicationContext().getAssets(), "fonts/" + fileName);
1510+
return typeface;
1511+
} catch (Exception e) {
1512+
e.printStackTrace();
1513+
}
1514+
}
1515+
} catch (Exception e) {
1516+
e.printStackTrace();
1517+
}
1518+
1519+
return Typeface.DEFAULT;
1520+
}
1521+
1522+
/**
1523+
* Extracts the filename from a path, removing any directory prefixes.
1524+
*
1525+
* @param path The full path to the file
1526+
* @return Just the filename with extension
1527+
*/
1528+
private String getFileName(String path) {
1529+
if (path == null || path.isEmpty()) {
1530+
return path;
1531+
}
1532+
1533+
int lastSeparator = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
1534+
if (lastSeparator >= 0 && lastSeparator < path.length() - 1) {
1535+
return path.substring(lastSeparator + 1);
1536+
}
1537+
1538+
return path;
1539+
}
1540+
1541+
/**
1542+
* Enables or disables displaying in full-screen mode, hiding the status and navigation bars.
1543+
* @param isEnabled A boolean to enable/disable setFullscreen.
1544+
*/
1545+
@ReactMethod
1546+
public void setFullscreen(final boolean isEnabled) {
1547+
MainThreadHandler.runOnMainThread(new Runnable() {
1548+
@Override
1549+
public void run() {
1550+
try {
1551+
Instabug.setFullscreen(isEnabled);
1552+
} catch (Exception e) {
1553+
e.printStackTrace();
1554+
}
1555+
}
1556+
});
1557+
}
13541558
}

android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,4 +704,75 @@ public void testGetNetworkBodyMaxSize_resolvesPromiseWithExpectedValue() {
704704
verify(promise).resolve(expected);
705705
}
706706

707+
@Test
708+
public void testEnablSetFullScreen() {
709+
boolean isEnabled = true;
710+
711+
// when
712+
rnModule.setFullscreen(isEnabled);
713+
714+
// then
715+
verify(Instabug.class, times(1));
716+
Instabug.setFullscreen(isEnabled);
717+
}
718+
719+
@Test
720+
public void testDisableSetFullScreen() {
721+
// given
722+
boolean isEnabled = false;
723+
724+
// when
725+
rnModule.setFullscreen(isEnabled);
726+
727+
// then
728+
verify(Instabug.class, times(1));
729+
Instabug.setFullscreen(isEnabled);
730+
}
731+
732+
@Test
733+
public void testSetTheme() {
734+
// given
735+
JavaOnlyMap themeConfig = new JavaOnlyMap();
736+
themeConfig.putString("primaryColor", "#FF0000");
737+
themeConfig.putString("primaryTextColor", "#00FF00");
738+
themeConfig.putString("secondaryTextColor", "#0000FF");
739+
themeConfig.putString("titleTextColor", "#FFFF00");
740+
themeConfig.putString("backgroundColor", "#FF00FF");
741+
themeConfig.putString("primaryTextStyle", "bold");
742+
themeConfig.putString("secondaryTextStyle", "italic");
743+
themeConfig.putString("ctaTextStyle", "bold");
744+
themeConfig.putString("primaryFontPath", "TestFont.ttf");
745+
themeConfig.putString("secondaryFontPath", "fonts/AnotherFont.ttf");
746+
themeConfig.putString("ctaFontPath", "./assets/fonts/CTAFont.ttf");
747+
748+
// Mock IBGTheme.Builder
749+
com.instabug.library.model.IBGTheme.Builder mockBuilder = mock(com.instabug.library.model.IBGTheme.Builder.class);
750+
com.instabug.library.model.IBGTheme mockTheme = mock(com.instabug.library.model.IBGTheme.class);
751+
752+
try (MockedConstruction<com.instabug.library.model.IBGTheme.Builder> mockedBuilder = mockConstruction(
753+
com.instabug.library.model.IBGTheme.Builder.class,
754+
(mock, context) -> {
755+
when(mock.setPrimaryColor(anyInt())).thenReturn(mock);
756+
when(mock.setPrimaryTextColor(anyInt())).thenReturn(mock);
757+
when(mock.setSecondaryTextColor(anyInt())).thenReturn(mock);
758+
when(mock.setTitleTextColor(anyInt())).thenReturn(mock);
759+
when(mock.setBackgroundColor(anyInt())).thenReturn(mock);
760+
when(mock.setPrimaryTextStyle(anyInt())).thenReturn(mock);
761+
when(mock.setSecondaryTextStyle(anyInt())).thenReturn(mock);
762+
when(mock.setCtaTextStyle(anyInt())).thenReturn(mock);
763+
when(mock.setPrimaryTextFont(any())).thenReturn(mock);
764+
when(mock.setSecondaryTextFont(any())).thenReturn(mock);
765+
when(mock.setCtaTextFont(any())).thenReturn(mock);
766+
when(mock.build()).thenReturn(mockTheme);
767+
})) {
768+
769+
// when
770+
rnModule.setTheme(themeConfig);
771+
772+
// then
773+
verify(Instabug.class, times(1));
774+
Instabug.setTheme(mockTheme);
775+
}
776+
}
777+
707778
}

examples/default/ios/InstabugTests/InstabugSampleTests.m

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,73 @@ - (void)testGetNetworkBodyMaxSize {
651651

652652
OCMVerify(ClassMethod([mock getNetworkBodyMaxSize]));
653653
}
654+
- (void)testSetTheme {
655+
id mock = OCMClassMock([Instabug class]);
656+
id mockTheme = OCMClassMock([IBGTheme class]);
657+
658+
// Create theme configuration dictionary
659+
NSDictionary *themeConfig = @{
660+
@"primaryColor": @"#FF0000",
661+
@"backgroundColor": @"#00FF00",
662+
@"titleTextColor": @"#0000FF",
663+
@"subtitleTextColor": @"#FFFF00",
664+
@"primaryTextColor": @"#FF00FF",
665+
@"secondaryTextColor": @"#00FFFF",
666+
@"callToActionTextColor": @"#800080",
667+
@"headerBackgroundColor": @"#808080",
668+
@"footerBackgroundColor": @"#C0C0C0",
669+
@"rowBackgroundColor": @"#FFFFFF",
670+
@"selectedRowBackgroundColor": @"#E6E6FA",
671+
@"rowSeparatorColor": @"#D3D3D3",
672+
@"primaryFontPath": @"TestFont.ttf",
673+
@"secondaryFontPath": @"fonts/AnotherFont.ttf",
674+
@"ctaFontPath": @"./assets/fonts/CTAFont.ttf"
675+
};
676+
677+
// Mock IBGTheme creation and configuration
678+
OCMStub([mockTheme primaryColor]).andReturn([UIColor redColor]);
679+
OCMStub([mockTheme backgroundColor]).andReturn([UIColor greenColor]);
680+
OCMStub([mockTheme titleTextColor]).andReturn([UIColor blueColor]);
681+
OCMStub([mockTheme subtitleTextColor]).andReturn([UIColor yellowColor]);
682+
OCMStub([mockTheme primaryTextColor]).andReturn([UIColor magentaColor]);
683+
OCMStub([mockTheme secondaryTextColor]).andReturn([UIColor cyanColor]);
684+
OCMStub([mockTheme callToActionTextColor]).andReturn([UIColor purpleColor]);
685+
OCMStub([mockTheme headerBackgroundColor]).andReturn([UIColor grayColor]);
686+
OCMStub([mockTheme footerBackgroundColor]).andReturn([UIColor lightGrayColor]);
687+
OCMStub([mockTheme rowBackgroundColor]).andReturn([UIColor whiteColor]);
688+
OCMStub([mockTheme selectedRowBackgroundColor]).andReturn([UIColor redColor]);
689+
OCMStub([mockTheme rowSeparatorColor]).andReturn([UIColor lightGrayColor]);
690+
OCMStub([mockTheme primaryTextFont]).andReturn([UIFont systemFontOfSize:17.0]);
691+
OCMStub([mockTheme secondaryTextFont]).andReturn([UIFont systemFontOfSize:17.0]);
692+
OCMStub([mockTheme callToActionTextFont]).andReturn([UIFont systemFontOfSize:17.0]);
693+
694+
// Mock theme property setting
695+
OCMStub([mockTheme setPrimaryColor:[OCMArg any]]).andReturn(mockTheme);
696+
OCMStub([mockTheme setBackgroundColor:[OCMArg any]]).andReturn(mockTheme);
697+
OCMStub([mockTheme setTitleTextColor:[OCMArg any]]).andReturn(mockTheme);
698+
OCMStub([mockTheme setSubtitleTextColor:[OCMArg any]]).andReturn(mockTheme);
699+
OCMStub([mockTheme setPrimaryTextColor:[OCMArg any]]).andReturn(mockTheme);
700+
OCMStub([mockTheme setSecondaryTextColor:[OCMArg any]]).andReturn(mockTheme);
701+
OCMStub([mockTheme setCallToActionTextColor:[OCMArg any]]).andReturn(mockTheme);
702+
OCMStub([mockTheme setHeaderBackgroundColor:[OCMArg any]]).andReturn(mockTheme);
703+
OCMStub([mockTheme setFooterBackgroundColor:[OCMArg any]]).andReturn(mockTheme);
704+
OCMStub([mockTheme setRowBackgroundColor:[OCMArg any]]).andReturn(mockTheme);
705+
OCMStub([mockTheme setSelectedRowBackgroundColor:[OCMArg any]]).andReturn(mockTheme);
706+
OCMStub([mockTheme setRowSeparatorColor:[OCMArg any]]).andReturn(mockTheme);
707+
OCMStub([mockTheme setPrimaryTextFont:[OCMArg any]]).andReturn(mockTheme);
708+
OCMStub([mockTheme setSecondaryTextFont:[OCMArg any]]).andReturn(mockTheme);
709+
OCMStub([mockTheme setCallToActionTextFont:[OCMArg any]]).andReturn(mockTheme);
710+
711+
// Mock Instabug.theme property
712+
OCMStub([mock theme]).andReturn(mockTheme);
713+
OCMStub([mock setTheme:[OCMArg any]]);
714+
715+
// Call the method
716+
[self.instabugBridge setTheme:themeConfig];
717+
718+
// Verify that setTheme was called
719+
OCMVerify([mock setTheme:[OCMArg any]]);
720+
}
654721

655722

656723
@end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
assets: ['./assets/fonts/'],
3+
};

ios/RNInstabug/InstabugReactBridge.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242

4343
- (void)setPrimaryColor:(UIColor *)color;
4444

45+
- (void)setTheme:(NSDictionary *)themeConfig;
46+
4547
- (void)appendTags:(NSArray *)tags;
4648

4749
- (void)resetTags;

0 commit comments

Comments
 (0)