Repository: Imprint-Tech/imprint-sdk-android
Pull request #8: (CHORE)SPND-1161: Code enhancements
Author: login: Qubika-JuanPabloSandin
Created: 2025-03-19T13:06:48Z | Merged: 2025-03-20T20:29:06Z

<!--
Features -> FEAT
Chores -> CHORE
Tests -> TEST
Refactor -> REFACTOR
Fix -> FIX
Platform upgrade/improvement -> IMPR

Title format: (FEAT/CHORE/TEST/REFACTOR/FIX/IMPR)[JIRA-Ticket] Commit message
-->

## Ticket Link(s)
<!-- Add the link(s) of the JIRA ticket(s) specified in the title -->
https://imprint.atlassian.net/browse/SPND-1161?atlOrigin=eyJpIjoiNjAzMjk3YWI2NWM5NDFjZDljMmU1ZjBhYjE1NGNiNzkiLCJwIjoiaiJ9

## Description
* Adding Koin as dependency injection tool in the sdk codebase. This also required some refactoring.
* Small changes to folder structure and classes naming






Diff:
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4d4ce89..a6842ee 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,6 @@
 [versions]
-agp = "8.5.2"
+agp = "8.7.3"
+koinAndroid = "4.0.2"
 kotlin = "2.1.0"
 coreKtx = "1.15.0"
 junit = "4.13.2"
@@ -18,7 +19,7 @@ activityCompose = "1.10.1"
 lifecycleRuntimeKtxVersion = "2.8.7"
 composeBom = "2025.03.00"
 kotlinBom = "2.1.0"
-imprintSDK = "add-demo-app-SNAPSHOT"
+imprintSDK = "SPND-1161-adr-perform-code-enhancements-to-the-sdk-codebase-2-SNAPSHOT"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -28,6 +29,7 @@ androidx-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedat
 androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
 androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
 androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" }
 material = { group = "com.google.android.material", name = "material", version.ref = "material" }
 material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
 androidx-ui = { module = "androidx.compose.ui:ui", version.ref = "ui" }
@@ -40,7 +42,7 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi
 androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
 androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
 kotlin-bom = { group = "org.jetbrains.kotlin", name = "kotlin-bom", version.ref = "kotlinBom" }
