From 22fd9f96ea8082a68776afe4fef5dd24ecb31bb2 Mon Sep 17 00:00:00 2001 From: obaidgini Date: Mon, 8 Sep 2025 16:36:08 +0200 Subject: [PATCH 001/116] feat(capture-sdk): Adding the unit and integration tests for the user Journey feature flag PP-1700 --- capture-sdk/sdk/build.gradle.kts | 3 +- ...iCaptureHelperForInstrumentationTests.java | 13 ++ .../ginicapture/GiniCaptureFragmentTest.kt | 185 ++++++++++++++++++ .../BufferedUserAnalyticsEventTracker.kt | 4 + .../BufferedUserAnalyticsEventTrackerTest.kt | 126 ++++++++++++ 5 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/GiniCaptureHelperForInstrumentationTests.java create mode 100644 capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt create mode 100644 capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt diff --git a/capture-sdk/sdk/build.gradle.kts b/capture-sdk/sdk/build.gradle.kts index 3960cc2c2b..5932ab8778 100644 --- a/capture-sdk/sdk/build.gradle.kts +++ b/capture-sdk/sdk/build.gradle.kts @@ -192,7 +192,8 @@ dependencies { androidTestImplementation(libs.androidx.test.uiautomator) androidTestImplementation(libs.mockito.android) androidTestImplementation(libs.androidx.multidex) - + testImplementation(libs.mockito.kotlin2) + androidTestImplementation(libs.mockito.kotlin2) androidTestUtil(libs.androidx.test.orchestrator) } diff --git a/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/GiniCaptureHelperForInstrumentationTests.java b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/GiniCaptureHelperForInstrumentationTests.java new file mode 100644 index 0000000000..a4dd6c01a4 --- /dev/null +++ b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/GiniCaptureHelperForInstrumentationTests.java @@ -0,0 +1,13 @@ +package net.gini.android.capture; + +import androidx.annotation.Nullable; + +/** + * Helper class to set the {@link GiniCapture} instance for instrumentation tests. + */ +public class GiniCaptureHelperForInstrumentationTests { + public static void setGiniCaptureInstance(@Nullable final GiniCapture giniCapture) { + GiniCapture.setInstance(giniCapture); + } + +} diff --git a/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt new file mode 100644 index 0000000000..e04e17b2c6 --- /dev/null +++ b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt @@ -0,0 +1,185 @@ +package net.gini.android.capture.ginicapture + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import androidx.fragment.app.testing.FragmentScenario +import androidx.lifecycle.Lifecycle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import jersey.repackaged.jsr166e.CompletableFuture +import net.gini.android.capture.DocumentImportEnabledFileTypes +import net.gini.android.capture.EntryPoint +import net.gini.android.capture.GiniCapture +import net.gini.android.capture.GiniCaptureFragment +import net.gini.android.capture.GiniCaptureHelperForInstrumentationTests +import net.gini.android.capture.di.CaptureSdkIsolatedKoinContext +import net.gini.android.capture.internal.document.ImageMultiPageDocumentMemoryStore +import net.gini.android.capture.internal.network.Configuration +import net.gini.android.capture.internal.network.ConfigurationNetworkResult +import net.gini.android.capture.internal.network.NetworkRequestsManager +import net.gini.android.capture.internal.provider.GiniBankConfigurationProvider +import net.gini.android.capture.tracking.useranalytics.BufferedUserAnalyticsEventTracker +import net.gini.android.capture.tracking.useranalytics.UserAnalytics +import net.gini.android.capture.view.DefaultLoadingIndicatorAdapter +import net.gini.android.capture.view.DefaultNavigationBarTopAdapter +import net.gini.android.capture.view.InjectedViewAdapterInstance +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.dsl.module +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import java.util.UUID + +/** + * This class is responsible for testing the behavior of BufferedUserAnalyticsEventTracker + * in-case the user journey analytics is enabled or disabled from the Gini API. + * for more detailed information how Analytics works in this project, + * Please refer to the documentation written in the Unit tests of + * BufferedUserAnalyticsEventTracker + * [net.gini.android.capture.tracking.BufferedUserAnalyticsEventTrackerTest] + * + * */ + +@RunWith(AndroidJUnit4::class) +class GiniCaptureFragmentTest { + private lateinit var networkRequestsManager: NetworkRequestsManager + private lateinit var giniCapture: GiniCapture + private lateinit var giniInternal: GiniCapture.Internal + private lateinit var memoryStore: ImageMultiPageDocumentMemoryStore + private val koinTestModule = module { + single { GiniBankConfigurationProvider() } + } + + /** + * We are using multiple dependencies in different classes, so many mocks are needed + * to fully test the functionality of AnalyticsTracker, by running the GiniCaptureFragment + * in Isolation. + * Mock was needed for [NetworkRequestsManager], [GiniCapture], [GiniCapture.Internal], + * [ImageMultiPageDocumentMemoryStore], [GiniBankConfigurationProvider] + * Also, in [GiniCaptureFragment], we are using koin to update + * the [GiniBankConfigurationProvider], for that we need to load and unload the module, + * other wise an exception will be thrown from koin for not loaded module. + * In the end, we need to set the mocked [GiniCapture] instance, and we have a helper class + * [GiniCaptureHelperForInstrumentationTests] for that. + * + * */ + + @Before + fun setUp() { + CaptureSdkIsolatedKoinContext.koin.loadModules(listOf(koinTestModule)) + + networkRequestsManager = mock() + giniCapture = mock() + giniInternal = mock() + memoryStore = mock() + + whenever(giniInternal.networkRequestsManager).thenReturn(networkRequestsManager) + whenever(giniCapture.internal()).thenReturn(giniInternal) + whenever(giniCapture.entryPoint).thenReturn(EntryPoint.BUTTON) + whenever(giniInternal.imageMultiPageDocumentMemoryStore).thenReturn(memoryStore) + whenever(giniInternal.navigationBarTopAdapterInstance).thenReturn( + InjectedViewAdapterInstance(DefaultNavigationBarTopAdapter()) + ) + whenever(giniCapture.documentImportEnabledFileTypes).thenReturn( + DocumentImportEnabledFileTypes.NONE + ) + whenever(giniCapture.internal().loadingIndicatorAdapterInstance).thenReturn( + InjectedViewAdapterInstance(DefaultLoadingIndicatorAdapter()) + ) + + GiniCaptureHelperForInstrumentationTests.setGiniCaptureInstance(giniCapture) + } + + + /** + * Unload the koin modules which were loaded in the [setUp]. + * */ + + @After + fun tearDown() = CaptureSdkIsolatedKoinContext.koin.unloadModules(listOf(koinTestModule)) + + + @Test + fun analyticsTracker_shouldBeEmpty_whenUserJourneyDisabled() { + + whenever(networkRequestsManager.getConfigurations(any())).thenReturn( + CompletableFuture.completedFuture(getMockedConfiguration(userJourneyEnabled = false)) + ) + + launchGiniCaptureFragment().use { scenario -> + + scenario.moveToState(Lifecycle.State.STARTED) + + scenario.onFragment { _ -> + assertThat(getAnalyticsTracker().getTrackers()).isEmpty() + } + } + } + + + @Test + fun analyticsTracker_shouldNotBeEmpty_whenUserJourneyEnabled() { + + whenever(networkRequestsManager.getConfigurations(any())).thenReturn( + CompletableFuture.completedFuture(getMockedConfiguration(userJourneyEnabled = true)) + ) + + launchGiniCaptureFragment().use { scenario -> + + scenario.moveToState(Lifecycle.State.STARTED) + + scenario.onFragment { _ -> + assertThat(getAnalyticsTracker().getTrackers()).isNotEmpty() + } + } + } + + + private fun getMockedConfiguration(userJourneyEnabled: Boolean): ConfigurationNetworkResult { + val testConfig = Configuration( + id = UUID.randomUUID(), + clientID = TEST_CLIENT_ID, + isUserJourneyAnalyticsEnabled = userJourneyEnabled, + isSkontoEnabled = false, + isReturnAssistantEnabled = false, + isTransactionDocsEnabled = false, + isQrCodeEducationEnabled = false, + isInstantPaymentEnabled = false, + isEInvoiceEnabled = false, + amplitudeApiKey = TEST_API_KEY + ) + + return ConfigurationNetworkResult(testConfig, UUID.randomUUID()) + } + + private fun getAnalyticsTracker(): BufferedUserAnalyticsEventTracker { + return UserAnalytics.getAnalyticsEventTracker() as BufferedUserAnalyticsEventTracker + } + + /** + * Helper method to launch the [GiniCaptureFragment] in a container, + * needed in all the tests. + * + * */ + + private fun launchGiniCaptureFragment(): FragmentScenario { + return FragmentScenario.launchInContainer( + fragmentClass = GiniCaptureFragment::class.java, + factory = object : FragmentFactory() { + override fun instantiate(classLoader: ClassLoader, className: String): Fragment { + return GiniCaptureFragment.createInstance().apply { + setListener(mock()) + } + } + } + ) + } + + companion object { + private const val TEST_CLIENT_ID = "test-client-id" + private const val TEST_API_KEY = "test-api-key" + } +} diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt index d680fcfa4b..2dd132a315 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt @@ -1,6 +1,7 @@ package net.gini.android.capture.tracking.useranalytics import android.content.Context +import androidx.annotation.VisibleForTesting import net.gini.android.capture.internal.network.NetworkRequestsManager import net.gini.android.capture.internal.provider.UniqueIdProvider import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsEventProperty @@ -141,4 +142,7 @@ internal class BufferedUserAnalyticsEventTracker( eventTrackers.forEach(block) } + @VisibleForTesting + internal fun getTrackers(): Set = eventTrackers.toSet() + } \ No newline at end of file diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt new file mode 100644 index 0000000000..de6b190bd6 --- /dev/null +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt @@ -0,0 +1,126 @@ +package net.gini.android.capture.tracking + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.mockk +import net.gini.android.capture.tracking.useranalytics.BufferedUserAnalyticsEventTracker +import net.gini.android.capture.tracking.useranalytics.UserAnalyticsEvent +import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsUserProperty +import net.gini.android.capture.tracking.useranalytics.tracker.AmplitudeUserAnalyticsEventTracker +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.Assert.assertTrue +import org.junit.Assert.assertFalse + +/** + * How our analytics working? + * + * We are initializing the [BufferedUserAnalyticsEventTracker] and it adds all the trackers through + * [BufferedUserAnalyticsEventTracker.setPlatformTokens] into a set. + * If the config value (isUserJourneyEnabled) from the backend is false, no tracker should be + * added to the set, and eventually no event will be tracked. + * + * If the values is true, the passed tracker (e.g [AmplitudeUserAnalyticsEventTracker]) + * will be added to the set and all the events will be tracked. + * + * This class will test this behaviour of [BufferedUserAnalyticsEventTracker]. If we pass false, + * No tracker should be added and vice versa. + * + * Things to consider: + * We are using queues to store the events, user properties and super properties. + * We can not test by checking these queues, because they are private, we can expose them for + * testing, but the main problem is that we are using polling for the queues + * in [BufferedUserAnalyticsEventTracker.trySendEvents] which empties the queues, so even if we + * expose them, it will be always empty after trackEvent is called, + * on the other the trackers servers the same thing, if the tracker is empty, it + * already means that no events will be tracked. That's why every test of this class will check + * the trackers set. For all positive and negative cases. + * + * */ + +@RunWith(AndroidJUnit4::class) +class BufferedUserAnalyticsEventTrackerTest { + + private lateinit var tracker: BufferedUserAnalyticsEventTracker + private val mockContext: Context = mockk(relaxed = true) + private val testSessionId = "test-session" + private val testApiKey = "test-api-key" + private val testCaptureVersion = "3.10.1" + + @Before + fun setup() { + tracker = BufferedUserAnalyticsEventTracker(mockContext, testSessionId) + } + + @Test + fun `when userJourney disabled, Initialize does not add trackers`() { + initializeTracker(isUserJourneyEnabled = false) + + tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED) + + assertTrue(tracker.getTrackers().isEmpty()) + } + + + @Test + fun `when userJourney enabled, Initialize does add trackers`() { + initializeTracker(isUserJourneyEnabled = true) + + tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED) + + assertFalse(tracker.getTrackers().isEmpty()) + } + + + @Test + fun `when userJourney disabled, setEventSuperProperty does not add trackers`() { + initializeTracker(isUserJourneyEnabled = false) + + tracker.setEventSuperProperty(emptySet()) + + assertTrue(tracker.getTrackers().isEmpty()) + } + + @Test + fun `when userJourney enabled, setEventSuperProperty does add trackers`() { + initializeTracker(isUserJourneyEnabled = true) + + tracker.setEventSuperProperty(emptySet()) + + assertFalse(tracker.getTrackers().isEmpty()) + } + + @Test + fun `when userJourney disabled, trackEvent does not add tracker`() { + initializeTracker(isUserJourneyEnabled = false) + + tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet()) + + assertTrue(tracker.getTrackers().isEmpty()) + } + + @Test + fun `when userJourney enabled, trackEvent does add tracker`() { + initializeTracker(isUserJourneyEnabled = true) + + tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet()) + + assertFalse(tracker.getTrackers().isEmpty()) + } + + private fun initializeTracker(isUserJourneyEnabled: Boolean) { + + tracker.setPlatformTokens( + AmplitudeUserAnalyticsEventTracker.AmplitudeAnalyticsApiKey(testApiKey), + networkRequestsManager = mockk(relaxed = true), + isUserJourneyEnabled = isUserJourneyEnabled + ) + + tracker.setUserProperty( + setOf( + UserAnalyticsUserProperty.CaptureSdkVersionName(testCaptureVersion), + ) + ) + } +} From 7210f71b4ce00d2ef6bcdd6b7a95a67374c1b13c Mon Sep 17 00:00:00 2001 From: obaidgini Date: Mon, 8 Sep 2025 17:18:16 +0200 Subject: [PATCH 002/116] feat(capture-sdk): accommodating the co-pilot comments PP-1700 --- .../tracking/BufferedUserAnalyticsEventTrackerTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt index de6b190bd6..35a9dd2cd9 100644 --- a/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt @@ -14,14 +14,14 @@ import org.junit.Assert.assertTrue import org.junit.Assert.assertFalse /** - * How our analytics working? + * How do our analytics work? * * We are initializing the [BufferedUserAnalyticsEventTracker] and it adds all the trackers through * [BufferedUserAnalyticsEventTracker.setPlatformTokens] into a set. * If the config value (isUserJourneyEnabled) from the backend is false, no tracker should be * added to the set, and eventually no event will be tracked. * - * If the values is true, the passed tracker (e.g [AmplitudeUserAnalyticsEventTracker]) + * If the value is true, the passed tracker (e.g [AmplitudeUserAnalyticsEventTracker]) * will be added to the set and all the events will be tracked. * * This class will test this behaviour of [BufferedUserAnalyticsEventTracker]. If we pass false, @@ -33,7 +33,7 @@ import org.junit.Assert.assertFalse * testing, but the main problem is that we are using polling for the queues * in [BufferedUserAnalyticsEventTracker.trySendEvents] which empties the queues, so even if we * expose them, it will be always empty after trackEvent is called, - * on the other the trackers servers the same thing, if the tracker is empty, it + * on the other hand, the trackers serve the same thing. If the tracker is empty, it * already means that no events will be tracked. That's why every test of this class will check * the trackers set. For all positive and negative cases. * From 401bf929711e0dedecd26d8567f322578da3b26a Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 1 Oct 2025 10:40:11 +0200 Subject: [PATCH 003/116] feat(capture-sdk): refactored the methods of UserAnalyticsEventTracker to return boolean for better testing PP-1700 --- .../useranalytics/UserAnalyticsEventTracker.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt index 4b86086224..73f475b310 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt @@ -7,19 +7,22 @@ import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsU interface UserAnalyticsEventTracker { - fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) + fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty): Boolean - fun setEventSuperProperty(property: Set) + fun setEventSuperProperty(property: Set): Boolean - fun setUserProperty(userProperty: UserAnalyticsUserProperty) + fun setUserProperty(userProperty: UserAnalyticsUserProperty): Boolean - fun setUserProperty(userProperties: Set) + fun setUserProperty(userProperties: Set): Boolean - fun trackEvent(eventName: UserAnalyticsEvent) + fun trackEvent(eventName: UserAnalyticsEvent): Boolean - fun trackEvent(eventName: UserAnalyticsEvent, properties: Set) + fun trackEvent( + eventName: UserAnalyticsEvent, + properties: Set + ): Boolean - fun flushEvents() + fun flushEvents(): Boolean } From 9312a3056501228e45542a5304183b7593beedec Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 1 Oct 2025 10:40:36 +0200 Subject: [PATCH 004/116] feat(capture-sdk): refactored the methods of AmplitudeUserAnalyticsEventTracker to return boolean for better testing PP-1700 --- .../AmplitudeUserAnalyticsEventTracker.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/tracker/AmplitudeUserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/tracker/AmplitudeUserAnalyticsEventTracker.kt index 017b4e0e93..6f257fb449 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/tracker/AmplitudeUserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/tracker/AmplitudeUserAnalyticsEventTracker.kt @@ -45,24 +45,26 @@ internal class AmplitudeUserAnalyticsEventTracker( context ) - override fun setUserProperty(userProperties: Set) { + override fun setUserProperty(userProperties: Set) : Boolean { this.userProperties = userProperties.associate { it.getPair() } + return true } - override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) { - setEventSuperProperty(setOf(property)) + override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) : Boolean{ + return setEventSuperProperty(setOf(property)) } - override fun setEventSuperProperty(property: Set) { + override fun setEventSuperProperty(property: Set) : Boolean{ superProperties.addAll(property) + return true } - override fun setUserProperty(userProperty: UserAnalyticsUserProperty) { - setUserProperty(setOf(userProperty)) + override fun setUserProperty(userProperty: UserAnalyticsUserProperty): Boolean { + return setUserProperty(setOf(userProperty)) } - override fun trackEvent(eventName: UserAnalyticsEvent) { - trackEvent(eventName, emptySet()) + override fun trackEvent(eventName: UserAnalyticsEvent) : Boolean{ + return trackEvent(eventName, emptySet()) } private val events: MutableList = mutableListOf() @@ -70,7 +72,7 @@ internal class AmplitudeUserAnalyticsEventTracker( override fun trackEvent( eventName: UserAnalyticsEvent, properties: Set - ) { + ) : Boolean{ val superPropertiesMap = superProperties.associate { it.getPair() } val propertiesMap = properties.associate { it.getPair() } @@ -102,6 +104,7 @@ internal class AmplitudeUserAnalyticsEventTracker( LOG.debug("\nEvent: ${eventName.eventName}\n" + finalProperties.toList().joinToString("\n") { " ${it.first}=${it.second}" }) + return true } fun startRepeatingJob(): Job { @@ -113,10 +116,11 @@ internal class AmplitudeUserAnalyticsEventTracker( } } - override fun flushEvents() { + override fun flushEvents() : Boolean{ CoroutineScope(Dispatchers.IO).launch { sendEventsToAmplitudeApi() } + return true } private fun sendEventsToAmplitudeApi() { From 2463adbe699065ff77a237f85aaf30417dc906de Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 1 Oct 2025 10:40:56 +0200 Subject: [PATCH 005/116] feat(capture-sdk): refactored the methods of BufferedUserAnalyticsEventTracker to return boolean for better testing PP-1700 --- .../BufferedUserAnalyticsEventTracker.kt | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt index 2dd132a315..aaf79d0b01 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt @@ -65,52 +65,54 @@ internal class BufferedUserAnalyticsEventTracker( } - override fun setEventSuperProperty(property: Set) { + override fun setEventSuperProperty(property: Set): Boolean { if (!mIsUserJourneyEnabled) - return + return false this.eventSuperProperties.add(property) - trySendEvents() + return trySendEvents() } - override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) { - setEventSuperProperty(setOf(property)) + override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty): Boolean { + return setEventSuperProperty(setOf(property)) } - override fun setUserProperty(userProperties: Set) { + override fun setUserProperty(userProperties: Set): Boolean { if (!mIsUserJourneyEnabled) - return + return false this.userProperties.add(userProperties) - trySendEvents() + return trySendEvents() } - override fun setUserProperty(userProperty: UserAnalyticsUserProperty) { - setUserProperty(setOf(userProperty)) + override fun setUserProperty(userProperty: UserAnalyticsUserProperty): Boolean { + return setUserProperty(setOf(userProperty)) } override fun trackEvent( eventName: UserAnalyticsEvent, properties: Set - ) { + ): Boolean { if (!mIsUserJourneyEnabled) - return + return false events.add(Pair(eventName, properties)) - trySendEvents() + return trySendEvents() } - override fun trackEvent(eventName: UserAnalyticsEvent) { - trackEvent(eventName, emptySet()) + override fun trackEvent(eventName: UserAnalyticsEvent): Boolean { + return trackEvent(eventName, emptySet()) } - override fun flushEvents() { - amplitude?.flushEvents() + override fun flushEvents(): Boolean { + return amplitude?.let { + amplitude?.flushEvents() + } ?: false } - private fun trySendEvents() { + private fun trySendEvents(): Boolean { if (!mIsUserJourneyEnabled) - return + return false if (eventTrackers.isEmpty()) { LOG.debug("No trackers found. Skipping sending events") - return + return false } LOG.debug("${eventTrackers.size} Tracker(s) found. Sending events...") @@ -136,6 +138,7 @@ internal class BufferedUserAnalyticsEventTracker( LOG.debug("Events sent") + return true } private fun everyTracker(block: (UserAnalyticsEventTracker) -> Unit) { From e565aea42b179367ed30550546d2b5b2d5b3cabd Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 1 Oct 2025 10:42:08 +0200 Subject: [PATCH 006/116] feat(capture-sdk): Changed the comments as per PR comments PP-1700 --- .../capture/ginicapture/GiniCaptureFragmentTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt index e04e17b2c6..3c250a48a0 100644 --- a/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt +++ b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt @@ -34,13 +34,13 @@ import com.nhaarman.mockitokotlin2.whenever import java.util.UUID /** - * This class is responsible for testing the behavior of BufferedUserAnalyticsEventTracker - * in-case the user journey analytics is enabled or disabled from the Gini API. - * for more detailed information how Analytics works in this project, - * Please refer to the documentation written in the Unit tests of - * BufferedUserAnalyticsEventTracker - * [net.gini.android.capture.tracking.BufferedUserAnalyticsEventTrackerTest] - * + * Integration test to verify the correct behavior of Analytics. + * Classes involved + * - [GiniCaptureFragment] + * - [GiniCapture] + * - [GiniCapture.Internal] + * - [NetworkRequestsManager] + * - [BufferedUserAnalyticsEventTracker] * */ @RunWith(AndroidJUnit4::class) From b4fd4283114b5e32063cf6b073f2e5465517826e Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 1 Oct 2025 10:43:39 +0200 Subject: [PATCH 007/116] feat(capture-sdk): Changed the tests, according to new refactor and booleans. PP-1700 --- .../BufferedUserAnalyticsEventTrackerTest.kt | 85 ++++++++----------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt index 35a9dd2cd9..9ba40844f5 100644 --- a/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt @@ -14,38 +14,27 @@ import org.junit.Assert.assertTrue import org.junit.Assert.assertFalse /** - * How do our analytics work? + * Tests the behavior of [BufferedUserAnalyticsEventTracker]. * - * We are initializing the [BufferedUserAnalyticsEventTracker] and it adds all the trackers through - * [BufferedUserAnalyticsEventTracker.setPlatformTokens] into a set. - * If the config value (isUserJourneyEnabled) from the backend is false, no tracker should be - * added to the set, and eventually no event will be tracked. + * If userJourney is disabled, no tracker is added and events/properties return false. + * If enabled, trackers are added and methods return true. * - * If the value is true, the passed tracker (e.g [AmplitudeUserAnalyticsEventTracker]) - * will be added to the set and all the events will be tracked. - * - * This class will test this behaviour of [BufferedUserAnalyticsEventTracker]. If we pass false, - * No tracker should be added and vice versa. - * - * Things to consider: - * We are using queues to store the events, user properties and super properties. - * We can not test by checking these queues, because they are private, we can expose them for - * testing, but the main problem is that we are using polling for the queues - * in [BufferedUserAnalyticsEventTracker.trySendEvents] which empties the queues, so even if we - * expose them, it will be always empty after trackEvent is called, - * on the other hand, the trackers serve the same thing. If the tracker is empty, it - * already means that no events will be tracked. That's why every test of this class will check - * the trackers set. For all positive and negative cases. - * - * */ + * Each test verifies this behavior by checking method results instead of internal queues. + */ @RunWith(AndroidJUnit4::class) class BufferedUserAnalyticsEventTrackerTest { private lateinit var tracker: BufferedUserAnalyticsEventTracker private val mockContext: Context = mockk(relaxed = true) + + // Random testSessionId for testing private val testSessionId = "test-session" + + // Random api key for testing private val testApiKey = "test-api-key" + + // This is an arbitrary version for testing private val testCaptureVersion = "3.10.1" @Before @@ -54,62 +43,62 @@ class BufferedUserAnalyticsEventTrackerTest { } @Test - fun `when userJourney disabled, Initialize does not add trackers`() { - initializeTracker(isUserJourneyEnabled = false) + fun `trackEvent returns false when userJourney is disabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = false) - tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED) + val result = tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED) - assertTrue(tracker.getTrackers().isEmpty()) + assertFalse(result) } @Test - fun `when userJourney enabled, Initialize does add trackers`() { - initializeTracker(isUserJourneyEnabled = true) + fun `trackEvent returns true when userJourney is enabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = true) - tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED) + val result = tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED) - assertFalse(tracker.getTrackers().isEmpty()) + assertTrue(result) } @Test - fun `when userJourney disabled, setEventSuperProperty does not add trackers`() { - initializeTracker(isUserJourneyEnabled = false) + fun `setEventSuperProperty returns false when userJourney is disabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = false) - tracker.setEventSuperProperty(emptySet()) + val result = tracker.setEventSuperProperty(emptySet()) - assertTrue(tracker.getTrackers().isEmpty()) + assertFalse(result) } @Test - fun `when userJourney enabled, setEventSuperProperty does add trackers`() { - initializeTracker(isUserJourneyEnabled = true) + fun `setEventSuperProperty returns true when userJourney is enabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = true) - tracker.setEventSuperProperty(emptySet()) + val result = tracker.setEventSuperProperty(emptySet()) - assertFalse(tracker.getTrackers().isEmpty()) + assertTrue(result) } @Test - fun `when userJourney disabled, trackEvent does not add tracker`() { - initializeTracker(isUserJourneyEnabled = false) + fun `trackEvent with properties returns false when userJourney is disabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = false) - tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet()) + val result = tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet()) - assertTrue(tracker.getTrackers().isEmpty()) + assertFalse(result) } @Test - fun `when userJourney enabled, trackEvent does add tracker`() { - initializeTracker(isUserJourneyEnabled = true) + fun `trackEvent with properties returns true when userJourney is enabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = true) - tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet()) + val result = tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet()) - assertFalse(tracker.getTrackers().isEmpty()) + assertTrue(result) } - private fun initializeTracker(isUserJourneyEnabled: Boolean) { + private fun setupTrackerWithUserJourney(isUserJourneyEnabled: Boolean) { tracker.setPlatformTokens( AmplitudeUserAnalyticsEventTracker.AmplitudeAnalyticsApiKey(testApiKey), @@ -119,7 +108,7 @@ class BufferedUserAnalyticsEventTrackerTest { tracker.setUserProperty( setOf( - UserAnalyticsUserProperty.CaptureSdkVersionName(testCaptureVersion), + UserAnalyticsUserProperty.CaptureSdkVersionName(testCaptureVersion) ) ) } From 025d397be7d7f8b240834ff1c07bd0fad843845e Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 1 Oct 2025 10:45:06 +0200 Subject: [PATCH 008/116] feat(capture-sdk): Changed the setup according to new return type of methods PP-1700 --- .../capture/onboarding/OnboardingScreenPresenterTest.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/onboarding/OnboardingScreenPresenterTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/onboarding/OnboardingScreenPresenterTest.kt index 7a4fdbe6ce..df978634fa 100644 --- a/capture-sdk/sdk/src/test/java/net/gini/android/capture/onboarding/OnboardingScreenPresenterTest.kt +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/onboarding/OnboardingScreenPresenterTest.kt @@ -4,12 +4,9 @@ import android.app.Activity import com.google.common.collect.Lists import com.google.common.truth.Correspondence import com.google.common.truth.Truth -import io.mockk.Runs import io.mockk.every -import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject -import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify import junitparams.JUnitParamsRunner @@ -53,8 +50,8 @@ class OnboardingScreenPresenterTest { @Before fun setUp() { mUserAnalyticsEventTracker = mockk().apply { - every { trackEvent(any()) } just Runs - every { trackEvent(any(), any()) } just Runs + every { trackEvent(any()) } returns true + every { trackEvent(any(), any()) } returns true } mActivity = mockk() From c2ff4764ef181f6f6313b0b79b638ff90d4cb435 Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 1 Oct 2025 10:55:56 +0200 Subject: [PATCH 009/116] feat(capture-sdk): Addressing the detekt issues PP-1700 --- .../useranalytics/BufferedUserAnalyticsEventTracker.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt index aaf79d0b01..2a46fea71d 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt @@ -108,10 +108,9 @@ internal class BufferedUserAnalyticsEventTracker( } private fun trySendEvents(): Boolean { - if (!mIsUserJourneyEnabled) - return false - if (eventTrackers.isEmpty()) { - LOG.debug("No trackers found. Skipping sending events") + if (!mIsUserJourneyEnabled || eventTrackers.isEmpty()) { + if (eventTrackers.isEmpty()) + LOG.debug("No trackers found. Skipping sending events") return false } From 56bf21f0b5b63689ca1f971725be8279356a85da Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 1 Oct 2025 12:29:31 +0200 Subject: [PATCH 010/116] feat(capture-sdk): Resolving the test failures because we changed the definition of method. PP-1700 --- .../android/capture/camera/CameraFragmentImplTest.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/camera/CameraFragmentImplTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/camera/CameraFragmentImplTest.kt index 0fd65844c1..0dd5783f08 100644 --- a/capture-sdk/sdk/src/test/java/net/gini/android/capture/camera/CameraFragmentImplTest.kt +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/camera/CameraFragmentImplTest.kt @@ -6,13 +6,8 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import com.nhaarman.mockitokotlin2.* -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk import jersey.repackaged.jsr166e.CompletableFuture import net.gini.android.capture.GiniCapture import net.gini.android.capture.internal.camera.api.CameraInterface @@ -100,8 +95,8 @@ class CameraFragmentImplTest { on { visibility } doReturn View.INVISIBLE } val analyticsTrackerMock = mock { - on { trackEvent(any()) }.then {} - on { trackEvent(any(), any()) }.then {} + on { trackEvent(any()) }.thenReturn(true) + on { trackEvent(any(), any()) }.thenReturn(true) } fragmentImpl.mLayoutNoPermission = noPermissionLayoutMock From 3ec84d79517b3858c8dafbb09bed854087c84886 Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 15 Oct 2025 12:42:25 +0200 Subject: [PATCH 011/116] feat(capture-sdk): Adding a simple SharedPreferenceHelper to store and retrieve strings PP-1727: --- .../capture/util/SharedPreferenceHelper.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/util/SharedPreferenceHelper.kt diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/SharedPreferenceHelper.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/SharedPreferenceHelper.kt new file mode 100644 index 0000000000..8e82638d53 --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/SharedPreferenceHelper.kt @@ -0,0 +1,25 @@ +package net.gini.android.capture.util + +import android.content.Context +import androidx.core.content.edit + +/** + * Generic class for saving simple data in to the shared preferences. + * In future this could be extended to support other data types as well. + * */ + +object SharedPreferenceHelper { + + private const val PREFS_KEY = "generic_data_preferences" + const val SAF_STORAGE_URI_KEY = "SAF_storage_uri_key" + + fun saveString(key: String, value: String, context: Context) { + val prefs = context.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE) + prefs.edit { putString(key, value) } + } + + fun getString(key: String, context: Context): String? { + val prefs = context.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE) + return prefs.getString(key, "") + } +} \ No newline at end of file From 95a11d235c5a6ba5b4471cda9dd0410267db51ca Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 15 Oct 2025 12:42:58 +0200 Subject: [PATCH 012/116] feat(capture-sdk): Adding a Helper class for all the SAF related work at one place PP-1727 --- .../gini/android/capture/util/SAFHelper.kt | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/util/SAFHelper.kt diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/SAFHelper.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/SAFHelper.kt new file mode 100644 index 0000000000..210b4b3ae5 --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/SAFHelper.kt @@ -0,0 +1,130 @@ +package net.gini.android.capture.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import net.gini.android.capture.R + + +/** + * SAFHelper + * + * This object helps with saving files using Android's Storage Access Framework (SAF). + * It checks write permissions, opens a folder picker if needed, remembers folder access, + * and saves one or more files into the selected folder. + * + * Used when the app needs to let the user choose a folder and save files. + */ + +object SAFHelper { + + /** + * Checks if the app has write permission for the given folder URI. + * + * @param context The context used to access the content resolver. + * @param folderUri The URI of the folder to check. + * @return True if write permission exists, false otherwise. + */ + + fun hasWritePermission(context: Context, folderUri: Uri): Boolean { + val result = context.contentResolver.persistedUriPermissions.any { + it.uri == folderUri && it.isWritePermission + } + return result + } + + /** + * Creates an intent that opens a system folder picker for the user. + * The intent requests both read and write access to the chosen folder. + * + * @return Intent ready to start with startActivityForResult() or ActivityResultLauncher. + */ + + fun createFolderPickerIntent(): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ) + } + } + + /** + * Saves the user's folder selection permission so it can be reused later + * without asking again. + * + * @param context The context used to access the content resolver. + * @param dataIntent The intent returned from the folder picker. + */ + + fun persistFolderPermission(context: Context, dataIntent: Intent?) { + if (dataIntent == null) return + val folderUri = dataIntent.data ?: return + val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + try { + context.contentResolver.takePersistableUriPermission(folderUri, takeFlags) + } catch (e: SecurityException) { + e.printStackTrace() + } + } + + /** + * Saves multiple files to the selected folder. + * + * @param context The context used to access files and resolver. + * @param folderUri The URI of the destination folder. + * @param sourceUris List of URIs of the source files to copy. + * @return Number of files successfully saved. + */ + + fun saveFilesToFolder(context: Context, + folderUri: Uri, + sourceUris: List, + ): Int = runBlocking { + withContext(Dispatchers.IO) { + val pickedDir = DocumentFile.fromTreeUri(context, folderUri) ?: return@withContext 0 + + val results = sourceUris.map { uri -> + val fileName = context.getString(R.string.gc_invoice_file_name, System.currentTimeMillis()) + async { saveSingleFile(context, pickedDir, uri, fileName) } + }.awaitAll() + + results.count { it } + } + } + + /** + * Copies one file at a time to the given folder. + * + * @param context The context used to open streams. + * @param folder The DocumentFile representing the target folder. + * @param sourceUri The URI of the source file. + * @return True if the file was saved successfully, false otherwise. + */ + + private fun saveSingleFile(context: Context, folder: DocumentFile, sourceUri: Uri, fileName: String): Boolean { + return try { + val resolver = context.contentResolver + val newFile = folder.createFile("image/jpeg", fileName) ?: return false + + resolver.openInputStream(sourceUri).use { input -> + resolver.openOutputStream(newFile.uri).use { output -> + if (input == null || output == null) return false + input.copyTo(output) + } + } + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } +} \ No newline at end of file From 2a4439df9a8ff41da0c658c34d7e66592c1c24c2 Mon Sep 17 00:00:00 2001 From: obaidgini Date: Wed, 15 Oct 2025 13:11:22 +0200 Subject: [PATCH 013/116] feat(capture-sdk): Adding the switch track color and background, also the design, this is temporary, will be changed according to new design PP-1727 --- .../main/res/color/gbs_switch_thumb_color.xml | 5 ++ .../main/res/color/gbs_switch_track_color.xml | 5 ++ .../gc_fragment_multi_page_review.xml | 69 ++++++++++++++++++- .../layout/gc_fragment_multi_page_review.xml | 54 ++++++++++++++- 4 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 capture-sdk/sdk/src/main/res/color/gbs_switch_thumb_color.xml create mode 100644 capture-sdk/sdk/src/main/res/color/gbs_switch_track_color.xml diff --git a/capture-sdk/sdk/src/main/res/color/gbs_switch_thumb_color.xml b/capture-sdk/sdk/src/main/res/color/gbs_switch_thumb_color.xml new file mode 100644 index 0000000000..15b0ff889a --- /dev/null +++ b/capture-sdk/sdk/src/main/res/color/gbs_switch_thumb_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/capture-sdk/sdk/src/main/res/color/gbs_switch_track_color.xml b/capture-sdk/sdk/src/main/res/color/gbs_switch_track_color.xml new file mode 100644 index 0000000000..3ee655556d --- /dev/null +++ b/capture-sdk/sdk/src/main/res/color/gbs_switch_track_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/capture-sdk/sdk/src/main/res/layout-land/gc_fragment_multi_page_review.xml b/capture-sdk/sdk/src/main/res/layout-land/gc_fragment_multi_page_review.xml index 3b075f51ae..0125073550 100644 --- a/capture-sdk/sdk/src/main/res/layout-land/gc_fragment_multi_page_review.xml +++ b/capture-sdk/sdk/src/main/res/layout-land/gc_fragment_multi_page_review.xml @@ -41,6 +41,15 @@ app:layout_constraintStart_toStartOf="@+id/gc_view_pager_wrapper" app:layout_constraintTop_toBottomOf="@id/gc_navigation_top_bar" /> + + + @@ -70,14 +79,68 @@ + + + + + + + + + + + + + app:layout_constraintStart_toEndOf="@+id/gc_guideline_viewpager_end" + app:layout_constraintTop_toBottomOf="@+id/gc_save_invoices_wrapper"> + + + + + + + + + + + Date: Wed, 15 Oct 2025 13:12:55 +0200 Subject: [PATCH 014/116] feat(capture-sdk): Adding strings and styles, every item is temporary and will be changed in next commits PP-1727 --- bank-sdk/example-app/src/main/res/values/strings.xml | 1 + capture-sdk/sdk/src/main/res/values-en/strings.xml | 3 +++ capture-sdk/sdk/src/main/res/values/strings.xml | 5 +++++ capture-sdk/sdk/src/main/res/values/styles.xml | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/bank-sdk/example-app/src/main/res/values/strings.xml b/bank-sdk/example-app/src/main/res/values/strings.xml index 0b39c0ddc7..75bff5a440 100644 --- a/bank-sdk/example-app/src/main/res/values/strings.xml +++ b/bank-sdk/example-app/src/main/res/values/strings.xml @@ -90,6 +90,7 @@ Onboarding \`lighting\` page custom illustration Onboarding \`QR code\` page custom illustration Onboarding \`multi page\` page custom illustration + Remove SAF Data Onboarding custom bottom navigation bar The custom bottom navigation bar is shown if \`Bottom navigation bar\` is also enabled. diff --git a/capture-sdk/sdk/src/main/res/values-en/strings.xml b/capture-sdk/sdk/src/main/res/values-en/strings.xml index 895c5e46a7..7f372a71e5 100644 --- a/capture-sdk/sdk/src/main/res/values-en/strings.xml +++ b/capture-sdk/sdk/src/main/res/values-en/strings.xml @@ -146,6 +146,9 @@ Unknown QR code Retrieving invoice Make sure the payment details are visible + Save to gallery + A copy of the invoice will be saved to your local gallery. + FotoUeberweisung_Rechnung_%1$d. Pages Process Continue diff --git a/capture-sdk/sdk/src/main/res/values/strings.xml b/capture-sdk/sdk/src/main/res/values/strings.xml index 233095d59e..2ab21b05c3 100644 --- a/capture-sdk/sdk/src/main/res/values/strings.xml +++ b/capture-sdk/sdk/src/main/res/values/strings.xml @@ -163,6 +163,11 @@ QR-Code ohne Zahlinformationen Rechnung wird übermittelt Sind alle Zahlungsinformationen gut lesbar? + Save to gallery + A copy of the invoice will be saved to your local gallery. + FotoUeberweisung_Rechnung_%1$d. + Successfully saved %1$d files + Files not saved Seite Verarbeiten Verarbeiten diff --git a/capture-sdk/sdk/src/main/res/values/styles.xml b/capture-sdk/sdk/src/main/res/values/styles.xml index 8cc0d1d847..aaeced0db3 100644 --- a/capture-sdk/sdk/src/main/res/values/styles.xml +++ b/capture-sdk/sdk/src/main/res/values/styles.xml @@ -83,4 +83,10 @@ +