diff --git a/.circleci/config.yml b/.circleci/config.yml index 2cc6ada92..0d5343223 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -237,7 +237,7 @@ jobs: - setup_flutter - android/start-emulator-and-run-tests: run-tests-working-directory: e2e - additional-avd-args: --device 3 + additional-avd-args: --device 25 system-image: system-images;android-33;default;x86_64 post-emulator-launch-assemble-command: cd packages/instabug_flutter/example && flutter build apk --debug test-command: dotnet test @@ -278,7 +278,7 @@ jobs: command: flutter build ios --simulator - run: name: Run E2E Tests - no_output_timeout: 30m + no_output_timeout: 20m working_directory: e2e command: dotnet test diff --git a/e2e/BugReportingTests.cs b/e2e/BugReportingTests.cs index f938cfcbd..f48afbf3b 100644 --- a/e2e/BugReportingTests.cs +++ b/e2e/BugReportingTests.cs @@ -1,9 +1,9 @@ -using System.Drawing; using E2E.Utils; using Xunit; using Instabug.Captain; using NoSuchElementException = OpenQA.Selenium.NoSuchElementException; +using System.Drawing; namespace E2E; @@ -46,6 +46,9 @@ public void ReportABug() [Fact] public void FloatingButtonInvocationEvent() { + + Console.WriteLine("FloatingButtonInvocationEvent"); + captain.FindById( android: "instabug_floating_button", ios: "IBGFloatingButtonAccessibilityIdentifier" @@ -57,9 +60,13 @@ public void FloatingButtonInvocationEvent() [Fact] public void ShakeInvocationEvent() { + + Console.WriteLine("ShakeInvocationEvent"); + if (!Platform.IsIOS) return; - captain.FindByText("Shake").Tap(); + + captain.FindByTextScroll("Shake").Tap(); captain.Shake(); @@ -69,8 +76,12 @@ public void ShakeInvocationEvent() [Fact] public void TwoFingersSwipeLeftInvocationEvent() { - ScrollUp(); - captain.FindByText("Two Fingers Swipe Left").Tap(); + + Console.WriteLine("TwoFingersSwipeLeftInvocationEvent"); + + + + captain.FindByTextScroll("Two Fingers Swipe Left").Tap(); Thread.Sleep(500); @@ -89,7 +100,11 @@ public void TwoFingersSwipeLeftInvocationEvent() [Fact] public void NoneInvocationEvent() { - captain.FindByText("None").Tap(); + + Console.WriteLine("NoneInvocationEvent"); + + + captain.FindByTextScroll("None").Tap(); captain.WaitForAssertion(() => Assert.Throws(() => @@ -105,7 +120,13 @@ public void NoneInvocationEvent() [Fact] public void ManualInvocation() { - captain.FindByText("Invoke").Tap(); + + + Console.WriteLine("ManualInvocation"); + + + + captain.FindByTextScroll("Invoke").Tap(); AssertOptionsPromptIsDisplayed(); } @@ -113,14 +134,24 @@ public void ManualInvocation() [Fact] public void MultipleScreenshotsInReproSteps() { - ScrollDownLittle(); - captain.FindByText("Enter screen name").Tap(); + + Console.WriteLine("MultipleScreenshotsInReproSteps"); + + + + +captain.FindByTextScroll("Enter screen name").Tap(); captain.Type("My Screen"); captain.HideKeyboard(); - captain.FindByText("Report Screen Change").Tap(); - captain.FindByText("Send Bug Report").Tap(); + captain.HideKeyboard(); + Thread.Sleep(500); + + captain.FindByTextScroll("Report Screen Change")?.Tap(); + Thread.Sleep(500); + captain.FindByTextScroll("Send Bug Report")?.Tap(); + captain.FindById( android: "instabug_text_view_repro_steps_disclaimer", ios: "IBGBugVCReproStepsDisclaimerAccessibilityIdentifier" @@ -136,27 +167,30 @@ public void MultipleScreenshotsInReproSteps() [Fact(Skip = "The test is flaky on iOS so we're skipping it to unblock the v13.2.0 release")] public void ChangeReportTypes() { - ScrollUp(); - captain.FindByText("Bug", exact: true).Tap(); + + Console.WriteLine("ChangeReportTypes"); + + + captain.FindByTextScroll("Bug", exact: true).Tap(); if (Platform.IsAndroid) { - captain.FindByText("Invoke").Tap(); + captain.FindByTextScroll("Invoke").Tap(); // Shows bug reporting screen Assert.True(captain.FindById("ib_bug_scroll_view").Displayed); // Close bug reporting screen captain.GoBack(); - captain.FindByText("DISCARD").Tap(); + captain.FindByTextScroll("DISCARD").Tap(); Thread.Sleep(500); } - captain.FindByText("Feedback").Tap(); + captain.FindByTextScroll("Feedback").Tap(); - captain.FindByText("Invoke").Tap(); + captain.FindByTextScroll("Invoke").Tap(); // Shows both bug reporting and feature requests in prompt options AssertOptionsPromptIsDisplayed(); @@ -169,10 +203,12 @@ public void ChangeReportTypes() [Fact] public void ChangeFloatingButtonEdge() { - ScrollDown(); - captain.FindByText("Move Floating Button to Left").Tap(); - Thread.Sleep(500); + Console.WriteLine("ChangeFloatingButtonEdge"); + + + captain.FindByTextScroll("Move Floating Button to Left",false,false)?.Tap(); + captain.WaitForAssertion(() => { @@ -189,16 +225,16 @@ public void ChangeFloatingButtonEdge() [Fact] public void OnDismissCallbackIsCalled() { - ScrollDownLittle(); - captain.FindByText("Set On Dismiss Callback").Tap(); - captain.FindByText("Invoke").Tap(); + captain.FindByTextScroll("Set On Dismiss Callback",false,false).Tap(); + captain.FindByTextScroll("Invoke",false,false).Tap(); AssertOptionsPromptIsDisplayed(); - captain.FindByText("Cancel").Tap(); + captain.FindByTextScroll("Cancel").Tap(); var popUpText = captain.FindByText("onDismiss callback called with DismissType.cancel and ReportType.other"); Assert.True(popUpText.Displayed); + } } diff --git a/e2e/FeatureRequestsTests.cs b/e2e/FeatureRequestsTests.cs index 41c97f684..724758bee 100644 --- a/e2e/FeatureRequestsTests.cs +++ b/e2e/FeatureRequestsTests.cs @@ -10,10 +10,10 @@ public class FeatureRequestsTests : CaptainTest [Fact] public void ShowFeatureRequetsScreen() { - ScrollDown(); - ScrollDown(); - captain.FindByText("Show Feature Requests").Tap(); + Console.WriteLine("ShowFeatureRequetsScreen"); + + captain.FindByTextScroll("Show Feature Requests",false,false).Tap(); var screenTitle = captain.FindById( android: "ib_fr_toolbar_main", diff --git a/e2e/InstabugTests.cs b/e2e/InstabugTests.cs index 1b67ae8cd..e376f87bf 100644 --- a/e2e/InstabugTests.cs +++ b/e2e/InstabugTests.cs @@ -11,14 +11,16 @@ public class InstabugTests : CaptainTest [Fact] public void ChangePrimaryColor() { + Console.WriteLine("ChangePrimaryColor"); + var color = "#FF0000"; var expected = Color.FromArgb(255, 0, 0); - captain.FindByText("Enter primary color").Tap(); + captain.FindByTextScroll("Enter primary color").Tap(); captain.Type(color); captain.HideKeyboard(); - captain.FindByText("Change Primary Color").Tap(); + captain.FindByTextScroll("Change Primary Color").Tap(); captain.WaitForAssertion(() => { diff --git a/e2e/SurveysTests.cs b/e2e/SurveysTests.cs index 1ed0eba48..0d67f99ab 100644 --- a/e2e/SurveysTests.cs +++ b/e2e/SurveysTests.cs @@ -10,8 +10,9 @@ public class SurveysTests : CaptainTest [Fact] public void ShowManualSurvey() { - ScrollDownLittle(); - captain.FindByText("Show Manual Survey").Tap(); + Console.WriteLine("ShowManualSurvey"); + + captain.FindByTextScroll("Show Manual Survey",false,false).Tap(); captain.WaitForAssertion(() => { diff --git a/e2e/Utils/CaptainTest.cs b/e2e/Utils/CaptainTest.cs index 3b42f13cb..e32255d4b 100644 --- a/e2e/Utils/CaptainTest.cs +++ b/e2e/Utils/CaptainTest.cs @@ -1,5 +1,7 @@ using System.Drawing; using Instabug.Captain; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium.MultiTouch; namespace E2E.Utils; @@ -10,6 +12,7 @@ public class CaptainTest : IDisposable AndroidApp = Path.GetFullPath("../../../../packages/instabug_flutter/example/build/app/outputs/flutter-apk/app-debug.apk"), AndroidAppId = "com.instabug.flutter.example", AndroidVersion = "13", + IosApp = Path.GetFullPath("../../../../packages/instabug_flutter/example/build/ios/iphonesimulator/Runner.app"), IosAppId = "com.instabug.InstabugSample", IosVersion = "17.2", @@ -28,28 +31,5 @@ public void Dispose() captain.RestartApp(); } - protected void ScrollDown() - { - captain.Swipe( - start: new Point(captain.Window.Size.Width / 2, captain.Window.Size.Height - 200), - end: new Point(captain.Window.Size.Width / 2, 250) - ); - } - - protected void ScrollDownLittle() - { - captain.Swipe( - start: new Point(captain.Window.Size.Width / 2, captain.Window.Size.Height - 200), - end: new Point(captain.Window.Size.Width / 2, captain.Window.Size.Height - 220) - ); - } - - protected void ScrollUp() - { - captain.Swipe( - start: new Point(captain.Window.Size.Width / 2, 250), - end: new Point(captain.Window.Size.Width / 2, captain.Window.Size.Height - 200) - ); - } } diff --git a/packages/instabug_flutter/android/build.gradle b/packages/instabug_flutter/android/build.gradle index e3b5d0bca..dd0ff1fc0 100644 --- a/packages/instabug_flutter/android/build.gradle +++ b/packages/instabug_flutter/android/build.gradle @@ -16,6 +16,13 @@ rootProject.allprojects { repositories { google() mavenCentral() + maven { + url "https://mvn.instabug.com/nexus/repository/instabug-internal/" + credentials { + username "instabug" + password System.getenv('INSTABUG_REPOSITORY_PASSWORD') + } + } } } @@ -44,7 +51,8 @@ android { } dependencies { - api 'com.instabug.library:instabug:14.1.0' + api 'com.instabug.library:instabug:14.2.0.6611870-SNAPSHOT' + testImplementation 'junit:junit:4.13.2' testImplementation "org.mockito:mockito-inline:3.12.1" testImplementation "io.mockk:mockk:1.13.13" diff --git a/packages/instabug_flutter/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java b/packages/instabug_flutter/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java index 45ee1d9cc..503d1a643 100644 --- a/packages/instabug_flutter/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java +++ b/packages/instabug_flutter/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java @@ -44,6 +44,7 @@ import java.io.InputStream; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -126,6 +127,16 @@ public void init(@NonNull String token, @NonNull List invocationEvents, .build(); Instabug.setScreenshotProvider(screenshotProvider); + + try{ + Class myClass = Class.forName("com.instabug.library.Instabug"); + // Enable/Disable native user steps capturing + Method method = myClass.getDeclaredMethod("shouldDisableNativeUserStepsCapturing", boolean.class); + method.setAccessible(true); + method.invoke(null,true); + } catch (Exception e) { + e.printStackTrace(); + } } @Override @@ -159,6 +170,33 @@ public void logOut() { Instabug.logoutUser(); } + @Override + public void setEnableUserSteps(@NonNull Boolean isEnabled) { + Instabug.setTrackingUserStepsState(isEnabled ? Feature.State.ENABLED : Feature.State.DISABLED); + } + + @Override + + public void logUserSteps(@NonNull String gestureType, @NonNull String message,@Nullable String viewName) { + try { + final String stepType = ArgsRegistry.gestureStepType.get(gestureType); + final long timeStamp = System.currentTimeMillis(); + String view = ""; + + Method method = Reflection.getMethod(Class.forName("com.instabug.library.Instabug"), "addUserStep", + long.class, String.class, String.class, String.class, String.class); + if (method != null) { + if (viewName != null){ + view = viewName; + } + + method.invoke(null, timeStamp, stepType, message, null, view); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + @Override public void setLocale(@NonNull String locale) { final InstabugLocale resolvedLocale = ArgsRegistry.locales.get(locale); @@ -362,6 +400,13 @@ public void reportScreenChange(@NonNull String screenName) { if (method != null) { method.invoke(null, null, screenName); } + + Method reportViewChange = Reflection.getMethod(Class.forName("com.instabug.library.Instabug"), "reportCurrentViewChange", + String.class); + if (reportViewChange != null) { + reportViewChange.invoke(null,screenName); + } + } catch (Exception e) { e.printStackTrace(); } @@ -497,7 +542,7 @@ public void willRedirectToStore() { Instabug.willRedirectToStore(); } - public static void setScreenshotCaptor(ScreenshotCaptor screenshotCaptor,InternalCore internalCore) { + public static void setScreenshotCaptor(ScreenshotCaptor screenshotCaptor, InternalCore internalCore) { internalCore._setScreenshotCaptor(new com.instabug.library.screenshot.ScreenshotCaptor() { @Override public void capture(@NonNull ScreenshotRequest screenshotRequest) { diff --git a/packages/instabug_flutter/android/src/main/java/com/instabug/flutter/util/ArgsRegistry.java b/packages/instabug_flutter/android/src/main/java/com/instabug/flutter/util/ArgsRegistry.java index 222a72836..ec37144b8 100644 --- a/packages/instabug_flutter/android/src/main/java/com/instabug/flutter/util/ArgsRegistry.java +++ b/packages/instabug_flutter/android/src/main/java/com/instabug/flutter/util/ArgsRegistry.java @@ -16,6 +16,7 @@ import com.instabug.library.invocation.InstabugInvocationEvent; import com.instabug.library.invocation.util.InstabugFloatingButtonEdge; import com.instabug.library.invocation.util.InstabugVideoRecordingButtonPosition; +import com.instabug.library.model.StepType; import com.instabug.library.ui.onboarding.WelcomeMessage; import java.util.HashMap; @@ -53,11 +54,12 @@ public T get(Object key) { put("InvocationOption.disablePostSendingDialog", Option.DISABLE_POST_SENDING_DIALOG); }}; + public static final ArgsMap colorThemes = new ArgsMap() {{ put("ColorTheme.light", InstabugColorTheme.InstabugColorThemeLight); put("ColorTheme.dark", InstabugColorTheme.InstabugColorThemeDark); }}; - public static ArgsMap nonFatalExceptionLevel = new ArgsMap() {{ + public static ArgsMap nonFatalExceptionLevel = new ArgsMap() {{ put("NonFatalExceptionLevel.critical", IBGNonFatalException.Level.CRITICAL); put("NonFatalExceptionLevel.error", IBGNonFatalException.Level.ERROR); put("NonFatalExceptionLevel.warning", IBGNonFatalException.Level.WARNING); @@ -206,4 +208,13 @@ public T get(Object key) { put("CustomTextPlaceHolderKey.messagesNotificationAndOthers", Key.CHATS_MULTIPLE_MESSAGE_NOTIFICATION); put("CustomTextPlaceHolderKey.insufficientContentMessage", Key.COMMENT_FIELD_INSUFFICIENT_CONTENT); }}; + + public static final ArgsMap gestureStepType = new ArgsMap() {{ + put("GestureType.swipe", StepType.SWIPE); + put("GestureType.scroll", StepType.SCROLL); + put("GestureType.tap", StepType.TAP); + put("GestureType.pinch", StepType.PINCH); + put("GestureType.longPress", StepType.LONG_PRESS); + put("GestureType.doubleTap", StepType.DOUBLE_TAP); + }}; } diff --git a/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index a9c8ba464..f0de4c401 100644 --- a/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -6,8 +6,10 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; @@ -30,6 +32,7 @@ import com.instabug.bug.BugReporting; import com.instabug.flutter.generated.InstabugPigeon; import com.instabug.flutter.modules.InstabugApi; +import com.instabug.flutter.util.ArgsRegistry; import com.instabug.flutter.util.GlobalMocks; import com.instabug.flutter.util.MockReflected; import com.instabug.library.Feature; @@ -654,4 +657,36 @@ public void testSetScreenshotCaptor() { InstabugApi.setScreenshotCaptor(any(), internalCore); verify(internalCore)._setScreenshotCaptor(any(ScreenshotCaptor.class)); } + + @Test + public void testSetUserStepsEnabledGivenTrue() { + boolean isEnabled = true; + + api.setEnableUserSteps(isEnabled); + + mInstabug.verify(() -> Instabug.setTrackingUserStepsState(Feature.State.ENABLED)); + } + + @Test + public void testSetUserStepsEnabledGivenFalse() { + boolean isEnabled = false; + + api.setEnableUserSteps(isEnabled); + + mInstabug.verify(() -> Instabug.setTrackingUserStepsState(Feature.State.DISABLED)); + } + + @Test + public void testLogUserSteps() { + + final String gestureType = "GestureType.tap"; + final String message = "message"; + final String view = "view"; + + api.logUserSteps(gestureType, message,view); + + reflected.verify(() -> MockReflected.addUserStep(anyLong(), eq(ArgsRegistry.gestureStepType.get(gestureType)), eq(message), isNull(), eq(view))); + + } + } diff --git a/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java b/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java index 0795acf77..4b37a6d6a 100644 --- a/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java +++ b/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java @@ -106,6 +106,14 @@ public static void setUp() throws NoSuchMethodException { Method mEndScreenLoadingCP = MockReflected.class.getDeclaredMethod("endScreenLoadingCP", Long.class, Long.class); mEndScreenLoadingCP.setAccessible(true); reflection.when(() -> Reflection.getMethod(Class.forName("com.instabug.apm.APM"), "endScreenLoadingCP", Long.class, Long.class)).thenReturn(mEndScreenLoadingCP); + + + Method mAddUserStepCp = MockReflected.class.getDeclaredMethod("addUserStep", + long.class, String.class, String.class, String.class, String.class); + mAddUserStepCp.setAccessible(true); + reflection.when(() -> Reflection.getMethod(Class.forName("com.instabug.library.Instabug"), "addUserStep", + long.class, String.class, String.class, String.class, String.class)).thenReturn(mAddUserStepCp);; + } public static void close() { diff --git a/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/util/MockReflected.java b/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/util/MockReflected.java index 42c85664b..f73364b2e 100644 --- a/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/util/MockReflected.java +++ b/packages/instabug_flutter/android/src/test/java/com/instabug/flutter/util/MockReflected.java @@ -1,10 +1,12 @@ package com.instabug.flutter.util; import android.graphics.Bitmap; + import androidx.annotation.Nullable; import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.crash.models.IBGNonFatalException; +import com.instabug.library.model.StepType; import org.json.JSONObject; @@ -19,33 +21,46 @@ public class MockReflected { /** * Instabug.reportScreenChange */ - public static void reportScreenChange(Bitmap screenshot, String name) {} + public static void reportScreenChange(Bitmap screenshot, String name) { + } /** * Instabug.setCustomBrandingImage */ - public static void setCustomBrandingImage(Bitmap light, Bitmap dark) {} + public static void setCustomBrandingImage(Bitmap light, Bitmap dark) { + } /** * Instabug.setCurrentPlatform */ - public static void setCurrentPlatform(int platform) {} + public static void setCurrentPlatform(int platform) { + } /** * APMNetworkLogger.log */ - public static void apmNetworkLog(long requestStartTime, long requestDuration, String requestHeaders, String requestBody, long requestBodySize, String requestMethod, String requestUrl, String responseHeaders, String responseBody, String responseBodySize, long statusCode, int responseContentType, String errorMessage, String var18, @Nullable String gqlQueryName, @Nullable String serverErrorMessage, @Nullable APMCPNetworkLog.W3CExternalTraceAttributes w3CExternalTraceAttributes) {} + public static void apmNetworkLog(long requestStartTime, long requestDuration, String requestHeaders, String requestBody, long requestBodySize, String requestMethod, String requestUrl, String responseHeaders, String responseBody, String responseBodySize, long statusCode, int responseContentType, String errorMessage, String var18, @Nullable String gqlQueryName, @Nullable String serverErrorMessage, @Nullable APMCPNetworkLog.W3CExternalTraceAttributes w3CExternalTraceAttributes) { + } /** * CrashReporting.reportException */ - public static void crashReportException(JSONObject exception, boolean isHandled) {} - public static void crashReportException(JSONObject exception, boolean isHandled, Map userAttributes, JSONObject fingerPrint, IBGNonFatalException.Level level) {} + public static void crashReportException(JSONObject exception, boolean isHandled) { + } + + public static void crashReportException(JSONObject exception, boolean isHandled, Map userAttributes, JSONObject fingerPrint, IBGNonFatalException.Level level) { + } + + public static void startUiTraceCP(String screenName, Long microTimeStamp, Long traceId) { + } - public static void startUiTraceCP(String screenName, Long microTimeStamp, Long traceId) {} + public static void reportScreenLoadingCP(Long startTimeStampMicro, Long durationMicro, Long uiTraceId) { + } - public static void reportScreenLoadingCP(Long startTimeStampMicro, Long durationMicro, Long uiTraceId) {} + public static void endScreenLoadingCP(Long timeStampMicro, Long uiTraceId) { + } - public static void endScreenLoadingCP(Long timeStampMicro, Long uiTraceId) {} + public static void addUserStep(long timestamp, @StepType String stepType, String message, String label, String viewType) { + } } diff --git a/packages/instabug_flutter/example/android/build.gradle b/packages/instabug_flutter/example/android/build.gradle index e7ee2eaf8..e00c736e4 100644 --- a/packages/instabug_flutter/example/android/build.gradle +++ b/packages/instabug_flutter/example/android/build.gradle @@ -15,6 +15,7 @@ allprojects { repositories { google() mavenCentral() + } } diff --git a/packages/instabug_flutter/example/ios/InstabugTests/ArgsRegistryTests.m b/packages/instabug_flutter/example/ios/InstabugTests/ArgsRegistryTests.m index bded153fd..a59f88d4e 100644 --- a/packages/instabug_flutter/example/ios/InstabugTests/ArgsRegistryTests.m +++ b/packages/instabug_flutter/example/ios/InstabugTests/ArgsRegistryTests.m @@ -188,6 +188,21 @@ - (void)testLocales { } } +- (void)testUserStepsGesture { + NSArray *values = @[ + @(IBGUIEventTypeSwipe), + @(IBGUIEventTypeScroll), + @(IBGUIEventTypeTap), + @(IBGUIEventTypePinch), + @(IBGUIEventTypeLongPress), + @(IBGUIEventTypeDoubleTap), + ]; + + for (NSNumber *value in values) { + XCTAssertTrue([[ArgsRegistry.userStepsGesture allValues] containsObject:value]); + } +} + - (void)testPlaceholders { NSArray *values = @[ kIBGShakeStartAlertTextStringName, diff --git a/packages/instabug_flutter/example/ios/InstabugTests/InstabugApiTests.m b/packages/instabug_flutter/example/ios/InstabugTests/InstabugApiTests.m index 7c0cc63dc..4d7898c2b 100644 --- a/packages/instabug_flutter/example/ios/InstabugTests/InstabugApiTests.m +++ b/packages/instabug_flutter/example/ios/InstabugTests/InstabugApiTests.m @@ -602,4 +602,25 @@ - (void)testisW3CFeatureFlagsEnabled { } +- (void)testSetEnableUserStepsIsEnabled{ + NSNumber *isEnabled = @1; + FlutterError *error; + + [self.api setEnableUserStepsIsEnabled:isEnabled error:&error]; + + OCMVerify([self.mInstabug setTrackUserSteps:YES]); + +} + +- (void)testLogUserStepsGestureType{ + NSString* message = @"message"; + NSString* view = @"viewName"; + FlutterError *error; + + [self.api logUserStepsGestureType:@"GestureType.tap" message:message viewName:view error: &error]; + + XCTAssertNil(error, @"Error should be nil"); + +} + @end diff --git a/packages/instabug_flutter/example/ios/Podfile b/packages/instabug_flutter/example/ios/Podfile index be56c059e..509b3e686 100644 --- a/packages/instabug_flutter/example/ios/Podfile +++ b/packages/instabug_flutter/example/ios/Podfile @@ -29,6 +29,8 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! + pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/feature-flutter_user_steps-base/14.1.0/Instabug.podspec' + # pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/feature-flutter-private-views-base/14.0.0/Instabug.podspec' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end diff --git a/packages/instabug_flutter/example/ios/Podfile.lock b/packages/instabug_flutter/example/ios/Podfile.lock index 5dd77dc53..4d0647f67 100644 --- a/packages/instabug_flutter/example/ios/Podfile.lock +++ b/packages/instabug_flutter/example/ios/Podfile.lock @@ -4,9 +4,6 @@ PODS: - instabug_flutter (14.0.0): - Flutter - Instabug (= 14.1.0) - - instabug_private_views (0.0.1): - - Flutter - - instabug_flutter - OCMock (3.6) - video_player_avfoundation (0.0.1): - Flutter @@ -14,34 +11,32 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) + - Instabug (from `https://ios-releases.instabug.com/custom/feature-flutter_user_steps-base/14.1.0/Instabug.podspec`) - instabug_flutter (from `.symlinks/plugins/instabug_flutter/ios`) - - instabug_private_views (from `.symlinks/plugins/instabug_private_views/ios`) - OCMock (= 3.6) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) SPEC REPOS: trunk: - - Instabug - OCMock EXTERNAL SOURCES: Flutter: :path: Flutter + Instabug: + :podspec: https://ios-releases.instabug.com/custom/feature-flutter_user_steps-base/14.1.0/Instabug.podspec instabug_flutter: :path: ".symlinks/plugins/instabug_flutter/ios" - instabug_private_views: - :path: ".symlinks/plugins/instabug_private_views/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Instabug: 8cbca8974168c815658133e2813f5ac3a36f8e20 + Instabug: 8eb6f63f3ac66f062025c15293549ab67150e9f9 instabug_flutter: a24751dfaedd29475da2af062d3e19d697438f72 - instabug_private_views: df53ff3f1cc842cb686d43e077099d3b36426a7f OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 -PODFILE CHECKSUM: bb7a3c60be3b970b608fa878e26d5fadfbeb1157 +PODFILE CHECKSUM: 87a326d297554318d9004349a35d82ffe3f0c228 COCOAPODS: 1.14.3 diff --git a/packages/instabug_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/instabug_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 858ba01e5..89e630a84 100644 --- a/packages/instabug_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/instabug_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -552,7 +552,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 56S6Q9SA8U; + DEVELOPMENT_TEAM = VF78H8LBT4; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -688,8 +688,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 56S6Q9SA8U; + DEVELOPMENT_TEAM = VF78H8LBT4; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -706,6 +708,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.instabug.InstabugSample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -720,7 +723,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 56S6Q9SA8U; + DEVELOPMENT_TEAM = VF78H8LBT4; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/instabug_flutter/example/ios/Runner/Info.plist b/packages/instabug_flutter/example/ios/Runner/Info.plist index 4e0eac976..b2d0e1954 100644 --- a/packages/instabug_flutter/example/ios/Runner/Info.plist +++ b/packages/instabug_flutter/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -22,6 +24,12 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSMicrophoneUsageDescription + Instabug needs access to your microphone so you can attach voice notes. + NSPhotoLibraryUsageDescription + Instabug needs access to your photo library so you can attach images. + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,13 +49,5 @@ UIViewControllerBasedStatusBarAppearance - NSMicrophoneUsageDescription - Instabug needs access to your microphone so you can attach voice notes. - NSPhotoLibraryUsageDescription - Instabug needs access to your photo library so you can attach images. - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/packages/instabug_flutter/example/lib/main.dart b/packages/instabug_flutter/example/lib/main.dart index abc04e020..95c49a49a 100644 --- a/packages/instabug_flutter/example/lib/main.dart +++ b/packages/instabug_flutter/example/lib/main.dart @@ -1,14 +1,20 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:developer'; import 'dart:io'; -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; -import 'package:instabug_flutter_example/src/screens/private_view_page.dart'; -import 'package:instabug_http_client/instabug_http_client.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; import 'package:instabug_flutter_example/src/app_routes.dart'; +import 'package:instabug_flutter_example/src/native/instabug_flutter_example_method_channel.dart'; +import 'package:instabug_flutter_example/src/screens/private_view_page.dart'; +import 'package:instabug_flutter_example/src/widget/instabug_button.dart'; +import 'package:instabug_flutter_example/src/widget/instabug_clipboard_input.dart'; +import 'package:instabug_flutter_example/src/widget/instabug_text_field.dart'; import 'package:instabug_flutter_example/src/widget/nested_view.dart'; +import 'package:instabug_flutter_example/src/widget/section_title.dart'; +import 'package:instabug_http_client/instabug_http_client.dart'; import 'package:instabug_flutter_example/src/native/instabug_flutter_example_method_channel.dart'; import 'package:instabug_flutter_example/src/widget/instabug_button.dart'; @@ -18,29 +24,19 @@ import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager import 'package:instabug_flutter_example/src/widget/section_title.dart'; -part 'src/screens/crashes_page.dart'; - -part 'src/screens/complex_page.dart'; - -part 'src/screens/apm_page.dart'; - -part 'src/screens/screen_capture_premature_extension_page.dart'; - -part 'src/screens/screen_loading_page.dart'; - -part 'src/screens/my_home_page.dart'; - part 'src/components/fatal_crashes_content.dart'; - -part 'src/components/non_fatal_crashes_content.dart'; - +part 'src/components/flows_content.dart'; part 'src/components/network_content.dart'; - +part 'src/components/non_fatal_crashes_content.dart'; part 'src/components/page.dart'; - part 'src/components/traces_content.dart'; - -part 'src/components/flows_content.dart'; +part 'src/screens/apm_page.dart'; +part 'src/screens/complex_page.dart'; +part 'src/screens/crashes_page.dart'; +part 'src/screens/my_home_page.dart'; +part 'src/screens/screen_capture_premature_extension_page.dart'; +part 'src/screens/screen_loading_page.dart'; +part 'src/screens/user_steps_page.dart'; void main() { runZonedGuarded( @@ -50,15 +46,17 @@ void main() { Instabug.init( token: 'ed6f659591566da19b67857e1b9d40ab', invocationEvents: [InvocationEvent.floatingButton], - debugLogsLevel: LogLevel.verbose, + debugLogsLevel: LogLevel.none, ); + Instabug.setWelcomeMessageMode(WelcomeMessageMode.disabled); + + FlutterError.onError = (FlutterErrorDetails details) { Zone.current.handleUncaughtError(details.exception, details.stack!); }; - - runApp(const MyApp()); + runApp(const InstabugUserSteps(child: MyApp())); // runApp(const MyApp()); }, CrashReporting.reportCrash, ); diff --git a/packages/instabug_flutter/example/lib/src/app_routes.dart b/packages/instabug_flutter/example/lib/src/app_routes.dart index 9175d5405..ee3c3b06b 100644 --- a/packages/instabug_flutter/example/lib/src/app_routes.dart +++ b/packages/instabug_flutter/example/lib/src/app_routes.dart @@ -15,4 +15,5 @@ final appRoutes = { const ScreenLoadingPage(), ScreenCapturePrematureExtensionPage.screenName: (BuildContext context) => const ScreenCapturePrematureExtensionPage(), + UserStepsPage.screenName: (BuildContext context) => const UserStepsPage() }; diff --git a/packages/instabug_flutter/example/lib/src/components/page.dart b/packages/instabug_flutter/example/lib/src/components/page.dart index de61d4b65..ccb977ce8 100644 --- a/packages/instabug_flutter/example/lib/src/components/page.dart +++ b/packages/instabug_flutter/example/lib/src/components/page.dart @@ -20,7 +20,9 @@ class Page extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( key: scaffoldKey, - appBar: AppBar(title: Text(title)), + appBar: AppBar( + title: Text(title), + ), body: SingleChildScrollView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only(top: 20.0), diff --git a/packages/instabug_flutter/example/lib/src/screens/my_home_page.dart b/packages/instabug_flutter/example/lib/src/screens/my_home_page.dart index 4c4b681a3..ae2d2c53e 100644 --- a/packages/instabug_flutter/example/lib/src/screens/my_home_page.dart +++ b/packages/instabug_flutter/example/lib/src/screens/my_home_page.dart @@ -175,6 +175,16 @@ class _MyHomePageState extends State { ); } + void _navigateToUserStepsPage() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const UserStepsPage(), + settings: const RouteSettings(name: UserStepsPage.screenName), + ), + ); + } + @override Widget build(BuildContext context) { return Page( @@ -356,6 +366,10 @@ class _MyHomePageState extends State { ), ], ), + InstabugButton( + text: 'User Steps', + onPressed: _navigateToUserStepsPage, + ), SectionTitle('FeatureFlags'), InstabugTextField( controller: featureFlagsController, diff --git a/packages/instabug_flutter/example/lib/src/screens/user_steps_page.dart b/packages/instabug_flutter/example/lib/src/screens/user_steps_page.dart new file mode 100644 index 000000000..f97cfacc2 --- /dev/null +++ b/packages/instabug_flutter/example/lib/src/screens/user_steps_page.dart @@ -0,0 +1,262 @@ +part of '../../main.dart'; + +class UserStepsPage extends StatefulWidget { + static const screenName = 'user_steps'; + + const UserStepsPage({Key? key}) : super(key: key); + + @override + _UserStepsPageState createState() => _UserStepsPageState(); +} + +class _UserStepsPageState extends State { + double _currentSliderValue = 20.0; + + RangeValues _currentRangeValues = const RangeValues(40, 80); + + String? _sliderStatus; + + bool? isChecked = true; + + int? _selectedValue = 1; + + bool light = true; + double _scale = 1.0; // Initial scale of the image + + TextEditingController _controller = TextEditingController(); + + String _currentValue = ''; + List _items = List.generate(20, (index) => 'Item ${index + 1}'); + + @override + void initState() { + super.initState(); + + _controller.addListener(() { + setState(() { + _currentValue = _controller.text; + }); + }); + } + + void _handleRadioValueChanged(int? value) { + setState(() { + _selectedValue = value; + }); + } + + @override + Widget build(BuildContext context) { + return Page( + title: 'User Steps', + children: [ + BackButton(), + NotificationListener( + onNotification: (ScrollNotification notification) { + return false; + }, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate( + 100, + (_) => InkWell( + onTap: () {}, + child: Container( + width: 100, + height: 100, + color: Colors.red, + margin: EdgeInsets.all(8), + ), + ), + ), + ), + ), + ), + SectionTitle('Sliders'), + Slider( + value: _currentSliderValue, + max: 100, + divisions: 5, + label: _currentSliderValue.round().toString(), + onChanged: (double value) { + setState(() { + _currentSliderValue = value; + }); + }, + ), + RangeSlider( + values: _currentRangeValues, + max: 100, + divisions: 5, + labels: RangeLabels( + _currentRangeValues.start.round().toString(), + _currentRangeValues.end.round().toString(), + ), + onChanged: (RangeValues values) { + setState(() { + _currentRangeValues = values; + }); + }, + ), + SectionTitle('Images'), + Row( + children: [ + Image.asset( + 'assets/img.png', + height: 100, + ), + Image.network( + "https://t3.ftcdn.net/jpg/00/50/07/64/360_F_50076454_TCvZEw37VyB5ZhcwEjkJHddtuV1cFmKY.jpg", + height: 100, + ), + ], + ), + InstabugButton(text: "I'm a button"), + ElevatedButton(onPressed: () {}, child: Text("data")), + SectionTitle('Toggles'), + Row( + children: [ + Checkbox( + tristate: true, + value: isChecked, + onChanged: (bool? value) { + setState(() { + isChecked = value; + }); + }, + ), + Radio( + value: 0, + groupValue: _selectedValue, + onChanged: _handleRadioValueChanged, + ), + Switch( + value: light, + activeColor: Colors.red, + onChanged: (bool value) { + setState(() { + light = value; + }); + }, + ), + ], + ), + GestureDetector( + onScaleUpdate: (details) { + setState(() { + _scale = details.scale; + _scale = _scale.clamp(1.0, + 3.0); // Limit zoom between 1x and 3x// Update scale based on pinch gesture + }); + }, + onScaleEnd: (details) { + // You can add logic to reset or clamp the scale if needed + if (_scale < 1.0) { + _scale = 1.0; // Prevent shrinking below original size + } + }, + child: Transform.scale( + scale: _scale, // Apply the scale transformation + child: Image.asset( + "assets/img.png", + height: 300, + ), + ), + ), + SectionTitle('TextInput'), + Column( + children: [ + Padding( + padding: EdgeInsets.all(16.0), // Set the padding value + child: Column( + children: [ + TextField( + key: Key('text_field'), + controller: _controller, + // Bind the controller to the TextField + decoration: InputDecoration( + labelText: "Type something in a text field with key", + border: OutlineInputBorder(), + ), + ), + TextField( + controller: _controller, + // Bind the controller to the TextField + decoration: InputDecoration( + labelText: "Private view", + border: OutlineInputBorder(), + ), + ), + TextField( + controller: _controller, + // Bind the controller to the TextField + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Password', + ), + ), + TextFormField( + obscureText: true, + controller: _controller, + // Bind the controller to the TextField + decoration: const InputDecoration( + icon: Icon(Icons.person), + hintText: 'What do people call you?', + labelText: 'Name *', + ), + onSaved: (String? value) { + // This optional block of code can be used to run + // code when the user saves the form. + }, + validator: (String? value) { + return (value != null && value.contains('@')) + ? 'Do not use the @ char.' + : null; + }, + ), + ], + ), + ), + ListView.builder( + itemCount: _items.length, + shrinkWrap: true, + itemBuilder: (context, index) { + return Dismissible( + key: Key(_items[index]), + // Unique key for each item + onDismissed: (direction) { + // Remove the item from the list + setState(() { + _items.removeAt(index); + }); + + // Show a snackbar or other UI feedback on dismissal + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Item dismissed')), + ); + }, + background: Container(color: Colors.red), + // Background color when swiped + direction: DismissDirection.endToStart, + // Swipe direction (left to right) + child: ListTile( + title: Text(_items[index]), + ), + ); + }, + ), + ], + ), + ], + ); + } + + @override + void dispose() { + _controller + .dispose(); // Dispose of the controller when the widget is destroyed + super.dispose(); + } +} diff --git a/packages/instabug_flutter/example/pubspec.lock b/packages/instabug_flutter/example/pubspec.lock index 44060645d..2fe784636 100644 --- a/packages/instabug_flutter/example/pubspec.lock +++ b/packages/instabug_flutter/example/pubspec.lock @@ -136,13 +136,6 @@ packages: relative: true source: path version: "2.4.0" - instabug_private_views: - dependency: "direct overridden" - description: - path: "../../instabug_private_views" - relative: true - source: path - version: "1.0.1" leak_tracker: dependency: transitive description: @@ -328,10 +321,10 @@ packages: dependency: transitive description: name: video_player_avfoundation - sha256: cd5ab8a8bc0eab65ab0cea40304097edc46da574c8c1ecdee96f28cd8ef3792f + sha256: "33224c19775fd244be2d6e3dbd8e1826ab162877bd61123bf71890772119a2b7" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.6.5" video_player_platform_interface: dependency: transitive description: diff --git a/packages/instabug_flutter/example/pubspec.yaml b/packages/instabug_flutter/example/pubspec.yaml index ec7be1ada..46ab1d7c8 100644 --- a/packages/instabug_flutter/example/pubspec.yaml +++ b/packages/instabug_flutter/example/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: sdk: flutter http: ^0.13.0 instabug_flutter: - path: '../' + path: "../" instabug_http_client: ^2.4.0 video_player: dev_dependencies: @@ -37,7 +37,7 @@ dev_dependencies: dependency_overrides: instabug_flutter: - path: '../' + path: "../" # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -51,7 +51,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/img.png + - assets/img.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. diff --git a/packages/instabug_flutter/example/pubspec_overrides.yaml b/packages/instabug_flutter/example/pubspec_overrides.yaml new file mode 100644 index 000000000..c41d20248 --- /dev/null +++ b/packages/instabug_flutter/example/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: instabug_flutter,instabug_http_client +dependency_overrides: + instabug_flutter: + path: ../ + instabug_http_client: + path: ../../instabug_http_client diff --git a/packages/instabug_flutter/ios/Classes/Modules/InstabugApi.m b/packages/instabug_flutter/ios/Classes/Modules/InstabugApi.m index 84dc9cf3f..ba809f00b 100644 --- a/packages/instabug_flutter/ios/Classes/Modules/InstabugApi.m +++ b/packages/instabug_flutter/ios/Classes/Modules/InstabugApi.m @@ -55,6 +55,7 @@ - (void)initToken:(NSString *)token invocationEvents:(NSArray *)invo [Instabug setSdkDebugLogsLevel:resolvedLogLevel]; [Instabug startWithToken:token invocationEvents:resolvedEvents]; + Instabug.sendEventsSwizzling = false; } - (void)showWithError:(FlutterError *_Nullable *_Nonnull)error { @@ -393,9 +394,31 @@ - (void)registerFeatureFlagChangeListenerWithError:(FlutterError * _Nullable __a return result; } +- (void)logUserStepsGestureType:(NSString *)gestureType message:(NSString *)message viewName:(NSString *)viewName error:(FlutterError * _Nullable __autoreleasing *)error +{ + @try { + + IBGUIEventType event = ArgsRegistry.userStepsGesture[gestureType].integerValue; + IBGUserStep *userStep = [[IBGUserStep alloc] initWithEvent:event automatic: YES]; + + userStep = [userStep setMessage: message]; + userStep = [userStep setViewTypeName:viewName]; + [userStep logUserStep]; + } + @catch (NSException *exception) { + NSLog(@"%@", exception); + + } +} + + +- (void)setEnableUserStepsIsEnabled:(nonnull NSNumber *)isEnabled error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + Instabug.trackUserSteps = isEnabled.boolValue; +} + + (void)setScreenshotMaskingHandler:(nullable void (^)(UIImage * _Nonnull __strong, void (^ _Nonnull __strong)(UIImage * _Nonnull __strong)))maskingHandler { - [Instabug setScreenshotMaskingHandler:maskingHandler]; + [Instabug setScreenshotMaskingHandler:maskingHandler]; } @end diff --git a/packages/instabug_flutter/ios/Classes/Util/ArgsRegistry.h b/packages/instabug_flutter/ios/Classes/Util/ArgsRegistry.h index a465365df..f6f5347bc 100644 --- a/packages/instabug_flutter/ios/Classes/Util/ArgsRegistry.h +++ b/packages/instabug_flutter/ios/Classes/Util/ArgsRegistry.h @@ -20,6 +20,8 @@ typedef NSDictionary ArgsDictionary; + (ArgsDictionary *)nonFatalExceptionLevel; + (ArgsDictionary *)locales; ++ (ArgsDictionary *)userStepsGesture; + + (NSDictionary *)placeholders; @end diff --git a/packages/instabug_flutter/ios/Classes/Util/ArgsRegistry.m b/packages/instabug_flutter/ios/Classes/Util/ArgsRegistry.m index 47de80d13..6cb9eae31 100644 --- a/packages/instabug_flutter/ios/Classes/Util/ArgsRegistry.m +++ b/packages/instabug_flutter/ios/Classes/Util/ArgsRegistry.m @@ -211,4 +211,15 @@ + (ArgsDictionary *)locales { }; } ++ (ArgsDictionary *) userStepsGesture { + return @{ + @"GestureType.swipe" : @(IBGUIEventTypeSwipe), + @"GestureType.scroll" : @(IBGUIEventTypeScroll), + @"GestureType.tap" : @(IBGUIEventTypeTap), + @"GestureType.pinch" : @(IBGUIEventTypePinch), + @"GestureType.longPress" : @(IBGUIEventTypeLongPress), + @"GestureType.doubleTap" : @(IBGUIEventTypeDoubleTap), + }; +} + @end diff --git a/packages/instabug_flutter/ios/Classes/Util/Instabug+CP.h b/packages/instabug_flutter/ios/Classes/Util/Instabug+CP.h index 79e988c29..5e1ef85bb 100644 --- a/packages/instabug_flutter/ios/Classes/Util/Instabug+CP.h +++ b/packages/instabug_flutter/ios/Classes/Util/Instabug+CP.h @@ -4,5 +4,6 @@ @interface Instabug (CP) + (void)setScreenshotMaskingHandler:(nullable void (^)(UIImage *, void (^)(UIImage *)))maskingHandler; +@property(nonatomic, assign, class) BOOL sendEventsSwizzling; @end diff --git a/packages/instabug_flutter/ios/instabug_flutter.podspec b/packages/instabug_flutter/ios/instabug_flutter.podspec index f339d7fe0..26b6ea47a 100644 --- a/packages/instabug_flutter/ios/instabug_flutter.podspec +++ b/packages/instabug_flutter/ios/instabug_flutter.podspec @@ -18,5 +18,7 @@ Pod::Spec.new do |s| s.dependency 'Flutter' s.dependency 'Instabug', '14.1.0' + + end diff --git a/packages/instabug_flutter/lib/instabug_flutter.dart b/packages/instabug_flutter/lib/instabug_flutter.dart index e38545897..26bf50459 100644 --- a/packages/instabug_flutter/lib/instabug_flutter.dart +++ b/packages/instabug_flutter/lib/instabug_flutter.dart @@ -22,3 +22,4 @@ export 'src/utils/instabug_navigator_observer.dart'; export 'src/utils/screen_loading/instabug_capture_screen_loading.dart'; export 'src/utils/screen_loading/route_matcher.dart'; export 'src/utils/screen_name_masker.dart' show ScreenNameMaskingCallback; +export 'src/utils/user_steps/instabug_user_steps.dart'; diff --git a/packages/instabug_flutter/lib/src/modules/instabug.dart b/packages/instabug_flutter/lib/src/modules/instabug.dart index 6bba8ed1f..82d0528f1 100644 --- a/packages/instabug_flutter/lib/src/modules/instabug.dart +++ b/packages/instabug_flutter/lib/src/modules/instabug.dart @@ -11,6 +11,7 @@ import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/material.dart'; + // to maintain supported versions prior to Flutter 3.3 // ignore: unused_import import 'package:flutter/services.dart'; @@ -21,6 +22,7 @@ import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; import 'package:instabug_flutter/src/utils/instabug_logger.dart'; import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; +import 'package:instabug_flutter/src/utils/user_steps/user_step_details.dart'; import 'package:meta/meta.dart'; enum InvocationEvent { @@ -482,4 +484,20 @@ class Instabug { static Future willRedirectToStore() async { return _host.willRedirectToStore(); } + + /// Enables and disables user interaction steps. + /// [boolean] isEnabled + static Future enableUserSteps(bool isEnabled) async { + return _host.setEnableUserSteps(isEnabled); + } + + /// Enables and disables manual invocation and prompt options for bug and feedback. + /// [boolean] isEnabled + static Future logUserSteps( + GestureType gestureType, + String message, + String? viewName, + ) async { + return _host.logUserSteps(gestureType.toString(), message, viewName); + } } diff --git a/packages/instabug_flutter/lib/src/utils/user_steps/instabug_user_steps.dart b/packages/instabug_flutter/lib/src/utils/user_steps/instabug_user_steps.dart new file mode 100644 index 000000000..15799c5fd --- /dev/null +++ b/packages/instabug_flutter/lib/src/utils/user_steps/instabug_user_steps.dart @@ -0,0 +1,262 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/utils/user_steps/user_step_details.dart'; +import 'package:instabug_flutter/src/utils/user_steps/widget_utils.dart'; + +Element? _clickTrackerElement; + +class InstabugUserSteps extends StatefulWidget { + final Widget child; + + const InstabugUserSteps({Key? key, required this.child}) : super(key: key); + + @override + InstabugUserStepsState createState() => InstabugUserStepsState(); + + @override + StatefulElement createElement() { + final element = super.createElement(); + _clickTrackerElement = element; + return element; + } +} + +class InstabugUserStepsState extends State { + static const double _doubleTapThreshold = 300.0; // milliseconds + static const double _pinchSensitivity = 20.0; + static const double _swipeSensitivity = 50.0; + static const double _scrollSensitivity = 50.0; + static const double _tapSensitivity = 20 * 20; + + Timer? _longPressTimer; + Offset? _pointerDownLocation; + GestureType? _gestureType; + String? _gestureMetaData; + DateTime? _lastTapTime; + double _pinchDistance = 0.0; + int _pointerCount = 0; + double? _previousOffset; + + void _onPointerDown(PointerDownEvent event) { + _resetGestureTracking(); + _pointerDownLocation = event.localPosition; + _pointerCount += event.buttons; + _longPressTimer = Timer(const Duration(milliseconds: 500), () { + _gestureType = GestureType.longPress; + }); + } + + void _onPointerUp(PointerUpEvent event, BuildContext context) { + _longPressTimer?.cancel(); + + final gestureType = _detectGestureType(event.localPosition); + if (_gestureType != GestureType.longPress) { + _gestureType = gestureType; + } + + _pointerCount = 0; + + if (_gestureType == null) { + return; + } + final tappedWidget = + _getWidgetDetails(event.localPosition, context, _gestureType!); + if (tappedWidget != null) { + final userStepDetails = tappedWidget.copyWith( + gestureType: _gestureType, + gestureMetaData: _gestureMetaData, + ); + if (userStepDetails.gestureType == null || + userStepDetails.message == null) { + return; + } + + Instabug.logUserSteps( + userStepDetails.gestureType!, + userStepDetails.message!, + userStepDetails.widgetName, + ); + } + } + + GestureType? _detectGestureType(Offset upLocation) { + final delta = upLocation - (_pointerDownLocation ?? Offset.zero); + + if (_pointerCount == 1) { + if (_isTap(delta)) return _detectTapType(); + if (_isSwipe(delta)) return GestureType.swipe; + } else if (_pointerCount == 2 && _isPinch()) { + return GestureType.pinch; + } + + return null; + } + + bool _isTap(Offset delta) => delta.distanceSquared < _tapSensitivity; + + GestureType? _detectTapType() { + final now = DateTime.now(); + final isDoubleTap = _lastTapTime != null && + now.difference(_lastTapTime!).inMilliseconds <= _doubleTapThreshold; + + _lastTapTime = now; + return isDoubleTap ? GestureType.doubleTap : GestureType.tap; + } + + bool _isSwipe(Offset delta) { + final isHorizontal = delta.dx.abs() > delta.dy.abs(); + + if (isHorizontal && delta.dx.abs() > _swipeSensitivity) { + _gestureMetaData = delta.dx > 0 ? "Right" : "Left"; + return true; + } + + if (!isHorizontal && delta.dy.abs() > _swipeSensitivity) { + _gestureMetaData = delta.dy > 0 ? "Down" : "Up"; + return true; + } + + return false; + } + + bool _isPinch() => _pinchDistance.abs() > _pinchSensitivity; + + void _resetGestureTracking() { + _gestureType = null; + _gestureMetaData = null; + _longPressTimer?.cancel(); + } + + UserStepDetails? _getWidgetDetails( + Offset location, + BuildContext context, + GestureType gestureType, + ) { + Element? tappedElement; + var isPrivate = false; + + final rootElement = _clickTrackerElement; + if (rootElement == null || rootElement.widget != widget) return null; + + final hitTestResult = BoxHitTestResult(); + final renderBox = context.findRenderObject()! as RenderBox; + + renderBox.hitTest(hitTestResult, position: _pointerDownLocation!); + + final targets = hitTestResult.path + .where((e) => e.target is RenderBox) + .map((e) => e.target) + .toList(); + + void visitor(Element visitedElement) { + final renderObject = visitedElement.renderObject; + if (renderObject == null) return; + + if (targets.contains(renderObject)) { + final transform = renderObject.getTransformTo(rootElement.renderObject); + final paintBounds = + MatrixUtils.transformRect(transform, renderObject.paintBounds); + + if (paintBounds.contains(_pointerDownLocation!)) { + final widget = visitedElement.widget; + if (!isPrivate) { + isPrivate = widget.runtimeType.toString() == + 'InstabugPrivateView' || + widget.runtimeType.toString() == 'InstabugSliverPrivateView'; + } + if (_isTargetWidget(widget, gestureType)) { + tappedElement = visitedElement; + return; + } + } + } + if (tappedElement == null) { + visitedElement.visitChildElements(visitor); + } + } + + rootElement.visitChildElements(visitor); + if (tappedElement == null) return null; + return UserStepDetails(element: tappedElement, isPrivate: isPrivate); + } + + bool _isTargetWidget(Widget? widget, GestureType type) { + if (widget == null) return false; + switch (type) { + case GestureType.swipe: + return isSwipedWidget(widget); + case GestureType.tap: + case GestureType.longPress: + case GestureType.doubleTap: + return isTappedWidget(widget); + case GestureType.pinch: + return isPinchWidget(widget); + case GestureType.scroll: + return false; + } + } + + void _detectScrollDirection(double currentOffset, Axis direction) { + if (_previousOffset == null) return; + + final delta = (currentOffset - _previousOffset!).abs(); + if (delta < _scrollSensitivity) return; + final String swipeDirection; + if (direction == Axis.horizontal) { + swipeDirection = currentOffset > _previousOffset! ? "Left" : "Right"; + } else { + swipeDirection = currentOffset > _previousOffset! ? "Down" : "Up"; + } + + final userStepDetails = UserStepDetails( + element: null, + isPrivate: false, + gestureMetaData: swipeDirection, + gestureType: GestureType.scroll, + ); + + if (userStepDetails.gestureType == null || + userStepDetails.message == null) { + return; + } + Instabug.logUserSteps( + userStepDetails.gestureType!, + userStepDetails.message!, + "ListView", + ); + } + + @override + Widget build(BuildContext context) { + return Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: _onPointerDown, + onPointerMove: (event) { + if (_pointerCount == 2) { + _pinchDistance = + (event.localPosition - (_pointerDownLocation ?? Offset.zero)) + .distance; + } + }, + onPointerUp: (event) => _onPointerUp(event, context), + child: NotificationListener( + onNotification: (notification) { + if (notification is ScrollStartNotification) { + _previousOffset = notification.metrics.pixels; + } else if (notification is ScrollEndNotification) { + _detectScrollDirection( + notification.metrics.pixels, // Vertical position + notification.metrics.axis, + ); + } + + return true; + }, + child: widget.child, + ), + ); + } +} diff --git a/packages/instabug_flutter/lib/src/utils/user_steps/user_step_details.dart b/packages/instabug_flutter/lib/src/utils/user_steps/user_step_details.dart new file mode 100644 index 000000000..590758eeb --- /dev/null +++ b/packages/instabug_flutter/lib/src/utils/user_steps/user_step_details.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:instabug_flutter/src/utils/user_steps/widget_utils.dart'; + +enum GestureType { swipe, scroll, tap, pinch, longPress, doubleTap } + +extension GestureTypeText on GestureType { + String get text { + switch (this) { + case GestureType.swipe: + return "Swiped"; + case GestureType.scroll: + return "Scrolled"; + case GestureType.tap: + return "Tapped"; + case GestureType.pinch: + return "Pinched"; + case GestureType.longPress: + return "Long Pressed"; + case GestureType.doubleTap: + return "Double Tapped"; + } + } +} + +class UserStepDetails { + final Element? element; + final bool isPrivate; + final GestureType? gestureType; + final String? gestureMetaData; + final Widget? widget; + + UserStepDetails({ + required this.element, + required this.isPrivate, + this.gestureType, + this.gestureMetaData, + }) : widget = element?.widget; + + String? get key => widget == null ? null : keyToStringValue(widget!.key); + + String? get widgetName { + if (widget == null) return null; + if (widget is InkWell) { + final inkWell = widget! as InkWell; + if (inkWell.child == null) { + return widget.runtimeType.toString(); + } + return "${inkWell.child.runtimeType} Wrapped with ${widget.runtimeType}"; + } else if (widget is GestureDetector) { + final gestureDetector = widget! as GestureDetector; + + if (gestureDetector.child == null) { + return widget.runtimeType.toString(); + } + return "${gestureDetector.child.runtimeType} Wrapped with ${widget.runtimeType}"; + } + return widget.runtimeType.toString(); + } + + String? get message { + if (gestureType == null) return null; + if (gestureType == GestureType.pinch) { + return gestureType?.text; + } + var baseMessage = ""; + + if (gestureType == GestureType.scroll || gestureType == GestureType.swipe) { + baseMessage += + gestureMetaData?.isNotEmpty == true ? '$gestureMetaData ' : ''; + } + + if (widgetName != null) baseMessage += " $widgetName "; + + if (!isPrivate && widget != null) { + final additionalInfo = _getWidgetSpecificDetails(); + if (additionalInfo != null) baseMessage += additionalInfo; + } + + if (key != null) baseMessage += " with key '$key' "; + + return baseMessage.trimRight(); + } + + String? _getWidgetSpecificDetails() { + if (isSliderWidget(widget!)) { + final value = getSliderValue(widget!); + if (value?.isNotEmpty == true) { + return " to '$value'"; + } + } else if (isTextWidget(widget!) || isButtonWidget(widget!)) { + final label = getLabelRecursively(element!); + if (label?.isNotEmpty == true) { + return "'$label'"; + } + } else if (isToggleableWidget(widget!)) { + final value = getToggleValue(widget!); + if (value?.isNotEmpty == true) { + return " ('$value')"; + } + } else if (isTextInputWidget(widget!)) { + final value = getTextInputValue(widget!); + final hint = getTextHintValue(widget!); + if (value?.isNotEmpty == true) return " '$value'"; + if (hint?.isNotEmpty == true) return "(placeholder:'$hint')"; + } + return null; + } + + UserStepDetails copyWith({ + Element? element, + bool? isPrivate, + GestureType? gestureType, + String? gestureMetaData, + }) { + return UserStepDetails( + element: element ?? this.element, + isPrivate: isPrivate ?? this.isPrivate, + gestureType: gestureType ?? this.gestureType, + gestureMetaData: gestureMetaData ?? this.gestureMetaData, + ); + } +} diff --git a/packages/instabug_flutter/lib/src/utils/user_steps/widget_utils.dart b/packages/instabug_flutter/lib/src/utils/user_steps/widget_utils.dart new file mode 100644 index 000000000..3e2cc0cb1 --- /dev/null +++ b/packages/instabug_flutter/lib/src/utils/user_steps/widget_utils.dart @@ -0,0 +1,195 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +/// Converts a [Key] into a string representation, supporting various key types. +String? keyToStringValue(Key? key) { + if (key == null) return null; + + if (key is ValueKey) { + return key.value?.toString(); + } else if (key is GlobalObjectKey) { + return key.value.toString(); + } else if (key is ObjectKey) { + return key.value?.toString(); + } + + return key.toString(); +} + +/// Checks if a widget is a button or button-like component. +bool isButtonWidget(Widget widget) { + if (widget is ButtonStyleButton) return widget.enabled; + if (widget is MaterialButton) return widget.enabled; + if (widget is CupertinoButton) return widget.enabled; + if (widget is IconButton) return widget.onPressed != null; + if (widget is FloatingActionButton) return widget.onPressed != null; + if (widget is BackButton) return widget.onPressed != null; + if (widget is PopupMenuButton) return widget.enabled; + if (widget is DropdownButton) return widget.onTap != null; + + if (widget is GestureDetector) { + return widget.onTap != null || + widget.onLongPress != null || + widget.onDoubleTap != null || + widget.onTapDown != null; + } + + if (widget is InkWell) { + return widget.onTap != null || + widget.onLongPress != null || + widget.onDoubleTap != null || + widget.onTapDown != null; + } + + return false; +} + +/// Checks if a widget can respond to tap-related gestures. +bool isTappedWidget(Widget? widget) { + if (widget == null) return false; + + return isButtonWidget(widget) || + isToggleableWidget(widget) || + isSliderWidget(widget) || + isTextInputWidget(widget); +} + +/// Checks if a widget supports swipe gestures. +bool isSwipedWidget(Widget? widget) { + return widget is Slider || + widget is CupertinoSlider || + widget is RangeSlider || + widget is Dismissible; +} + +/// Determines if a widget supports pinch gestures (defaulting to those not tappable or swipeable). +bool isPinchWidget(Widget? widget) { + return !isSwipedWidget(widget); +} + +/// Checks if a widget is primarily for displaying text. +bool isTextWidget(Widget widget) { + return widget is Text || + widget is RichText || + widget is SelectableText || + widget is TextSpan || + widget is Placeholder || + widget is TextStyle; +} + +/// Checks if a widget is a slider. +bool isSliderWidget(Widget widget) { + return widget is Slider || widget is CupertinoSlider || widget is RangeSlider; +} + +/// Checks if a widget is an image or image-like. +bool isImageWidget(Widget? widget) { + if (widget == null) { + return false; + } + return widget is Image || + widget is FadeInImage || + widget is NetworkImage || + widget is AssetImage || + widget is Icon || + widget is FileImage || + widget is MemoryImage || + widget is ImageProvider; +} + +/// Checks if a widget is toggleable (e.g., switch, checkbox, etc.). +bool isToggleableWidget(Widget widget) { + return widget is Checkbox || + widget is CheckboxListTile || + widget is Radio || + widget is RadioListTile || + widget is Switch || + widget is SwitchListTile || + widget is CupertinoSwitch || + widget is ToggleButtons; +} + +/// Checks if a widget is a text input field. +bool isTextInputWidget(Widget widget) { + return widget is TextField || + widget is CupertinoTextField || + widget is EditableText; +} + +/// Retrieves the label of a widget if available. +String? getLabel(Widget widget) { + if (widget is Text) return widget.data; + if (widget is Semantics) return widget.properties.label; + if (widget is Icon) return widget.semanticLabel; + if (widget is Tooltip) return widget.message; + + return null; +} + +/// Retrieves the value of a toggleable widget. +String? getToggleValue(Widget widget) { + bool? value; + if (widget is Checkbox) value = widget.value; + if (widget is Radio) return widget.groupValue.toString(); + if (widget is RadioListTile) return widget.groupValue.toString(); + if (widget is Switch) value = widget.value; + if (widget is SwitchListTile) value = widget.value; + if (widget is CupertinoSwitch) value = widget.value; + if (widget is ToggleButtons) return widget.isSelected.toString(); + + if (value == false || value == null) { + return "UnSelected"; + } else if (value) { + return "Selected"; + } + return null; +} + +/// Retrieves the value entered in a text input field. +String? getTextInputValue(Widget widget) { + if (widget is TextField && !widget.obscureText) { + return widget.controller?.text; + } else if (widget is CupertinoTextField && !widget.obscureText) { + return widget.controller?.text; + } else if (widget is EditableText && !widget.obscureText) { + return widget.controller.text; + } + + return null; +} + +/// Retrieves the hint value of a text input widget. +String? getTextHintValue(Widget widget) { + if (widget is TextField && !widget.obscureText) { + return widget.decoration?.hintText ?? widget.decoration?.labelText; + } else if (widget is CupertinoTextField && !widget.obscureText) { + return widget.placeholder; + } + + return null; +} + +/// Retrieves the current value of a slider widget. +String? getSliderValue(Widget widget) { + if (widget is Slider) return widget.value.toString(); + if (widget is CupertinoSlider) return widget.value.toString(); + if (widget is RangeSlider) { + return "(${widget.values.start},${widget.values.end})"; + } + + return null; +} + +/// Recursively searches for a label within a widget hierarchy. +String? getLabelRecursively(Element element) { + String? label; + + void visitor(Element e) { + label ??= getLabel(e.widget); + if (label == null) e.visitChildren(visitor); + } + + visitor(element); + + return label; +} diff --git a/packages/instabug_flutter/pigeons/instabug.api.dart b/packages/instabug_flutter/pigeons/instabug.api.dart index c0187acb9..ca1d9eef7 100644 --- a/packages/instabug_flutter/pigeons/instabug.api.dart +++ b/packages/instabug_flutter/pigeons/instabug.api.dart @@ -12,39 +12,66 @@ abstract class FeatureFlagsFlutterApi { @HostApi() abstract class InstabugHostApi { void setEnabled(bool isEnabled); + bool isEnabled(); + bool isBuilt(); + void init(String token, List invocationEvents, String debugLogsLevel); void show(); + void showWelcomeMessageWithMode(String mode); void identifyUser(String email, String? name, String? userId); + void setUserData(String data); + void logUserEvent(String name); + void logOut(); + void setEnableUserSteps(bool isEnabled); + + void logUserSteps( + String gestureType, + String message, + String? viewName, + ); + void setLocale(String locale); + void setColorTheme(String theme); + void setWelcomeMessageMode(String mode); + void setPrimaryColor(int color); + void setSessionProfilerEnabled(bool enabled); + void setValueForStringWithKey(String value, String key); void appendTags(List tags); + void resetTags(); @async List? getTags(); void addExperiments(List experiments); + void removeExperiments(List experiments); + void clearAllExperiments(); + void addFeatureFlags(Map featureFlagsMap); + void removeFeatureFlags(List featureFlags); + void removeAllFeatureFlags(); void setUserAttribute(String value, String key); + void removeUserAttribute(String key); @async @@ -58,13 +85,17 @@ abstract class InstabugHostApi { String? crashMode, String? sessionReplayMode, ); + void reportScreenChange(String screenName); void setCustomBrandingImage(String light, String dark); + void setFont(String font); void addFileAttachmentWithURL(String filePath, String fileName); + void addFileAttachmentWithData(Uint8List data, String fileName); + void clearFileAttachments(); void networkLog(Map data); diff --git a/packages/instabug_flutter/pubspec.lock b/packages/instabug_flutter/pubspec.lock index 427305640..850b79e0f 100644 --- a/packages/instabug_flutter/pubspec.lock +++ b/packages/instabug_flutter/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: coverage - sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" crypto: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" dart_style: dependency: transitive description: @@ -255,10 +255,10 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: transitive description: @@ -271,10 +271,10 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: @@ -287,10 +287,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -415,10 +415,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" pana: dependency: "direct dev" description: @@ -455,10 +455,10 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: @@ -511,10 +511,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -540,10 +540,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: @@ -572,10 +572,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -628,10 +628,10 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: @@ -660,10 +660,10 @@ packages: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: diff --git a/packages/instabug_flutter/test/instabug_test.dart b/packages/instabug_flutter/test/instabug_test.dart index e2fd7d298..1ee67a9d3 100644 --- a/packages/instabug_flutter/test/instabug_test.dart +++ b/packages/instabug_flutter/test/instabug_test.dart @@ -8,6 +8,7 @@ import 'package:instabug_flutter/src/utils/enum_converter.dart'; import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; +import 'package:instabug_flutter/src/utils/user_steps/user_step_details.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -474,4 +475,26 @@ void main() { mHost.willRedirectToStore(), ).called(1); }); + + test('[enableUserSteps] should call host method', () async { + const enabled = true; + + await Instabug.enableUserSteps(enabled); + + verify( + mHost.setEnableUserSteps(enabled), + ).called(1); + }); + + test('[logUserSteps] should call host method', () async { + const message = "message"; + const gestureType = GestureType.tap; + const viewName = "view"; + + await Instabug.logUserSteps(gestureType, message, viewName); + + verify( + mHost.logUserSteps(gestureType.toString(), message, viewName), + ).called(1); + }); } diff --git a/packages/instabug_flutter/test/utils/user_steps/instabug_user_steps_test.dart b/packages/instabug_flutter/test/utils/user_steps/instabug_user_steps_test.dart new file mode 100644 index 000000000..9ac05e4cb --- /dev/null +++ b/packages/instabug_flutter/test/utils/user_steps/instabug_user_steps_test.dart @@ -0,0 +1,298 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/utils/user_steps/user_step_details.dart'; +import 'package:mockito/mockito.dart'; + +import '../../instabug_test.mocks.dart'; + +void main() { + late MockInstabugHostApi mockInstabugHostApi; + + setUp(() { + mockInstabugHostApi = MockInstabugHostApi(); + Instabug.$setHostApi(mockInstabugHostApi); + }); + + Widget buildTestWidget(Widget child) { + return MaterialApp(home: InstabugUserSteps(child: child)); + } + + group('InstabugUserSteps Widget', () { + testWidgets('builds child widget correctly', (tester) async { + await tester.pumpWidget(buildTestWidget(const Text('Test Widget'))); + expect(find.text('Test Widget'), findsOneWidget); + }); + + testWidgets('detects tap gestures', (tester) async { + await tester.pumpWidget( + buildTestWidget( + GestureDetector(onTap: () {}, child: const Text('Tap Me')), + ), + ); + + await tester.tap(find.text('Tap Me')); + await tester.pumpAndSettle(); + + verify( + mockInstabugHostApi.logUserSteps( + GestureType.tap.toString(), + any, + any, + ), + ).called(1); + }); + + testWidgets('detects long press gestures', (tester) async { + await tester.pumpWidget( + buildTestWidget( + GestureDetector( + onLongPress: () {}, + child: const Text('Long Press Me'), + ), + ), + ); + + final gesture = await tester + .startGesture(tester.getCenter(find.text('Long Press Me'))); + await tester + .pump(const Duration(seconds: 2)); // Simulate long press duration + await gesture.up(); + + await tester.pump(); + + verify( + mockInstabugHostApi.logUserSteps( + GestureType.longPress.toString(), + any, + any, + ), + ).called(1); + }); + + group('Swipe Gestures', () { + const scrollOffset = Offset(0, -200); + const smallScrollOffset = Offset(0, -20); + + testWidgets('detects scroll gestures', (tester) async { + await tester.pumpWidget( + buildTestWidget( + ListView(children: List.generate(50, (i) => Text('Item $i'))), + ), + ); + + await tester.fling(find.byType(ListView), scrollOffset, 1000); + await tester.pumpAndSettle(); + + verify( + mockInstabugHostApi.logUserSteps( + GestureType.scroll.toString(), + any, + any, + ), + ).called(1); + }); + + testWidgets('ignores small swipe gestures', (tester) async { + await tester.pumpWidget( + buildTestWidget( + ListView(children: List.generate(50, (i) => Text('Item $i'))), + ), + ); + + await tester.fling(find.byType(ListView), smallScrollOffset, 1000); + await tester.pumpAndSettle(); + + verifyNever( + mockInstabugHostApi.logUserSteps( + GestureType.scroll.toString(), + any, + any, + ), + ); + }); + + testWidgets('detects horizontal scroll', (tester) async { + await tester.pumpWidget( + buildTestWidget( + ListView( + scrollDirection: Axis.horizontal, + children: List.generate(20, (i) => Text('Item $i')), + ), + ), + ); + + await tester.drag(find.byType(ListView), const Offset(-300, 0)); + await tester.pumpAndSettle(); + + verify( + mockInstabugHostApi.logUserSteps( + GestureType.scroll.toString(), + argThat(contains('Left')), + "ListView", + ), + ).called(1); + }); + + testWidgets('detects vertical scroll direction', (tester) async { + await tester.pumpWidget( + buildTestWidget( + ListView(children: List.generate(20, (i) => Text('Item $i'))), + ), + ); + + await tester.drag(find.byType(ListView), const Offset(0, -300)); + await tester.pumpAndSettle(); + + verify( + mockInstabugHostApi.logUserSteps( + GestureType.scroll.toString(), + argThat(contains('Down')), + "ListView", + ), + ).called(1); + }); + + testWidgets('does not log small scroll gestures', (tester) async { + await tester.pumpWidget( + buildTestWidget( + ListView(children: List.generate(20, (i) => Text('Item $i'))), + ), + ); + + await tester.drag(find.byType(ListView), const Offset(0, -10)); + await tester.pumpAndSettle(); + + verifyNever( + mockInstabugHostApi.logUserSteps( + GestureType.scroll.toString(), + argThat(contains('Down')), + "ListView", + ), + ); + }); + }); + + group('Pinch Gestures', () { + testWidgets('handles pinch gestures', (tester) async { + await tester.pumpWidget( + buildTestWidget( + Transform.scale( + scale: 1.0, + child: const Icon(Icons.add, size: 300), + ), + ), + ); + + final iconFinder = find.byIcon(Icons.add); + final pinchStart = tester.getCenter(iconFinder); + + final gesture1 = await tester.startGesture(pinchStart); + final gesture2 = + await tester.startGesture(pinchStart + const Offset(100.0, 0.0)); + + await tester.pump(); + await gesture1.moveTo(pinchStart + const Offset(150.0, 0.0)); + await gesture2.moveTo(pinchStart + const Offset(70.0, 0.0)); + + await gesture1.up(); + await gesture2.up(); + + await tester.pump(const Duration(seconds: 1)); + + verify( + mockInstabugHostApi.logUserSteps( + GestureType.pinch.toString(), + any, + any, + ), + ).called(1); + }); + + testWidgets('ignores small pinch gestures', (tester) async { + await tester.pumpWidget( + buildTestWidget( + Transform.scale( + scale: 1.0, + child: const Icon(Icons.add, size: 300), + ), + ), + ); + + final iconFinder = find.byIcon(Icons.add); + final pinchStart = tester.getCenter(iconFinder); + + final gesture1 = await tester.startGesture(pinchStart); + final gesture2 = + await tester.startGesture(pinchStart + const Offset(100.0, 0.0)); + + await tester.pump(); + await gesture1.moveTo(pinchStart + const Offset(10.0, 0.0)); + await gesture2.moveTo(pinchStart + const Offset(110.0, 0.0)); + + await gesture1.up(); + await gesture2.up(); + + await tester.pump(const Duration(seconds: 1)); + + verifyNever( + mockInstabugHostApi.logUserSteps( + GestureType.pinch.toString(), + any, + any, + ), + ); + }); + }); + + group('Double Tap Gestures', () { + testWidgets('logs double tap gestures', (tester) async { + await tester.pumpWidget( + buildTestWidget( + GestureDetector( + onDoubleTap: () {}, + child: const Text('Double Tap Me'), + ), + ), + ); + + final doubleTapFinder = find.text('Double Tap Me'); + await tester.tap(doubleTapFinder); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tap(doubleTapFinder); + await tester.pumpAndSettle(); + + verify( + mockInstabugHostApi.logUserSteps( + GestureType.doubleTap.toString(), + any, + any, + ), + ).called(1); + }); + + testWidgets('does not log single taps as double taps', (tester) async { + await tester.pumpWidget( + buildTestWidget( + GestureDetector( + onDoubleTap: () {}, + child: const Text('Double Tap Me'), + ), + ), + ); + + final doubleTapFinder = find.text('Double Tap Me'); + await tester.tap(doubleTapFinder); + await tester.pump(const Duration(milliseconds: 50)); + + verifyNever( + mockInstabugHostApi.logUserSteps( + GestureType.doubleTap.toString(), + any, + any, + ), + ); + }); + }); + }); +} diff --git a/packages/instabug_flutter/test/utils/user_steps/user_step_details_test.dart b/packages/instabug_flutter/test/utils/user_steps/user_step_details_test.dart new file mode 100644 index 000000000..0d992755f --- /dev/null +++ b/packages/instabug_flutter/test/utils/user_steps/user_step_details_test.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/src/utils/user_steps/user_step_details.dart'; + +void main() { + group('GestureTypeText Extension', () { + test('GestureType.text returns correct text', () { + expect(GestureType.swipe.text, 'Swiped'); + expect(GestureType.scroll.text, 'Scrolled'); + expect(GestureType.tap.text, 'Tapped'); + expect(GestureType.pinch.text, 'Pinched'); + expect(GestureType.longPress.text, 'Long Pressed'); + expect(GestureType.doubleTap.text, 'Double Tapped'); + }); + }); + + group('UserStepDetails', () { + test('key returns correct value', () { + final widget = Container(key: const ValueKey('testKey')); + final element = widget.createElement(); + final details = UserStepDetails( + element: element, + isPrivate: false, + ); + + expect(details.key, 'testKey'); + }); + + test('widgetName identifies widget types correctly', () { + const inkWell = InkWell( + child: Text('Child'), + ); + final detailsInkWell = UserStepDetails( + element: inkWell.createElement(), + isPrivate: false, + ); + expect(detailsInkWell.widgetName, "Text Wrapped with InkWell"); + + final gestureDetector = GestureDetector( + child: const Icon(Icons.add), + ); + final detailsGestureDetector = UserStepDetails( + element: gestureDetector.createElement(), + isPrivate: false, + ); + expect( + detailsGestureDetector.widgetName, + "Icon Wrapped with GestureDetector", + ); + }); + + test('message constructs correctly with gestureType', () { + final widget = Container(key: const ValueKey('testKey')); + final element = widget.createElement(); + + final details = UserStepDetails( + element: element, + isPrivate: false, + gestureType: GestureType.tap, + ); + + expect( + details.message, + " Container with key 'testKey'", + ); + }); + + test('_getWidgetSpecificDetails handles slider widgets', () { + final slider = Slider(value: 0.5, onChanged: (_) {}); + final element = slider.createElement(); + + final details = UserStepDetails( + element: element, + isPrivate: false, + gestureType: GestureType.tap, + ); + + expect( + details.message, + contains(" Slider to '0.5'"), + ); + }); + + test('_getWidgetSpecificDetails handles null widget gracefully', () { + final details = UserStepDetails( + element: null, + isPrivate: false, + ); + + expect(details.message, isNull); + }); + + test('widgetName handles null child gracefully in InkWell', () { + const inkWell = InkWell(); + final details = UserStepDetails( + element: inkWell.createElement(), + isPrivate: false, + ); + expect(details.widgetName, "InkWell"); + }); + + test('message includes additional metadata when gestureMetaData is empty', + () { + final widget = Container(key: const ValueKey('testKey')); + final element = widget.createElement(); + + final details = UserStepDetails( + element: element, + isPrivate: false, + gestureType: GestureType.tap, + gestureMetaData: '', + ); + + expect( + details.message, + " Container with key 'testKey'", + ); + }); + + test('widgetName handles GestureDetector without child', () { + final gestureDetector = GestureDetector(); + final details = UserStepDetails( + element: gestureDetector.createElement(), + isPrivate: false, + ); + expect(details.widgetName, "GestureDetector"); + }); + }); +// }); +} diff --git a/packages/instabug_flutter/test/utils/user_steps/widget_utils_test.dart b/packages/instabug_flutter/test/utils/user_steps/widget_utils_test.dart new file mode 100644 index 000000000..631d49c61 --- /dev/null +++ b/packages/instabug_flutter/test/utils/user_steps/widget_utils_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/src/utils/user_steps/widget_utils.dart'; + +void main() { + group('keyToStringValue', () { + test('returns null for null key', () { + expect(keyToStringValue(null), isNull); + }); + + test('returns value for ValueKey', () { + expect(keyToStringValue(const ValueKey('test')), 'test'); + }); + + test('returns value for GlobalObjectKey', () { + const globalKey = GlobalObjectKey('globalKey'); + expect(keyToStringValue(globalKey), 'globalKey'); + }); + + test('returns value for ObjectKey', () { + expect(keyToStringValue(const ObjectKey('objectKey')), 'objectKey'); + }); + + test('returns toString for unknown key type', () { + const customKey = Key('customKey'); + expect(keyToStringValue(customKey), 'customKey'); + }); + }); + + group('isButtonWidget', () { + test('detects ButtonStyleButton', () { + final button = + ElevatedButton(onPressed: () {}, child: const Text('Button')); + expect(isButtonWidget(button), true); + }); + + test('detects disabled MaterialButton', () { + const button = MaterialButton(onPressed: null); + expect(isButtonWidget(button), false); + }); + + test('detects IconButton with onPressed', () { + final button = IconButton(onPressed: () {}, icon: const Icon(Icons.add)); + expect(isButtonWidget(button), true); + }); + + test('returns false for non-button widget', () { + const widget = Text('Not a button'); + expect(isButtonWidget(widget), false); + }); + }); + + group('isTappedWidget', () { + test('detects button widget', () { + final button = + ElevatedButton(onPressed: () {}, child: const Text('Button')); + expect(isTappedWidget(button), true); + }); + + test('returns false for null widget', () { + expect(isTappedWidget(null), false); + }); + }); + + group('isTextWidget', () { + test('detects Text widget', () { + const widget = Text('Hello'); + expect(isTextWidget(widget), true); + }); + + test('returns false for non-text widget', () { + const widget = Icon(Icons.add); + expect(isTextWidget(widget), false); + }); + }); + + group('getLabel', () { + test('returns label from Text widget', () { + const widget = Text('Label'); + expect(getLabel(widget), 'Label'); + }); + + test('returns label from Tooltip', () { + const widget = Tooltip(message: 'Tooltip message', child: Text('Child')); + expect(getLabel(widget), 'Tooltip message'); + }); + + test('returns null for unlabeled widget', () { + const widget = Icon(Icons.add); + expect(getLabel(widget), isNull); + }); + }); + + group('getSliderValue', () { + test('returns value from Slider', () { + final widget = Slider(value: 0.5, onChanged: (_) {}); + expect(getSliderValue(widget), '0.5'); + }); + + test('returns value from RangeSlider', () { + final widget = + RangeSlider(values: const RangeValues(0.2, 0.8), onChanged: (_) {}); + expect(getSliderValue(widget), '(0.2,0.8)'); + }); + + test('returns null for non-slider widget', () { + const widget = Text('Not a slider'); + expect(getSliderValue(widget), isNull); + }); + }); +} diff --git a/packages/instabug_flutter_modular/example/pubspec_overrides.yaml b/packages/instabug_flutter_modular/example/pubspec_overrides.yaml new file mode 100644 index 000000000..82c9b8e03 --- /dev/null +++ b/packages/instabug_flutter_modular/example/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: instabug_flutter,instabug_flutter_modular +dependency_overrides: + instabug_flutter: + path: ../../instabug_flutter + instabug_flutter_modular: + path: .. diff --git a/packages/instabug_flutter_modular/pubspec_overrides.yaml b/packages/instabug_flutter_modular/pubspec_overrides.yaml new file mode 100644 index 000000000..1f0beaf62 --- /dev/null +++ b/packages/instabug_flutter_modular/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +# melos_managed_dependency_overrides: instabug_flutter +dependency_overrides: + instabug_flutter: + path: ../instabug_flutter diff --git a/packages/instabug_http_client/example/pubspec_overrides.yaml b/packages/instabug_http_client/example/pubspec_overrides.yaml new file mode 100644 index 000000000..3daf4fd14 --- /dev/null +++ b/packages/instabug_http_client/example/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: instabug_flutter,instabug_http_client +dependency_overrides: + instabug_flutter: + path: ../../instabug_flutter + instabug_http_client: + path: .. diff --git a/packages/instabug_private_views/android/src/main/java/com/instabug/instabug_private_views/modules/InstabugPrivateView.java b/packages/instabug_private_views/android/src/main/java/com/instabug/instabug_private_views/modules/InstabugPrivateView.java index 9213eadf3..6a20b768d 100644 --- a/packages/instabug_private_views/android/src/main/java/com/instabug/instabug_private_views/modules/InstabugPrivateView.java +++ b/packages/instabug_private_views/android/src/main/java/com/instabug/instabug_private_views/modules/InstabugPrivateView.java @@ -1,14 +1,9 @@ package com.instabug.instabug_private_views.modules; -import androidx.annotation.NonNull; - -import com.instabug.flutter.generated.InstabugLogPigeon; import com.instabug.flutter.modules.InstabugApi; -import com.instabug.flutter.modules.InstabugLogApi; import com.instabug.flutter.util.privateViews.ScreenshotCaptor; import com.instabug.instabug_private_views.generated.InstabugPrivateViewPigeon; import com.instabug.library.internal.crossplatform.InternalCore; -import com.instabug.library.screenshot.instacapture.ScreenshotRequest; import io.flutter.plugin.common.BinaryMessenger; diff --git a/packages/instabug_private_views/android/src/main/java/com/instabug/instabug_private_views/modules/PrivateViewManager.java b/packages/instabug_private_views/android/src/main/java/com/instabug/instabug_private_views/modules/PrivateViewManager.java index 6b8abb152..d1d25d416 100644 --- a/packages/instabug_private_views/android/src/main/java/com/instabug/instabug_private_views/modules/PrivateViewManager.java +++ b/packages/instabug_private_views/android/src/main/java/com/instabug/instabug_private_views/modules/PrivateViewManager.java @@ -121,19 +121,23 @@ private void processScreenshot(ScreenshotResult result, AtomicReference privateViews) { - if (privateViews == null || privateViews.isEmpty()) return; - - Bitmap bitmap = result.getScreenshot(); - float pixelRatio = result.getPixelRatio(); - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); // Default color is black - - for (int i = 0; i < privateViews.size(); i += 4) { - float left = privateViews.get(i).floatValue() * pixelRatio; - float top = privateViews.get(i + 1).floatValue() * pixelRatio; - float right = privateViews.get(i + 2).floatValue() * pixelRatio; - float bottom = privateViews.get(i + 3).floatValue() * pixelRatio; - canvas.drawRect(left, top, right, bottom, paint); // Mask private view + try { + if (privateViews == null || privateViews.isEmpty()) return; + + Bitmap bitmap = result.getScreenshot(); + float pixelRatio = result.getPixelRatio(); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); // Default color is black + + for (int i = 0; i < privateViews.size(); i += 4) { + float left = privateViews.get(i).floatValue() * pixelRatio; + float top = privateViews.get(i + 1).floatValue() * pixelRatio; + float right = privateViews.get(i + 2).floatValue() * pixelRatio; + float bottom = privateViews.get(i + 3).floatValue() * pixelRatio; + canvas.drawRect(left, top, right, bottom, paint); // Mask private view + } + } catch (Exception e){ +e.printStackTrace(); } } } \ No newline at end of file diff --git a/packages/instabug_private_views/example-hybrid-ios-app/my_flutter/pubspec_overrides.yaml b/packages/instabug_private_views/example-hybrid-ios-app/my_flutter/pubspec_overrides.yaml new file mode 100644 index 000000000..385dc32cf --- /dev/null +++ b/packages/instabug_private_views/example-hybrid-ios-app/my_flutter/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: instabug_flutter,instabug_private_views +dependency_overrides: + instabug_flutter: + path: ../../../instabug_flutter + instabug_private_views: + path: ../.. diff --git a/packages/instabug_private_views/example/ios/Podfile b/packages/instabug_private_views/example/ios/Podfile index 8b43e9c1b..8621254c6 100644 --- a/packages/instabug_private_views/example/ios/Podfile +++ b/packages/instabug_private_views/example/ios/Podfile @@ -30,7 +30,7 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! -pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/feature-flutter-private-views-base/14.0.0/Instabug.podspec' + pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/feature-flutter_user_steps-base/14.1.0/Instabug.podspec' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do diff --git a/packages/instabug_private_views/example/ios/Podfile.lock b/packages/instabug_private_views/example/ios/Podfile.lock index 4ec18dae9..900830c0c 100644 --- a/packages/instabug_private_views/example/ios/Podfile.lock +++ b/packages/instabug_private_views/example/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - Flutter (1.0.0) - - Instabug (14.0.0) + - Instabug (14.1.0) - instabug_flutter (14.0.0): - Flutter - - Instabug (= 14.0.0) + - Instabug (= 14.1.0) - instabug_private_views (0.0.1): - Flutter - instabug_flutter @@ -14,7 +14,7 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) - - Instabug (from `https://ios-releases.instabug.com/custom/feature-flutter-private-views-base/14.0.0/Instabug.podspec`) + - Instabug (from `https://ios-releases.instabug.com/custom/feature-flutter_user_steps-base/14.1.0/Instabug.podspec`) - instabug_flutter (from `.symlinks/plugins/instabug_flutter/ios`) - instabug_private_views (from `.symlinks/plugins/instabug_private_views/ios`) - OCMock (= 3.6) @@ -28,7 +28,7 @@ EXTERNAL SOURCES: Flutter: :path: Flutter Instabug: - :podspec: https://ios-releases.instabug.com/custom/feature-flutter-private-views-base/14.0.0/Instabug.podspec + :podspec: https://ios-releases.instabug.com/custom/feature-flutter_user_steps-base/14.1.0/Instabug.podspec instabug_flutter: :path: ".symlinks/plugins/instabug_flutter/ios" instabug_private_views: @@ -38,12 +38,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Instabug: 9d2b06afbadfbd4630bc0116dc27d84360ed70b0 - instabug_flutter: ff8ab5ff34a476b1d2d887478ec190cda962b973 + Instabug: 8eb6f63f3ac66f062025c15293549ab67150e9f9 + instabug_flutter: a24751dfaedd29475da2af062d3e19d697438f72 instabug_private_views: df53ff3f1cc842cb686d43e077099d3b36426a7f OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 -PODFILE CHECKSUM: 38cf660255aba2d321a6f139341d5a867e19b769 +PODFILE CHECKSUM: af9cfc3aee253487693ebb906ba6b3358615392c -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.0 diff --git a/packages/instabug_private_views/example/lib/main.dart b/packages/instabug_private_views/example/lib/main.dart index 9ae8442dd..b379ba005 100644 --- a/packages/instabug_private_views/example/lib/main.dart +++ b/packages/instabug_private_views/example/lib/main.dart @@ -22,7 +22,7 @@ void main() { }; enableInstabugMaskingPrivateViews(); - runApp(const PrivateViewPage()); + runApp(const InstabugUserSteps(child:PrivateViewPage())); }, CrashReporting.reportCrash, ); diff --git a/packages/instabug_private_views/example/lib/private_view_page.dart b/packages/instabug_private_views/example/lib/private_view_page.dart index be01f1ba3..8f23969b3 100644 --- a/packages/instabug_private_views/example/lib/private_view_page.dart +++ b/packages/instabug_private_views/example/lib/private_view_page.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:instabug_private_views/instabug_private_view.dart'; +import 'package:instabug_private_views_example/widget/instabug_button.dart'; +import 'package:instabug_private_views_example/widget/section_title.dart'; import 'package:video_player/video_player.dart'; class PrivateViewPage extends StatefulWidget { @@ -11,6 +14,23 @@ class PrivateViewPage extends StatefulWidget { class _PrivateViewPageState extends State { late VideoPlayerController _controller; + double _currentSliderValue = 20.0; + + RangeValues _currentRangeValues = const RangeValues(40, 80); + + String? _sliderStatus; + + bool? isChecked = true; + + int? _selectedValue = 1; + + bool light = true; + double _scale = 1.0; // Initial scale of the image + + TextEditingController _controller2 = TextEditingController(); + + String _currentValue = ''; + List _items = List.generate(20, (index) => 'Item ${index + 1}'); @override void initState() { @@ -21,6 +41,11 @@ class _PrivateViewPageState extends State { )..initialize().then((_) { setState(() {}); }); + _controller2.addListener(() { + setState(() { + _currentValue = _controller2.text; + }); + }); } @override @@ -29,10 +54,17 @@ class _PrivateViewPageState extends State { super.dispose(); } + void _handleRadioValueChanged(int? value) { + setState(() { + _selectedValue = value; + }); + } + @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, + navigatorObservers: [InstabugNavigatorObserver()], home: Scaffold( appBar: AppBar(title: const Text("Private Views page")), body: SingleChildScrollView( @@ -62,6 +94,210 @@ class _PrivateViewPageState extends State { ), ), const SizedBox(height: 16), + BackButton(), + NotificationListener( + onNotification: (ScrollNotification notification) { + return false; + }, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate( + 100, + (_) => InkWell( + onTap: () {}, + child: Container( + width: 100, + height: 100, + color: Colors.red, + margin: EdgeInsets.all(8), + ), + ), + ), + ), + ), + ), + SectionTitle('Sliders'), + Slider( + value: _currentSliderValue, + max: 100, + divisions: 5, + label: _currentSliderValue.round().toString(), + onChanged: (double value) { + setState(() { + _currentSliderValue = value; + }); + }, + ), + RangeSlider( + values: _currentRangeValues, + max: 100, + divisions: 5, + labels: RangeLabels( + _currentRangeValues.start.round().toString(), + _currentRangeValues.end.round().toString(), + ), + onChanged: (RangeValues values) { + setState(() { + _currentRangeValues = values; + }); + }, + ), + SectionTitle('Images'), + Row( + children: [ + Image.asset( + 'assets/img.png', + height: 100, + ), + Image.network( + "https://t3.ftcdn.net/jpg/00/50/07/64/360_F_50076454_TCvZEw37VyB5ZhcwEjkJHddtuV1cFmKY.jpg", + height: 100, + ), + ], + ), + InstabugButton(text: "I'm a button"), + ElevatedButton(onPressed: () {}, child: Text("data")), + SectionTitle('Toggles'), + Row( + children: [ + Checkbox( + tristate: true, + value: isChecked, + onChanged: (bool? value) { + setState(() { + isChecked = value; + }); + }, + ), + Radio( + value: 0, + groupValue: _selectedValue, + onChanged: _handleRadioValueChanged, + ), + Switch( + value: light, + activeColor: Colors.red, + onChanged: (bool value) { + setState(() { + light = value; + }); + }, + ), + ], + ), + GestureDetector( + onScaleUpdate: (details) { + setState(() { + _scale = details.scale; + _scale = _scale.clamp(1.0, + 3.0); // Limit zoom between 1x and 3x// Update scale based on pinch gesture + }); + }, + onScaleEnd: (details) { + // You can add logic to reset or clamp the scale if needed + if (_scale < 1.0) { + _scale = 1.0; // Prevent shrinking below original size + } + }, + child: Transform.scale( + scale: _scale, // Apply the scale transformation + child: Image.asset( + "assets/img.png", + height: 300, + ), + ), + ), + SectionTitle('TextInput'), + Column( + children: [ + Padding( + padding: EdgeInsets.all(16.0), // Set the padding value + child: Column( + children: [ + TextField( + key: Key('text_field'), + controller: _controller2, + // Bind the controller to the TextField + decoration: InputDecoration( + labelText: "Type something in a text field with key", + border: OutlineInputBorder(), + ), + ), + InstabugPrivateView( + child: TextField( + controller: _controller2, + // Bind the controller to the TextField + decoration: InputDecoration( + labelText: "Private view", + border: OutlineInputBorder(), + ), + ), + ), + InstabugPrivateView( + child: TextField( + controller: _controller2, + // Bind the controller to the TextField + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Password', + ), + ), + ), + TextFormField( + obscureText: true, + controller: _controller2, + // Bind the controller to the TextField + decoration: const InputDecoration( + icon: Icon(Icons.person), + hintText: 'What do people call you?', + labelText: 'Name *', + ), + onSaved: (String? value) { + // This optional block of code can be used to run + // code when the user saves the form. + }, + validator: (String? value) { + return (value != null && value.contains('@')) + ? 'Do not use the @ char.' + : null; + }, + ), + ], + ), + ), + ListView.builder( + itemCount: _items.length, + shrinkWrap: true, + itemBuilder: (context, index) { + return Dismissible( + key: Key(_items[index]), + // Unique key for each item + onDismissed: (direction) { + // Remove the item from the list + setState(() { + _items.removeAt(index); + }); + + // Show a snackbar or other UI feedback on dismissal + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Item dismissed')), + ); + }, + background: Container(color: Colors.red), + // Background color when swiped + direction: DismissDirection.endToStart, + // Swipe direction (left to right) + child: ListTile( + title: Text(_items[index]), + ), + ); + }, + ), + ], + ), + InstabugPrivateView( child: Image.asset( 'assets/img.png', diff --git a/packages/instabug_private_views/example/pubspec.lock b/packages/instabug_private_views/example/pubspec.lock index 5e8a12aa4..29e14f6c8 100644 --- a/packages/instabug_private_views/example/pubspec.lock +++ b/packages/instabug_private_views/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" csslib: dependency: transitive description: @@ -97,7 +97,7 @@ packages: source: hosted version: "0.15.5" instabug_flutter: - dependency: "direct overridden" + dependency: "direct main" description: path: "../../instabug_flutter" relative: true @@ -114,18 +114,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -186,7 +186,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -199,10 +199,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -215,10 +215,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -231,10 +231,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" vector_math: dependency: transitive description: @@ -287,10 +287,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" web: dependency: transitive description: diff --git a/packages/instabug_private_views/example/pubspec.yaml b/packages/instabug_private_views/example/pubspec.yaml index 8e9d153ee..d387bb1ed 100644 --- a/packages/instabug_private_views/example/pubspec.yaml +++ b/packages/instabug_private_views/example/pubspec.yaml @@ -9,6 +9,8 @@ dependencies: flutter: sdk: flutter + instabug_flutter: + path: '../../instabug_flutter' instabug_private_views: path: '../' diff --git a/packages/instabug_private_views/example/pubspec_overrides.yaml b/packages/instabug_private_views/example/pubspec_overrides.yaml new file mode 100644 index 000000000..387b3324d --- /dev/null +++ b/packages/instabug_private_views/example/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: instabug_flutter,instabug_private_views +dependency_overrides: + instabug_flutter: + path: ../../instabug_flutter + instabug_private_views: + path: .. diff --git a/scripts/init.sh b/scripts/init.sh index 7ccdb2abc..1191ca84f 100644 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -26,6 +26,10 @@ if [ -d "test" ]; then echo "Folder test and its contents removed" fi +if [ -d "example" ]; then + rm -rf "example" + echo "Folder example and its contents removed" +fi if command -v melos &> /dev/null then