-imprint-sdk = { group = "com.github.Imprint-Tech", name = "imprint-sdk-android", version.ref = "imprintSDK"}
+imprint-sdk = { group = "com.github.Imprint-Tech", name = "imprint-sdk-android", version.ref = "imprintSDK" }
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 04d20b5..76f0dfd 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 #Thu Nov 07 15:10:53 PST 2024
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/sdk-demo/build.gradle.kts b/sdk-demo/build.gradle.kts
index c033297..0589bf5 100644
--- a/sdk-demo/build.gradle.kts
+++ b/sdk-demo/build.gradle.kts
@@ -1,5 +1,3 @@
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
 plugins {
   alias(libs.plugins.android.application)
   alias(libs.plugins.jetbrains.kotlin.android)
@@ -63,4 +61,6 @@ dependencies {
   implementation(libs.androidx.lifecycle.viewmodel.compose)
   implementation(libs.androidx.ui.tooling.preview)
   implementation(libs.imprint.sdk)
+  //Koin
+  implementation(libs.koin.android)
 }
\ No newline at end of file
diff --git a/sdk-demo/src/main/java/co/imprint/sdkdemo/MainActivity.kt b/sdk-demo/src/main/java/co/imprint/sdkdemo/MainActivity.kt
index 994f636..50fc12f 100644
--- a/sdk-demo/src/main/java/co/imprint/sdkdemo/MainActivity.kt
+++ b/sdk-demo/src/main/java/co/imprint/sdkdemo/MainActivity.kt
@@ -5,10 +5,21 @@ import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.lifecycle.viewmodel.compose.viewModel
 import co.imprint.sdkdemo.ui.theme.ImprintSDKDemoTheme
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.context.GlobalContext.startKoin
 
 class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
+
+    //Starting Koin instance to test the Koin context isolation of the SDK
+    startKoin {
+      androidLogger()
+      androidContext(applicationContext)
+      //No modules loaded
+    }
+
     setContent {
       ImprintSDKDemoTheme {
         val viewModel: MainViewModel = viewModel()
diff --git a/sdk-demo/src/main/java/co/imprint/sdkdemo/MainViewModel.kt b/sdk-demo/src/main/java/co/imprint/sdkdemo/MainViewModel.kt
index a54878c..e0fda57 100644
--- a/sdk-demo/src/main/java/co/imprint/sdkdemo/MainViewModel.kt
+++ b/sdk-demo/src/main/java/co/imprint/sdkdemo/MainViewModel.kt
@@ -2,9 +2,10 @@ package co.imprint.sdkdemo
 
 import android.content.Context
 import androidx.lifecycle.ViewModel
-import co.imprint.sdk.api.CompletionState
 import co.imprint.sdk.Imprint
-import co.imprint.sdk.api.ImprintConfiguration
+import co.imprint.sdk.domain.model.ImprintCompletionState
+import co.imprint.sdk.domain.model.ImprintConfiguration
+import co.imprint.sdk.domain.model.ImprintEnvironment
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 
@@ -35,9 +36,9 @@ class MainViewModel : ViewModel() {
 
   fun startApplication(context: Context) {
     val environment = when (_selectedEnvironment.value) {
-      Environment.STAGING -> ImprintConfiguration.Environment.STAGING
-      Environment.SANDBOX -> ImprintConfiguration.Environment.SANDBOX
-      Environment.PRODUCTION -> ImprintConfiguration.Environment.PRODUCTION
+      Environment.STAGING -> ImprintEnvironment.STAGING
+      Environment.SANDBOX -> ImprintEnvironment.SANDBOX
+      Environment.PRODUCTION -> ImprintEnvironment.PRODUCTION
     }
 
     // Configure the Imprint SDK with the required parameters
@@ -49,19 +50,22 @@ class MainViewModel : ViewModel() {
 
     // Callback function triggered when the application process is completed
     val onCompletion =
-      { state: CompletionState, metadata: Map<String, String?>? ->
+      { state: ImprintCompletionState, metadata: Map<String, String?>? ->
         val metadataInfo = metadata?.toString() ?: "No metadata"
         val resultText = when (state) {
-          CompletionState.OFFER_ACCEPTED -> {
+          ImprintCompletionState.OFFER_ACCEPTED -> {
             "Offer accepted\n$metadataInfo"
           }
-          CompletionState.REJECTED -> {
+
+          ImprintCompletionState.REJECTED -> {
             "Application rejected\n$metadataInfo"
           }
-          CompletionState.ABANDONED -> {
+
+          ImprintCompletionState.ABANDONED -> {
             "Application abandoned"
           }
-          CompletionState.ERROR -> {
+
+          ImprintCompletionState.ERROR -> {
             "Error occurred"
           }
         }
diff --git a/sdk/build.gradle.kts b/sdk/build.gradle.kts
index f9bb9e8..6f7bae8 100644
--- a/sdk/build.gradle.kts
+++ b/sdk/build.gradle.kts
@@ -12,8 +12,6 @@ android {
 
   defaultConfig {
     minSdk = 26
-    targetSdk = 35
-
     testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
   }
 
@@ -71,9 +69,11 @@ dependencies {
 
   // Lifecycle dependency for view models in Compose
   implementation(libs.androidx.lifecycle.viewmodel.compose)
-
   implementation(libs.androidx.activity.compose)
 
+  //Koin
+  implementation(libs.koin.android)
+
   // Optional: For testing Compose UI
   androidTestImplementation(libs.androidx.ui.test.junit4)
   debugImplementation(libs.androidx.ui.tooling)
diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml
index 4a5669c..f88698b 100644
--- a/sdk/src/main/AndroidManifest.xml
+++ b/sdk/src/main/AndroidManifest.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
     <uses-permission android:name="android.permission.INTERNET" />
-    <application
-        android:usesCleartextTraffic="true">
+    <application android:usesCleartextTraffic="true">
         <activity
-            android:name="co.imprint.sdk.ui.ApplicationActivity"
-            android:exported="true"/>
+            android:name="co.imprint.sdk.presentation.ApplicationActivity"
+            android:exported="true" />
     </application>
 </manifest>
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/Imprint.kt b/sdk/src/main/java/co/imprint/sdk/Imprint.kt
index 9db84d1..8da346a 100644
--- a/sdk/src/main/java/co/imprint/sdk/Imprint.kt
+++ b/sdk/src/main/java/co/imprint/sdk/Imprint.kt
@@ -2,11 +2,11 @@ package co.imprint.sdk
 
 import android.content.Context
 import android.content.Intent
-import co.imprint.sdk.api.CompletionState
-import co.imprint.sdk.callback.ImprintCallbackHolder
-import co.imprint.sdk.api.ImprintConfiguration
-import co.imprint.sdk.ui.ApplicationActivity
-import co.imprint.sdk.ui.ApplicationActivity.Companion.APPLICATION_CONFIGURATION
+import co.imprint.sdk.domain.model.ImprintCompletionState
+import co.imprint.sdk.domain.ImprintCallbackHolder
+import co.imprint.sdk.domain.model.ImprintConfiguration
+import co.imprint.sdk.presentation.ApplicationActivity
+import co.imprint.sdk.presentation.ApplicationActivity.Companion.APPLICATION_CONFIGURATION
 
 object Imprint {
 
@@ -19,7 +19,7 @@ object Imprint {
   fun startApplication(
     context: Context,
     configuration: ImprintConfiguration,
-    onCompletion: (CompletionState, Map<String, String?>?) -> Unit,
+    onCompletion: (ImprintCompletionState, Map<String, String?>?) -> Unit,
   ) {
     ImprintCallbackHolder.onApplicationCompletion = onCompletion
     val intent = Intent(context, ApplicationActivity::class.java).apply {
diff --git a/sdk/src/main/java/co/imprint/sdk/callback/ImprintCallbackHolder.kt b/sdk/src/main/java/co/imprint/sdk/callback/ImprintCallbackHolder.kt
deleted file mode 100644
index f4098b3..0000000
--- a/sdk/src/main/java/co/imprint/sdk/callback/ImprintCallbackHolder.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package co.imprint.sdk.callback
-
-import co.imprint.sdk.api.CompletionState
-
-internal object ImprintCallbackHolder {
-  var onApplicationCompletion: ((CompletionState, Map<String, String?>?) -> Unit)? =
-    null
-}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/di/IsolatedKoinComponent.kt b/sdk/src/main/java/co/imprint/sdk/di/IsolatedKoinComponent.kt
new file mode 100644
index 0000000..da4b6c0
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/di/IsolatedKoinComponent.kt
@@ -0,0 +1,17 @@
+package co.imprint.sdk.di
+
+import org.koin.core.Koin
+import org.koin.core.component.KoinComponent
+
+/**
+ * Interface for accessing the SDK's isolated Koin context.
+ *
+ * Any class within the SDK that requires dependency injection **must** implement this interface
+ * to ensure that dependencies are resolved from the SDK's isolated Koin instance rather than
+ * the app's global Koin instance.
+ *
+ * This prevents conflicts when the host app also uses Koin for dependency management.
+ */
+internal interface IsolatedKoinComponent : KoinComponent {
+  override fun getKoin(): Koin = SdkIsolatedKoinContext.koin
+}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/di/SdkIsolatedKoinContext.kt b/sdk/src/main/java/co/imprint/sdk/di/SdkIsolatedKoinContext.kt
new file mode 100644
index 0000000..1eaea79
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/di/SdkIsolatedKoinContext.kt
@@ -0,0 +1,25 @@
+package co.imprint.sdk.di
+
+import co.imprint.sdk.di.modules.dispatchersModule
+import co.imprint.sdk.di.modules.viewModelsModule
+import org.koin.dsl.koinApplication
+
+/**
+ * This object holds the instance of our isolated Koin context.
+ * Since we are developing an SDK, this is necessary because an Android app can only have
+ * a single global instance of Koin. By isolating our context, we avoid conflicts with
+ * apps that also use Koin alongside our SDK.
+ *
+ * For more details, refer to:
+ * https://insert-koin.io/docs/reference/koin-core/context-isolation/
+ */
+internal object SdkIsolatedKoinContext {
+
+  private val koinApp = koinApplication {
+
+    // declare used modules
+    modules(dispatchersModule, viewModelsModule)
+  }
+
+  val koin = koinApp.koin
+}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/di/modules/DispatchersModule.kt b/sdk/src/main/java/co/imprint/sdk/di/modules/DispatchersModule.kt
new file mode 100644
index 0000000..85f0675
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/di/modules/DispatchersModule.kt
@@ -0,0 +1,12 @@
+package co.imprint.sdk.di.modules
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+val dispatchersModule = module {
+  single<CoroutineDispatcher>(named("DefaultDispatcher")) { Dispatchers.Default }
+  single<CoroutineDispatcher>(named("IoDispatcher")) { Dispatchers.IO }
+  single<CoroutineDispatcher>(named("MainDispatcher")) { Dispatchers.Main }
+}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/di/modules/ViewModelsModule.kt b/sdk/src/main/java/co/imprint/sdk/di/modules/ViewModelsModule.kt
new file mode 100644
index 0000000..161323e
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/di/modules/ViewModelsModule.kt
@@ -0,0 +1,13 @@
+package co.imprint.sdk.di.modules
+
+import androidx.lifecycle.SavedStateHandle
+import co.imprint.sdk.presentation.ApplicationViewModel
+import org.koin.core.module.dsl.viewModel
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+val viewModelsModule = module {
+  viewModel { (state: SavedStateHandle) ->
+    ApplicationViewModel(ioDispatcher = get(named("IoDispatcher")), state = state)
+  }
+}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/domain/ImprintCallbackHolder.kt b/sdk/src/main/java/co/imprint/sdk/domain/ImprintCallbackHolder.kt
new file mode 100644
index 0000000..06be4e8
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/domain/ImprintCallbackHolder.kt
@@ -0,0 +1,8 @@
+package co.imprint.sdk.domain
+
+import co.imprint.sdk.domain.model.ImprintCompletionState
+
+internal object ImprintCallbackHolder {
+  var onApplicationCompletion: ((ImprintCompletionState, Map<String, String?>?) -> Unit)? =
+    null
+}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/api/ImprintCompletionState.kt b/sdk/src/main/java/co/imprint/sdk/domain/model/ImprintCompletionState.kt
similarity index 59%
rename from sdk/src/main/java/co/imprint/sdk/api/ImprintCompletionState.kt
rename to sdk/src/main/java/co/imprint/sdk/domain/model/ImprintCompletionState.kt
index dd1d519..c194ddb 100644
--- a/sdk/src/main/java/co/imprint/sdk/api/ImprintCompletionState.kt
+++ b/sdk/src/main/java/co/imprint/sdk/domain/model/ImprintCompletionState.kt
@@ -1,17 +1,17 @@
-package co.imprint.sdk.api
+package co.imprint.sdk.domain.model
 
 /**
  * Terminal states for the application process.
  *
  * Possible values are [OFFER_ACCEPTED], [REJECTED], [ABANDONED], [ERROR].
  */
-enum class CompletionState {
+enum class ImprintCompletionState {
   OFFER_ACCEPTED, REJECTED, ABANDONED, ERROR;
 
   companion object {
-    fun fromString(value: String?): CompletionState {
+    fun fromString(value: String?): ImprintCompletionState {
       return try {
-        value?.let { enumValueOf<CompletionState>(it) } ?: ABANDONED
+        value?.let { enumValueOf<ImprintCompletionState>(it) } ?: ABANDONED
       } catch (e: IllegalArgumentException) {
         ABANDONED
       }
diff --git a/sdk/src/main/java/co/imprint/sdk/api/ImprintConfiguration.kt b/sdk/src/main/java/co/imprint/sdk/domain/model/ImprintConfiguration.kt
similarity index 66%
rename from sdk/src/main/java/co/imprint/sdk/api/ImprintConfiguration.kt
rename to sdk/src/main/java/co/imprint/sdk/domain/model/ImprintConfiguration.kt
index 49841c7..9510b84 100644
--- a/sdk/src/main/java/co/imprint/sdk/api/ImprintConfiguration.kt
+++ b/sdk/src/main/java/co/imprint/sdk/domain/model/ImprintConfiguration.kt
@@ -1,6 +1,7 @@
-package co.imprint.sdk.api
+package co.imprint.sdk.domain.model
 
 import android.os.Parcelable
+import kotlinx.parcelize.IgnoredOnParcel
 import kotlinx.parcelize.Parcelize
 
 /**
@@ -9,19 +10,16 @@ import kotlinx.parcelize.Parcelize
  * @param clientSecret The client secret used to initiate the application session. Generated through Create Customer Session.
  * Please refer to the API documentation (https://docs.imprint.co/api-reference/customer-sessions/create-a-new-customer-session) for details on obtaining a clientSecret.
  * @param partnerReference The unique identifier for the program provided by the Imprint team.
- * @param environment The environment for the application process. Default value is [Environment.PRODUCTION].
+ * @param environment The environment for the application process. Default value is [ImprintEnvironment.PRODUCTION].
  */
 @Parcelize
 data class ImprintConfiguration(
   val clientSecret: String,
   val partnerReference: String,
-  val environment: Environment = Environment.PRODUCTION,
+  val environment: ImprintEnvironment = ImprintEnvironment.PRODUCTION,
 ) : Parcelable {
 
-  /**
-   * Available environments for the application process.
-   */
-  enum class Environment {
-    STAGING, SANDBOX, PRODUCTION
-  }
+  @IgnoredOnParcel
+  internal val webUrl: String =
+    "${environment.hostUrl}/start?client_secret=${clientSecret}&partner_reference=${partnerReference}"
 }
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/domain/model/ImprintEnvironment.kt b/sdk/src/main/java/co/imprint/sdk/domain/model/ImprintEnvironment.kt
new file mode 100644
index 0000000..b2e6774
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/domain/model/ImprintEnvironment.kt
@@ -0,0 +1,10 @@
+package co.imprint.sdk.domain.model
+
+/**
+ * Available environments for the application process.
+ */
+enum class ImprintEnvironment(internal val hostUrl: String) {
+  STAGING(hostUrl = "https://apply.stg.imprintapi.co"),
+  SANDBOX(hostUrl = "https://apply.sbx.imprint.co"),
+  PRODUCTION(hostUrl = "https://apply.imprint.co")
+}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/presentation/ApplicationActivity.kt b/sdk/src/main/java/co/imprint/sdk/presentation/ApplicationActivity.kt
new file mode 100644
index 0000000..2906f35
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/presentation/ApplicationActivity.kt
@@ -0,0 +1,26 @@
+package co.imprint.sdk.presentation
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import co.imprint.sdk.di.IsolatedKoinComponent
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+internal class ApplicationActivity : ComponentActivity(), IsolatedKoinComponent {
+
+  companion object {
+    const val APPLICATION_CONFIGURATION = "config"
+  }
+
+  override fun onCreate(savedInstanceState: Bundle?) {
+    super.onCreate(savedInstanceState)
+
+    val viewModel: ApplicationViewModel by viewModel()
+    setContent {
+      MaterialTheme {
+        ApplicationView(viewModel)
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/presentation/ApplicationView.kt b/sdk/src/main/java/co/imprint/sdk/presentation/ApplicationView.kt
new file mode 100644
index 0000000..ac134d3
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/presentation/ApplicationView.kt
@@ -0,0 +1,39 @@
+package co.imprint.sdk.presentation
+
+import android.app.Activity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import co.imprint.sdk.presentation.components.AppBar
+import co.imprint.sdk.presentation.components.WebViewWrapper
+
+@Composable
+internal fun ApplicationView(viewModel: ApplicationViewModel) {
+  val context = LocalContext.current
+  val activity = context as? Activity
+  val bitmap = viewModel.logoBitmap.collectAsState().value
+
+  Scaffold(
+    modifier = Modifier
+      .fillMaxSize()
+      .background(Color.White),
+    containerColor = Color.White,
+    topBar = {
+      AppBar(
+        bitmap = bitmap,
+        onDismiss = {
+          viewModel.onDismiss()
+          activity?.finish()
+        },
+      )
+    }
+  ) { innerPadding ->
+    WebViewWrapper(viewModel = viewModel, modifier = Modifier.padding(innerPadding))
+  }
+}
diff --git a/sdk/src/main/java/co/imprint/sdk/viewmodel/ApplicationViewModel.kt b/sdk/src/main/java/co/imprint/sdk/presentation/ApplicationViewModel.kt
similarity index 59%
rename from sdk/src/main/java/co/imprint/sdk/viewmodel/ApplicationViewModel.kt
rename to sdk/src/main/java/co/imprint/sdk/presentation/ApplicationViewModel.kt
index 8ba1c97..e57dcf1 100644
--- a/sdk/src/main/java/co/imprint/sdk/viewmodel/ApplicationViewModel.kt
+++ b/sdk/src/main/java/co/imprint/sdk/presentation/ApplicationViewModel.kt
@@ -1,13 +1,15 @@
-package co.imprint.sdk.viewmodel
+package co.imprint.sdk.presentation
 
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
+import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import co.imprint.sdk.api.CompletionState
-import co.imprint.sdk.callback.ImprintCallbackHolder
-import co.imprint.sdk.api.ImprintConfiguration
-import kotlinx.coroutines.Dispatchers
+import co.imprint.sdk.di.IsolatedKoinComponent
+import co.imprint.sdk.domain.ImprintCallbackHolder
+import co.imprint.sdk.domain.model.ImprintCompletionState
+import co.imprint.sdk.domain.model.ImprintConfiguration
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -16,26 +18,29 @@ import java.io.InputStream
 import java.net.HttpURLConnection
 import java.net.URL
 
-internal class ApplicationViewModel(private val configuration: ImprintConfiguration) : ViewModel() {
-  private val host = when (configuration.environment) {
-    ImprintConfiguration.Environment.STAGING -> "https://apply.stg.imprintapi.co"
-    ImprintConfiguration.Environment.SANDBOX -> "https://apply.sbx.imprint.co"
-    ImprintConfiguration.Environment.PRODUCTION -> "https://apply.imprint.co"
-  }
-  val webUrl = "$host/start?client_secret=${configuration.clientSecret}&partner_reference=${configuration.partnerReference}"
+internal class ApplicationViewModel(
+  private val ioDispatcher: CoroutineDispatcher,
+  state: SavedStateHandle,
+) : ViewModel(), IsolatedKoinComponent {
+
+  private val configuration: ImprintConfiguration =
+    state[ApplicationActivity.APPLICATION_CONFIGURATION]
+      ?: throw IllegalStateException("Imprint configuration is required to initialize the SDK")
+
+  val webUrl: String = configuration.webUrl
 
   private val _logoBitmap = MutableStateFlow<Bitmap?>(null)
   val logoBitmap: StateFlow<Bitmap?> = _logoBitmap.asStateFlow()
 
-  private var completionState: CompletionState = CompletionState.ABANDONED
+  private var completionState: ImprintCompletionState = ImprintCompletionState.ABANDONED
   private var completionData: Map<String, String?>? = null
 
   fun updateLogoUrl(url: String) {
-   loadImageBitmap(url = url)
+    loadImageBitmap(url = url)
   }
 
   fun updateCompletionState(
-    state: CompletionState,
+    state: ImprintCompletionState,
     data: Map<String, String?>?,
   ) {
     completionState = state
@@ -48,7 +53,7 @@ internal class ApplicationViewModel(private val configuration: ImprintConfigurat
   }
 
   private fun loadImageBitmap(url: String) {
-    viewModelScope.launch(Dispatchers.IO) {
+    viewModelScope.launch(ioDispatcher) {
       try {
         val connection = URL(url).openConnection() as HttpURLConnection
         connection.doInput = true
diff --git a/sdk/src/main/java/co/imprint/sdk/ui/ApplicationView.kt b/sdk/src/main/java/co/imprint/sdk/presentation/components/AppBar.kt
similarity index 55%
rename from sdk/src/main/java/co/imprint/sdk/ui/ApplicationView.kt
rename to sdk/src/main/java/co/imprint/sdk/presentation/components/AppBar.kt
index 7820d73..28ccd51 100644
--- a/sdk/src/main/java/co/imprint/sdk/ui/ApplicationView.kt
+++ b/sdk/src/main/java/co/imprint/sdk/presentation/components/AppBar.kt
@@ -1,49 +1,28 @@
-package co.imprint.sdk.ui
+package co.imprint.sdk.presentation.components
 
-import android.app.Activity
 import android.graphics.Bitmap
 import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Close
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
-import co.imprint.sdk.viewmodel.ApplicationViewModel
 
 @Composable
-internal fun ApplicationView(viewModel: ApplicationViewModel) {
-  val context = LocalContext.current
-  val activity = context as? Activity
-  val bitmap = viewModel.logoBitmap.collectAsState().value
-
-  Scaffold(
-    modifier = Modifier
-      .fillMaxSize()
-      .background(Color.White),
-    containerColor = Color.White,
-    topBar = {
-      AppBar(
-        bitmap = bitmap,
-        onDismiss = {
-          viewModel.onDismiss()
-          activity?.finish()
-        },
-      )
-    }
-  ) { innerPadding ->
-    WebViewWrapper(viewModel = viewModel, modifier = Modifier.padding(innerPadding))
-  }
-}
-
-@Composable
-private fun AppBar(
+internal fun AppBar(
   bitmap: Bitmap?,
   onDismiss: () -> Unit,
   modifier: Modifier = Modifier,
@@ -71,8 +50,7 @@ private fun LogoImage(bitmap: Bitmap?) {
         .wrapContentWidth()
         .padding(16.dp),
     )
-  }
-  else Box(modifier = Modifier.size(56.dp))
+  } else Box(modifier = Modifier.size(56.dp))
 }
 
 @Composable
diff --git a/sdk/src/main/java/co/imprint/sdk/ui/WebViewWrapper.kt b/sdk/src/main/java/co/imprint/sdk/presentation/components/WebViewWrapper.kt
similarity index 77%
rename from sdk/src/main/java/co/imprint/sdk/ui/WebViewWrapper.kt
rename to sdk/src/main/java/co/imprint/sdk/presentation/components/WebViewWrapper.kt
index ef5a3b8..ab270a1 100644
--- a/sdk/src/main/java/co/imprint/sdk/ui/WebViewWrapper.kt
+++ b/sdk/src/main/java/co/imprint/sdk/presentation/components/WebViewWrapper.kt
@@ -1,5 +1,6 @@
-package co.imprint.sdk.ui
+package co.imprint.sdk.presentation.components
 
+import android.annotation.SuppressLint
 import android.util.Log
 import android.webkit.JavascriptInterface
 import android.webkit.WebSettings
@@ -9,10 +10,12 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.viewinterop.AndroidView
-import co.imprint.sdk.viewmodel.ApplicationViewModel
-import co.imprint.sdk.api.CompletionState
+import co.imprint.sdk.domain.model.ImprintCompletionState
+import co.imprint.sdk.presentation.utils.toMapOrNull
+import co.imprint.sdk.presentation.ApplicationViewModel
 import org.json.JSONObject
 
+@SuppressLint("SetJavaScriptEnabled")
 @Composable
 internal fun WebViewWrapper(
   viewModel: ApplicationViewModel,
@@ -46,8 +49,8 @@ internal fun WebViewWrapper(
                 val logoURL = jsonObject.optString(Constants.LOGO_URL)
                 viewModel.updateLogoUrl(url = logoURL)
 
-                val completionData = dataJson?.let { jsonToMap(it) }
-                val state = CompletionState.fromString(eventName)
+                val completionData = dataJson.toMapOrNull()
+                val state = ImprintCompletionState.fromString(eventName)
                 viewModel.updateCompletionState(state, completionData)
               } catch (e: Exception) {
                 Log.e("WebViewWrapper", "onMessage: Error parsing data from Web view")
@@ -61,7 +64,7 @@ internal fun WebViewWrapper(
         loadUrl(viewModel.webUrl)
       }
     },
-    update = { webView ->
+    update = { _ ->
       // Optional: Additional WebView updates if needed
     }
   )
@@ -72,15 +75,4 @@ internal object Constants {
   const val LOGO_URL = "logoUrl"
   const val EVENT_NAME = "eventName"
   const val DATA = "data"
-}
-
-private fun jsonToMap(jsonObject: JSONObject): Map<String, String?> {
-  val map = mutableMapOf<String, String?>()
-  val keys = jsonObject.keys()
-  while (keys.hasNext()) {
-    val key = keys.next()
-    val value = jsonObject.optString(key) // Safely get the string value
-    map[key] = value
-  }
-  return map
 }
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/presentation/utils/Extensions.kt b/sdk/src/main/java/co/imprint/sdk/presentation/utils/Extensions.kt
new file mode 100644
index 0000000..684a12f
--- /dev/null
+++ b/sdk/src/main/java/co/imprint/sdk/presentation/utils/Extensions.kt
@@ -0,0 +1,15 @@
+package co.imprint.sdk.presentation.utils
+
+import org.json.JSONObject
+
+internal fun JSONObject?.toMapOrNull(): Map<String, String?>? {
+  if (this == null) return null
+  val mutableMap = mutableMapOf<String, String?>()
+  val keys = this.keys()
+  while (keys.hasNext()) {
+    val key = keys.next()
+    val value = this.optString(key) // Safely get the string value
+    mutableMap[key] = value
+  }
+  return mutableMap.toMap()
+}
\ No newline at end of file
diff --git a/sdk/src/main/java/co/imprint/sdk/ui/ApplicationActivity.kt b/sdk/src/main/java/co/imprint/sdk/ui/ApplicationActivity.kt
deleted file mode 100644
index b21524f..0000000
--- a/sdk/src/main/java/co/imprint/sdk/ui/ApplicationActivity.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package co.imprint.sdk.ui
-
-import android.os.Build
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.material3.MaterialTheme
-import co.imprint.sdk.viewmodel.ApplicationViewModel
-import co.imprint.sdk.api.ImprintConfiguration
-
-
-internal class ApplicationActivity : ComponentActivity() {
-
-  companion object {
-    const val APPLICATION_CONFIGURATION = "config"
-  }
-
-  override fun onCreate(savedInstanceState: Bundle?) {
-    super.onCreate(savedInstanceState)
-
-    // Retrieve configuration and make it non-nullable
-    val configuration = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
-      intent.getParcelableExtra(APPLICATION_CONFIGURATION, ImprintConfiguration::class.java)
-    } else {
-      @Suppress("DEPRECATION")
-      intent.getParcelableExtra<ImprintConfiguration>(APPLICATION_CONFIGURATION)
-    }
-
-    configuration?.let {
-      val viewModel = ApplicationViewModel(configuration = configuration)
-      // Set the Compose UI content
-      setContent {
-        MaterialTheme {
-          ApplicationView(viewModel)
-        }
-      }
-    }
-  }
-}
\ No newline at end of file


Reviews:
user: login: Qubika-JuanPabloSandin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:22:29Z
user: login: Qubika-JuanPabloSandin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:24:17Z
user: login: Qubika-JuanPabloSandin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:25:27Z
user: login: Qubika-JuanPabloSandin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:28:52Z
user: login: Qubika-JuanPabloSandin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:30:13Z
user: login: kaichang-imprint
body: The PR looks good overall! Thanks for the effort to improve the SDK.

One concern I have for the Koin integration is that, if the host APP also uses Koin, there will be an exception because the Koin is initialized twice. One solution could be the [Context Isolation](https://insert-koin.io/docs/reference/koin-core/context-isolation/). Could you take a look and see if it is a potential issue?
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T18:03:54Z
user: login: kaichang-imprint
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-20T15:44:44Z

Review comments:
diff_hunk: @@ -1,17 +1,17 @@
-package co.imprint.sdk.api
+package co.imprint.sdk.domain.model
 
 /**
  * Terminal states for the application process.
  *
  * Possible values are [OFFER_ACCEPTED], [REJECTED], [ABANDONED], [ERROR].
  */
-enum class CompletionState {
+enum class ImprintCompletionState {
path: sdk/src/main/java/co/imprint/sdk/domain/model/ImprintCompletionState.kt
user: login: Qubika-JuanPabloSandin
body: Renamed with the Imprint prefix just to be uniform with the rest of the domain classes
created_at: 2025-03-19T13:22:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,19 +10,16 @@ import kotlinx.parcelize.Parcelize
  * @param clientSecret The client secret used to initiate the application session. Generated through Create Customer Session.
  * Please refer to the API documentation (https://docs.imprint.co/api-reference/customer-sessions/create-a-new-customer-session) for details on obtaining a clientSecret.
  * @param partnerReference The unique identifier for the program provided by the Imprint team.
- * @param environment The environment for the application process. Default value is [Environment.PRODUCTION].
+ * @param environment The environment for the application process. Default value is [ImprintEnvironment.PRODUCTION].
  */
 @Parcelize
 data class ImprintConfiguration(
   val clientSecret: String,
   val partnerReference: String,
-  val environment: Environment = Environment.PRODUCTION,
+  val environment: ImprintEnvironment = ImprintEnvironment.PRODUCTION,
 ) : Parcelable {
 
-  /**
-   * Available environments for the application process.
-   */
-  enum class Environment {
-    STAGING, SANDBOX, PRODUCTION
-  }
+  @IgnoredOnParcel
+  internal val webUrl: String =
path: sdk/src/main/java/co/imprint/sdk/domain/model/ImprintConfiguration.kt
user: login: Qubika-JuanPabloSandin
body: As this web url is only based on the ImprintConfiguration instance, I believe its better to keep it here. 
created_at: 2025-03-19T13:24:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,26 +17,29 @@ import java.io.InputStream
 import java.net.HttpURLConnection
 import java.net.URL
 
-internal class ApplicationViewModel(private val configuration: ImprintConfiguration) : ViewModel() {
-  private val host = when (configuration.environment) {
-    ImprintConfiguration.Environment.STAGING -> "https://apply.stg.imprintapi.co"
-    ImprintConfiguration.Environment.SANDBOX -> "https://apply.sbx.imprint.co"
-    ImprintConfiguration.Environment.PRODUCTION -> "https://apply.imprint.co"
-  }
-  val webUrl = "$host/start?client_secret=${configuration.clientSecret}&partner_reference=${configuration.partnerReference}"
+internal class ApplicationViewModel(
+  private val ioDispatcher: CoroutineDispatcher,
+  state: SavedStateHandle,
+) : ViewModel() {
+
+  private val configuration: ImprintConfiguration =
path: sdk/src/main/java/co/imprint/sdk/presentation/ApplicationViewModel.kt
user: login: Qubika-JuanPabloSandin
body: Configuration is accesed directly using the injected state by Koin
created_at: 2025-03-19T13:25:26Z
author_association: CONTRIBUTOR
path: sdk/src/main/java/co/imprint/sdk/presentation/ApplicationActivity.kt
user: login: Qubika-JuanPabloSandin
body: One of the main changes here is that we don't need to retrieve the configuration here as the state is directly injected in the VM
created_at: 2025-03-19T13:28:52Z
author_association: CONTRIBUTOR
path: sdk/src/main/java/co/imprint/sdk/presentation/utils/Extensions.kt
user: login: Qubika-JuanPabloSandin
body: Moved this as an extension function so we can keep it away from WebViewWrapper file
created_at: 2025-03-19T13:30:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: macroscopeapp[bot]
created_at: 2025-03-19T13:07:29Z
body: ## Refactor SDK Architecture and Introduce Koin DI
Restructured SDK architecture by moving files to domain and presentation packages, introduced Koin dependency injection, updated Gradle and AGP versions, and renamed core SDK classes. Added `DispatchersModule` and `ViewModelModule` for DI configuration. Extracted UI components from `ApplicationView` into separate files and moved environment configuration to dedicated class.

### 📍Where to Start
The Koin module configurations in [ViewModelModule.kt](https://github.com/Imprint-Tech/imprint-sdk-android/pull/8/files#diff-8eff482817c487827dea788d512998efb5540abf9b1a027eec16f02502e01aae) and [DispatchersModule.kt](https://github.com/Imprint-Tech/imprint-sdk-android/pull/8/files#diff-cfc9adcc48105497eebc980c4493dc8f59468c14d6a4359690fac0abed9fe959), followed by the refactored [ApplicationActivity.kt](https://github.com/Imprint-Tech/imprint-sdk-android/pull/8/files#diff-a0c68455feb85ce66e9c04f971eccc15f5c606b108489b8dd2988dac5832e6f6)

----

_[Macroscope](https://app.macroscope.com) summarized 6e9842d and had no feedback after reviewing 0 code objects._
author_association: NONE
user: login: macroscopeapp[bot]
created_at: 2025-03-20T13:22:48Z
body: ### Changes in 41264e9

- Implemented isolated Koin dependency injection context in `co.imprint.sdk` through new `IsolatedKoinComponent` interface and `SdkIsolatedKoinContext` singleton, integrating with `ApplicationActivity` and `ApplicationViewModel`
- Restructured `co.imprint.sdk.di` package by moving `DispatchersModule` and `ViewModelsModule` to `modules` subpackage


----

_[Macroscope](https://app.macroscope.com) summarized 41264e9 and had no feedback after reviewing 0 code objects._
author_association: NONE
user: login: macroscopeapp[bot]
created_at: 2025-03-20T13:49:12Z
body: ### Changes in 72b6b72

- Added `koin-android` dependency to the project's Gradle build configuration
- Initialized `Koin` framework in Android `MainActivity.onCreate()` with empty module configuration and Android-specific logging setup


----

_[Macroscope](https://app.macroscope.com) summarized 72b6b72 and had no feedback after reviewing 0 code objects._
author_association: NONE