From 532890ff0638d54487fc1bc2df7605263175dc80 Mon Sep 17 00:00:00 2001 From: Vansh Gandhi Date: Tue, 8 Aug 2023 08:25:34 -0700 Subject: [PATCH] Prepare for 10.0.0-beta06 release (#187) * Remove Polling (#169) * Add Async Enhanced KYC endpoint (#168) * Add Async Enhanced KYC endpoint * Fix URL * Setting up loading button (#170) * setting up loading button * fixed pr comments * loading button tests * Include ID Info in Document Verification network request (#173) * Include ID Info in network request * Update CHANGELOG * Fix test * Update CHANGELOG * Actions updates (#178) * Support Merge Queue * Bump snapshot version * switch from Java 17 to 11 for the lib module (#179) * Setup Smile Config during Runtime (#160) * Remove polling (#145) * Remove polling * Update CHANGELOG * Rename getJobStatus to getSmartSelfieJobStatus to align with other job status names * Update error message to be actionable * Update error message to be actionable * Fix range * Add test * Add comment * Rename ImageType enums (#144) * Use `channelFlow` instead of `flow` * Update comment * Jobs List Screen (#148) * Jobs List Screen * Constraint trailingContent size * Review comments * Update Previews * Add DataStoreRepository * Persist Jobs (#158) * Orchestrated Jobs Screen * Remove unused test * Environment as a parameter * Fleshing out viewmodel * Expand JobStatusResponse fields * Persist Jobs * Fix jobtype * Use WhileSubscribed * MainScreenViewModel (#159) * MainScreenViewModel * Try to optimize * Allow for simultaneous debug and release install * Recomposition fixes * SmartSelfieAuthDialog * PR comments * Perform background job status polling (#166) * refactored main screen setting to add settings tab * create viewmodel in activity and re use in composables * return config as string to make it easy to edit on bottom sheet * updated settings page strings * added smile config bottom sheet * fix lint * archive APKs * fixed smile setup to allow changing config * running lint * Setup Smile Config Runtime [Alternative] (#172) * Allow for dynamic initialization * remove from main screen view model * created root screen and moved theming up to main activity * added a boolean flag do enable/disable dismissing the config bottom sheet * fixed lint * moved theme setup up * added option to dismiss bottom sheet * cleaning up root logic to a viewmodel * fixed build failure --------- Co-authored-by: Vansh Gandhi * added partner notes * PR comments * Update Actions * Rebase updates --------- Co-authored-by: Vansh Gandhi * Bump dev.zacsweers.moshix from 0.23.0 to 0.24.0 (#176) Bumps [dev.zacsweers.moshix](https://github.com/ZacSweers/MoshiX) from 0.23.0 to 0.24.0. - [Release notes](https://github.com/ZacSweers/MoshiX/releases) - [Changelog](https://github.com/ZacSweers/MoshiX/blob/main/CHANGELOG.md) - [Commits](https://github.com/ZacSweers/MoshiX/compare/0.23.0...0.24.0) --- updated-dependencies: - dependency-name: dev.zacsweers.moshix dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump io.sentry:sentry-bom from 6.25.2 to 6.26.0 (#177) Bumps [io.sentry:sentry-bom](https://github.com/getsentry/sentry-java) from 6.25.2 to 6.26.0. - [Release notes](https://github.com/getsentry/sentry-java/releases) - [Changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-java/compare/6.25.2...6.26.0) --- updated-dependencies: - dependency-name: io.sentry:sentry-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump com.google.devtools.ksp from 1.9.0-1.0.11 to 1.9.0-1.0.12 (#175) Bumps [com.google.devtools.ksp](https://github.com/google/ksp) from 1.9.0-1.0.11 to 1.9.0-1.0.12. - [Release notes](https://github.com/google/ksp/releases) - [Commits](https://github.com/google/ksp/compare/1.9.0-1.0.11...1.9.0-1.0.12) --- updated-dependencies: - dependency-name: com.google.devtools.ksp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Expose OkHttp as an `api` dependency (#186) * Prepare for 10.0.0-beta05 release (#174) * Remove Polling (#169) * Add Async Enhanced KYC endpoint (#168) * Add Async Enhanced KYC endpoint * Fix URL * Setting up loading button (#170) * setting up loading button * fixed pr comments * loading button tests * Include ID Info in Document Verification network request (#173) * Include ID Info in network request * Update CHANGELOG * Fix test * Update CHANGELOG --------- Co-authored-by: Juma Allan * Bump coroutines from 1.7.2 to 1.7.3 (#180) * Bump io.sentry:sentry-bom from 6.25.2 to 6.27.0 (#182) * Expose OkHttp as an `api` dependency * Bump AGP to 8.1.0 --------- Co-authored-by: Juma Allan Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Prepare for 10.0.0-beta06 * Update CHANGELOG --------- Signed-off-by: dependabot[bot] Co-authored-by: Juma Allan Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yaml | 13 +- .github/workflows/release_sdk.yaml | 21 +- CHANGELOG.md | 23 ++ gradle.properties | 4 - gradle/libs.versions.toml | 21 +- lib/VERSION | 2 +- lib/lib.gradle.kts | 11 +- .../main/java/com/smileidentity/SmileID.kt | 17 +- .../com/smileidentity/compose/SmileIDExt.kt | 4 +- .../compose/components/LoadingButton.kt | 2 +- .../compose/selfie/SelfieCaptureScreen.kt | 8 +- sample/sample.gradle.kts | 8 +- .../sample/repo/DataStoreRepositoryTest.kt | 2 +- .../java/com/smileidentity/sample/Screen.kt | 6 +- .../sample/SmileIDApplication.kt | 15 +- .../java/com/smileidentity/sample/Utils.kt | 3 + .../sample/activity/MainActivity.kt | 6 +- .../sample/compose/AboutUsScreen.kt | 101 ------ .../sample/compose/MainScreen.kt | 335 +++++++++--------- .../sample/compose/ProductScreens.kt | 4 +- .../sample/compose/ResourcesScreen.kt | 59 +++ .../sample/compose/RootScreen.kt | 97 +++++ .../sample/compose/SettingsScreen.kt | 78 ++++ .../components/SmileConfigModalBottomSheet.kt | 84 +++++ .../sample/repo/DataStoreRepository.kt | 14 +- .../sample/viewmodel/EnhancedKycViewModel.kt | 2 + .../sample/viewmodel/MainScreenViewModel.kt | 7 +- .../sample/viewmodel/RootViewModel.kt | 77 ++++ .../sample/viewmodel/SettingsViewModel.kt | 68 ++++ sample/src/main/res/values/strings.xml | 8 +- 30 files changed, 750 insertions(+), 350 deletions(-) delete mode 100644 sample/src/main/java/com/smileidentity/sample/compose/AboutUsScreen.kt create mode 100644 sample/src/main/java/com/smileidentity/sample/compose/RootScreen.kt create mode 100644 sample/src/main/java/com/smileidentity/sample/compose/SettingsScreen.kt create mode 100644 sample/src/main/java/com/smileidentity/sample/compose/components/SmileConfigModalBottomSheet.kt create mode 100644 sample/src/main/java/com/smileidentity/sample/viewmodel/RootViewModel.kt create mode 100644 sample/src/main/java/com/smileidentity/sample/viewmodel/SettingsViewModel.kt diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a51b6409d..d7c5b11a0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,8 +3,9 @@ name: Lint Build and Test on: pull_request: + merge_group: push: - branches: [ main ] + branches: [ main, develop ] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -21,11 +22,6 @@ jobs: with: distribution: temurin java-version: 17 - - name: Write Smile Config - run: | - echo "$SMILE_CONFIG" > sample/src/main/assets/smile_config.json - env: - SMILE_CONFIG: ${{ secrets.SMILE_CONFIG_PARTNER_2423 }} - name: Calculate Snapshot Version id: version # Read version number from the VERSION file and append -SNAPSHOT if not already present @@ -55,6 +51,11 @@ jobs: with: name: SDK AAR path: lib/build/outputs/aar/lib-debug.aar + - name: Archive Sample App + uses: actions/upload-artifact@v3 + with: + name: Sample App APK + path: sample/build/outputs/apk/debug/sample-debug.apk lint: runs-on: ubuntu-latest diff --git a/.github/workflows/release_sdk.yaml b/.github/workflows/release_sdk.yaml index 76ee3bbc8..2f53d842b 100644 --- a/.github/workflows/release_sdk.yaml +++ b/.github/workflows/release_sdk.yaml @@ -18,7 +18,7 @@ concurrency: jobs: release: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 steps: - uses: actions/checkout@v3 # https://github.com/actions/checkout/issues/766 @@ -29,11 +29,6 @@ jobs: with: distribution: temurin java-version: 17 - - name: Write Smile Config - run: | - echo "$SMILE_CONFIG" > sample/src/main/assets/smile_config.json - env: - SMILE_CONFIG: ${{ secrets.SMILE_CONFIG_PARTNER_2423 }} - name: Decode Keystore id: decode_keystore uses: timheuer/base64-to-file@v1 @@ -91,6 +86,11 @@ jobs: with: name: SDK AAR path: lib/build/outputs/aar/lib-release.aar + - name: Archive Sample App + uses: actions/upload-artifact@v3 + with: + name: Sample App APK + path: sample/build/outputs/apk/release/sample-release.apk - name: Create GitHub Release uses: actions/create-release@v1 id: create_release @@ -109,6 +109,15 @@ jobs: asset_path: lib/build/outputs/aar/lib-release.aar asset_name: com.smileidentity_android-sdk_${{ steps.read_version.outputs.version }}.aar asset_content_type: application/octet-stream + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: sample/build/outputs/apk/release/sample-release.apk + asset_name: sample_${{ steps.read_version.outputs.version }}.apk + asset_content_type: application/vnd.android.package-archive - name: Bump Version if: ${{ github.event.inputs.bump_version == 'true' }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index a3b3d4fb8..a6b865871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 10.0.0-beta07 (unreleased) + +### Added + +### Fixed + +### Changed + +### Removed + +## 10.0.0-beta06 + +### Fixed +- Added OkHttp as an `api` dependency +- Updated `LoadingButton` visibility modifier + +### Changed +- Switch from Java 17 to Java 11 to support Flutter +- Allow passing in a custom `Config` instance to `SmileID.initialize` +- Bump coroutines to 1.7.3 +- Bump Sentry to 6.28.0 +- Bump AndroidX Fragment to 1.6.1 + ## 10.0.0-beta05 ### Added diff --git a/gradle.properties b/gradle.properties index 20b9c08d2..957df09f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,3 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true kotlin.code.style=official android.defaults.buildfeatures.buildconfig=true - -# TODO: Remove once upgraded to AGP 8.1.0 -# Upgrade lint to a newer version to work around https://issuetracker.google.com/issues/185418482. -android.experimental.lint.version=8.1.0-rc01 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a38746419..6616d2ee2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,18 @@ [versions] accompanist-permissions = "0.30.1" -# TODO: Once updgraded to 8.1.0, remove the lint version from gradle.properties -android-gradle-plugin = "8.0.2" +android-gradle-plugin = "8.1.0" androidx-activity = "1.7.2" # TODO: Check if https://android-review.googlesource.com/c/platform/frameworks/support/+/2576871 has # been merged and if so, swap the buttons in ImageCaptureConfirmationDialog androidx-compose-bom = "2023.06.01" androidx-compose-compiler = "1.5.0" androidx-core = "1.10.1" -androidx-fragment = "1.6.0" +androidx-fragment = "1.6.1" androidx-lifecycle = "2.6.1" androidx-navigation = "2.6.0" androidx-test-core = "1.5.0" androidx-test-espresso = "3.5.1" -androidx-test-fragment = "1.6.0" +androidx-test-fragment = "1.6.1" androidx-test-junit = "1.1.5" androidx-test-rules = "1.5.0" camposer = "0.2.2" @@ -25,21 +24,21 @@ datastore = "1.0.0" junit = "4.13.2" kotlin = "1.9.0" kotlin-immutable-collections = "0.3.5" -ksp = "1.9.0-1.0.11" +ksp = "1.9.0-1.0.13" ktlint-plugin = "11.5.0" leakcanary = "2.12" maven-publish = "0.25.3" mockk = "1.13.5" moshi = "1.15.0" -moshix = "0.23.0" +moshix = "0.24.0" moshi-lazy-adapters = "2.2" okhttp = "4.11.0" play-services-mlkit-face-detection = "17.1.0" retrofit = "2.9.0" -sentry = "6.27.0" +sentry = "6.28.0" timber = "5.0.1" truth = "1.1.5" -uiautomator = "2.3.0-alpha03" +uiautomator = "2.3.0-alpha04" [plugins] android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } @@ -91,7 +90,6 @@ mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } moshi-adapters-lazy = { module = "com.serjltt.moshi:moshi-lazy-adapters", version.ref = "moshi-lazy-adapters" } -moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } @@ -99,12 +97,7 @@ play-services-mlkit-face-detection = { module = "com.google.android.gms:play-ser retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } sentry = { module = "io.sentry:sentry" } -sentry-android-core = { module = "io.sentry:sentry-android-core" } -sentry-android-fragment = { module = "io.sentry:sentry-android-fragment" } -sentry-android-okhttp = { module = "io.sentry:sentry-android-okhttp" } -sentry-android-timber = { module = "io.sentry:sentry-android-timber" } sentry-bom = { module = "io.sentry:sentry-bom", version.ref = "sentry" } -sentry-compose-android = { module = "io.sentry:sentry-compose-android" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } truth = { module = "com.google.truth:truth", version.ref = "truth" } uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" } diff --git a/lib/VERSION b/lib/VERSION index aa07b6ccc..7cc472bdb 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -10.0.0-beta05-SNAPSHOT +10.0.0-beta06-SNAPSHOT diff --git a/lib/lib.gradle.kts b/lib/lib.gradle.kts index 244a8fa5b..5d582086d 100644 --- a/lib/lib.gradle.kts +++ b/lib/lib.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) alias(libs.plugins.ktlint) + alias(libs.plugins.ksp) alias(libs.plugins.maven.publish) alias(libs.plugins.moshix) alias(libs.plugins.parcelize) @@ -47,12 +48,12 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + jvmTarget = JavaVersion.VERSION_11.toString() moduleName = "${groupId}_$artifactId" compileOptions { // https://kotlinlang.org/docs/opt-in-requirements.html#module-wide-opt-in @@ -132,6 +133,8 @@ mavenPublishing { } dependencies { + // OkHttp is exposed in public SmileID interface (initialize), hence "api" vs "implementation" + api(libs.okhttp) implementation(libs.retrofit) implementation(libs.retrofit.converter.moshi) implementation(libs.okhttp.logging.interceptor) @@ -141,6 +144,8 @@ dependencies { implementation(libs.moshi.adapters) implementation(libs.moshi.adapters.lazy) + implementation(libs.coroutines.core) + implementation(libs.androidx.core) implementation(libs.androidx.fragment) implementation(libs.androidx.activity.compose) diff --git a/lib/src/main/java/com/smileidentity/SmileID.kt b/lib/src/main/java/com/smileidentity/SmileID.kt index f7cf7a333..e649197dd 100644 --- a/lib/src/main/java/com/smileidentity/SmileID.kt +++ b/lib/src/main/java/com/smileidentity/SmileID.kt @@ -24,13 +24,14 @@ import retrofit2.converter.moshi.MoshiConverterFactory import timber.log.Timber import java.util.concurrent.TimeUnit -@Suppress("unused", "RemoveRedundantQualifierName") +@Suppress("unused") object SmileID { @JvmStatic lateinit var api: SmileIDService internal set val moshi: Moshi = initMoshi() // Initialized immediately so it can be used to parse Config lateinit var config: Config + internal set private lateinit var retrofit: Retrofit // Can't use lateinit on primitives, this default will be overwritten as soon as init is called @@ -44,7 +45,9 @@ object SmileID { /** * Initialize the SDK. This must be called before any other SDK methods. * - * @param context A [Context] instance which will be used to load the config file from assets + * @param context A [Context] instance + * @param config The [Config] to use for the SDK. If not provided, will attempt to load from + * assets (the recommended approach) * @param useSandbox Whether to use the sandbox environment. If false, uses production * @param enableCrashReporting Whether to enable crash reporting for *ONLY* Smile ID related * crashes. This is powered by Sentry, and further details on inner workings can be found in the @@ -55,11 +58,12 @@ object SmileID { @JvmOverloads fun initialize( context: Context, + config: Config = Config.fromAssets(context), useSandbox: Boolean = false, enableCrashReporting: Boolean = true, okHttpClient: OkHttpClient = getOkHttpClientBuilder().build(), ) { - SmileID.config = Config.fromAssets(context) + SmileID.config = config // Enable crash reporting as early as possible (the pre-req is that the config is loaded) if (enableCrashReporting) { val isInDebugMode = context.applicationInfo.flags and FLAG_DEBUGGABLE != 0 @@ -89,7 +93,9 @@ object SmileID { * authToken from [config] need not be used. * * @param apiKey The API Key to use - * @param context A [Context] instance which will be used to load the config file from assets + * @param context A [Context] instance + * @param config The [Config] to use for the SDK. If not provided, will attempt to load from + * assets (the recommended approach) * @param useSandbox Whether to use the sandbox environment. If false, uses production * @param enableCrashReporting Whether to enable crash reporting for *ONLY* Smile ID related * crashes. This is powered by Sentry, and further details on inner workings can be found in the @@ -101,12 +107,13 @@ object SmileID { fun initialize( apiKey: String, context: Context, + config: Config = Config.fromAssets(context), useSandbox: Boolean = false, enableCrashReporting: Boolean = true, okHttpClient: OkHttpClient = getOkHttpClientBuilder().build(), ) { SmileID.apiKey = apiKey - initialize(context, useSandbox, enableCrashReporting, okHttpClient) + initialize(context, config, useSandbox, enableCrashReporting, okHttpClient) } /** diff --git a/lib/src/main/java/com/smileidentity/compose/SmileIDExt.kt b/lib/src/main/java/com/smileidentity/compose/SmileIDExt.kt index 47060f80a..150381538 100644 --- a/lib/src/main/java/com/smileidentity/compose/SmileIDExt.kt +++ b/lib/src/main/java/com/smileidentity/compose/SmileIDExt.kt @@ -136,8 +136,8 @@ fun SmileID.SmartSelfieAuthentication( * generated * @param showAttribution Whether to show the Smile ID attribution or not on the Instructions screen * @param allowGalleryUpload Whether to allow the user to upload images from their gallery or not - * @param showInstructions Whether to deactivate capture screen's instructions for - * Document Verification. + * @param showInstructions Whether to deactivate capture screen's instructions for Document + * Verification (NB! If instructions are disabled, gallery upload won't be possible) * @param colorScheme The color scheme to use for the UI. This is passed in so that we show a Smile * ID branded UI by default, but allow the user to override it if they want. * @param typography The typography to use for the UI. This is passed in so that we show a Smile ID diff --git a/lib/src/main/java/com/smileidentity/compose/components/LoadingButton.kt b/lib/src/main/java/com/smileidentity/compose/components/LoadingButton.kt index 951dc9d5d..8204e8c76 100644 --- a/lib/src/main/java/com/smileidentity/compose/components/LoadingButton.kt +++ b/lib/src/main/java/com/smileidentity/compose/components/LoadingButton.kt @@ -16,7 +16,7 @@ import com.smileidentity.R import com.smileidentity.compose.preview.SmilePreviews @Composable -fun LoadingButton( +internal fun LoadingButton( buttonText: String, modifier: Modifier = Modifier, loading: Boolean = false, diff --git a/lib/src/main/java/com/smileidentity/compose/selfie/SelfieCaptureScreen.kt b/lib/src/main/java/com/smileidentity/compose/selfie/SelfieCaptureScreen.kt index bb377cfc7..eb017a982 100644 --- a/lib/src/main/java/com/smileidentity/compose/selfie/SelfieCaptureScreen.kt +++ b/lib/src/main/java/com/smileidentity/compose/selfie/SelfieCaptureScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -66,6 +67,7 @@ internal fun SelfieCaptureScreen( val cameraState = rememberCameraState() var camSelector by rememberCamSelector(CamSelector.Front) val viewfinderZoom = 1.1f + val faceFillPercent = remember { MAX_FACE_AREA_THRESHOLD * viewfinderZoom * 2 } // Force maximum brightness in order to light up the user's face ForceBrightness() Box(modifier = Modifier.fillMaxSize()) { @@ -89,14 +91,14 @@ internal fun SelfieCaptureScreen( // "out of bounds" content as a fraud prevention technique .scale(viewfinderZoom), ) - val animatedProgress = animateFloatAsState( + val animatedProgress by animateFloatAsState( targetValue = uiState.progress, animationSpec = tween(easing = LinearEasing), label = "selfie_progress", - ).value + ) FaceShapedProgressIndicator( progress = animatedProgress, - faceFillPercent = MAX_FACE_AREA_THRESHOLD * viewfinderZoom * 2, + faceFillPercent = faceFillPercent, modifier = Modifier .fillMaxSize() .testTag("selfie_progress_indicator"), diff --git a/sample/sample.gradle.kts b/sample/sample.gradle.kts index 3e7c44115..a247cb5a8 100644 --- a/sample/sample.gradle.kts +++ b/sample/sample.gradle.kts @@ -18,7 +18,7 @@ android { targetSdk = 34 versionCode = findProperty("VERSION_CODE")?.toString()?.toInt() ?: 1 // Include the SDK version in the app version name - versionName = "1.3_sdk-" + project(":lib").version.toString() + versionName = "1.4_" + project(":lib").version.toString() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -93,10 +93,8 @@ val checkSmileConfigFileTaskName = "checkSmileConfigFile" tasks.register(checkSmileConfigFileTaskName) { doLast { val configFile = file("src/main/assets/smile_config.json") - if (!configFile.exists()) { - throw IllegalArgumentException("Missing smile_config.json file in src/main/assets!") - } - if (configFile.readText().isBlank()) { + // It is okay if the Smile Config doesn't exist -- it will be prompted for upon startup + if (configFile.exists() && configFile.readText().isBlank()) { throw IllegalArgumentException("Empty smile_config.json file in src/main/assets!") } } diff --git a/sample/src/androidTest/java/com/smileidentity/sample/repo/DataStoreRepositoryTest.kt b/sample/src/androidTest/java/com/smileidentity/sample/repo/DataStoreRepositoryTest.kt index acc0ecaab..ff22a9acc 100644 --- a/sample/src/androidTest/java/com/smileidentity/sample/repo/DataStoreRepositoryTest.kt +++ b/sample/src/androidTest/java/com/smileidentity/sample/repo/DataStoreRepositoryTest.kt @@ -53,7 +53,7 @@ class DataStoreRepositoryTest { DataStoreRepository.clearConfig() // then - val actual = DataStoreRepository.getConfig().first() + val actual = DataStoreRepository.getConfigJsonString().first() assertNull(actual) } } diff --git a/sample/src/main/java/com/smileidentity/sample/Screen.kt b/sample/src/main/java/com/smileidentity/sample/Screen.kt index 6bd3f294d..2a12b3fa5 100644 --- a/sample/src/main/java/com/smileidentity/sample/Screen.kt +++ b/sample/src/main/java/com/smileidentity/sample/Screen.kt @@ -75,9 +75,9 @@ enum class BottomNavigationScreen( Filled.Info, Outlined.Info, ), - AboutUs( - "about_us", - R.string.about_us, + Settings( + "settings", + R.string.settings, Filled.Settings, Outlined.Settings, ), diff --git a/sample/src/main/java/com/smileidentity/sample/SmileIDApplication.kt b/sample/src/main/java/com/smileidentity/sample/SmileIDApplication.kt index c7c7a9128..dab15f98e 100644 --- a/sample/src/main/java/com/smileidentity/sample/SmileIDApplication.kt +++ b/sample/src/main/java/com/smileidentity/sample/SmileIDApplication.kt @@ -2,24 +2,19 @@ package com.smileidentity.sample import android.app.Application import android.content.Context -import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.smileidentity.SmileID -import com.smileidentity.SmileID.getOkHttpClientBuilder import com.smileidentity.sample.repo.DataStoreRepository import timber.log.Timber class SmileIDApplication : Application() { override fun onCreate() { super.onCreate() + appContext = this Timber.plant(Timber.DebugTree()) - val chucker = ChuckerInterceptor.Builder(this).build() - SmileID.initialize( - context = this, - useSandbox = true, - enableCrashReporting = !BuildConfig.DEBUG, - okHttpClient = getOkHttpClientBuilder().addInterceptor(chucker).build(), - ) + + // *****Note to Partners***** + // The line below is how you should initialize the SmileID SDK + // SmileID.initialize(this) } companion object { diff --git a/sample/src/main/java/com/smileidentity/sample/Utils.kt b/sample/src/main/java/com/smileidentity/sample/Utils.kt index 20915e5db..7dcc6e3df 100644 --- a/sample/src/main/java/com/smileidentity/sample/Utils.kt +++ b/sample/src/main/java/com/smileidentity/sample/Utils.kt @@ -18,6 +18,9 @@ fun Context.toast(@StringRes message: Int) { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } +// A simple extension function to make boolean checks expressive +fun Boolean.ifTrue(action: () -> T): T? = if (this) action() else null + fun SnackbarHostState.showSnackbar( scope: CoroutineScope, message: String, diff --git a/sample/src/main/java/com/smileidentity/sample/activity/MainActivity.kt b/sample/src/main/java/com/smileidentity/sample/activity/MainActivity.kt index f55b644dd..d1c3a7b9a 100644 --- a/sample/src/main/java/com/smileidentity/sample/activity/MainActivity.kt +++ b/sample/src/main/java/com/smileidentity/sample/activity/MainActivity.kt @@ -4,12 +4,14 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.view.WindowCompat -import com.smileidentity.sample.compose.MainScreen +import com.smileidentity.sample.compose.RootScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) - setContent { MainScreen() } + setContent { + RootScreen() + } } } diff --git a/sample/src/main/java/com/smileidentity/sample/compose/AboutUsScreen.kt b/sample/src/main/java/com/smileidentity/sample/compose/AboutUsScreen.kt deleted file mode 100644 index beca71f43..000000000 --- a/sample/src/main/java/com/smileidentity/sample/compose/AboutUsScreen.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.smileidentity.sample.compose - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowForward -import androidx.compose.material.icons.filled.Email -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Star -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.smileidentity.sample.R - -@Composable -fun AboutUsScreen(modifier: Modifier = Modifier) { - var shouldShowWhoWeAreDialog by rememberSaveable { mutableStateOf(false) } - val uriHandler = LocalUriHandler.current - val abouts = listOf( - Triple(R.string.about_us_who_we_are, Icons.Default.Info) { - shouldShowWhoWeAreDialog = true - }, - Triple(R.string.about_us_visit_our_website, Icons.Default.Star) { - uriHandler.openUri("https://smileidentity.com") - }, - Triple(R.string.about_us_contact_support, Icons.Default.Email) { - uriHandler.openUri("https://smileidentity.com/contact-us") - }, - ) - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - ) { - abouts.forEach { - ListItem( - headlineContent = { Text(stringResource(it.first)) }, - leadingContent = { Icon(it.second, null) }, - trailingContent = { Icon(Icons.Default.ArrowForward, null) }, - modifier = Modifier.clickable { it.third() }, - ) - Divider() - } - } - - if (shouldShowWhoWeAreDialog) { - WhoWeAreDialog { shouldShowWhoWeAreDialog = false } - } -} - -@Composable -fun WhoWeAreDialog(onDialogClose: () -> Unit = {}) { - AlertDialog( - onDismissRequest = onDialogClose, - title = { Text(text = stringResource(R.string.about_us_who_we_are)) }, - confirmButton = { - Button(onClick = onDialogClose) { Text(stringResource(R.string.okay)) } - }, - text = { - Text( - text = stringResource(R.string.about_us_who_we_are_content), - style = MaterialTheme.typography.bodyMedium, - ) - }, - ) -} - -@Preview -@Composable -private fun PreviewAboutUsScreen() { - SmileIDTheme { - Surface(color = MaterialTheme.colorScheme.background) { - AboutUsScreen() - } - } -} - -@Preview -@Composable -private fun PreviewWhoWeAreDialog() { - SmileIDTheme { - WhoWeAreDialog() - } -} diff --git a/sample/src/main/java/com/smileidentity/sample/compose/MainScreen.kt b/sample/src/main/java/com/smileidentity/sample/compose/MainScreen.kt index 87eb85391..1be09f96d 100644 --- a/sample/src/main/java/com/smileidentity/sample/compose/MainScreen.kt +++ b/sample/src/main/java/com/smileidentity/sample/compose/MainScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -43,7 +42,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -74,9 +72,9 @@ import kotlinx.coroutines.launch import java.net.URL @OptIn(ExperimentalLayoutApi::class) -@Preview @Composable fun MainScreen( + modifier: Modifier = Modifier, viewModel: MainScreenViewModel = viewModel( factory = viewModelFactory { MainScreenViewModel() }, ), @@ -105,180 +103,173 @@ fun MainScreen( } } } - - SmileIDTheme { - Surface { - Scaffold( - snackbarHost = { Snackbar() }, - topBar = { - TopBar( - showUpButton = showUpButton, - onNavigateUp = { navController.navigateUp() }, - isJobsScreenSelected = bottomNavSelection == BottomNavigationScreen.Jobs, + Scaffold( + modifier = modifier, + snackbarHost = { Snackbar() }, + topBar = { + TopBar( + showUpButton = showUpButton, + onNavigateUp = navController::navigateUp, + isJobsScreenSelected = bottomNavSelection == BottomNavigationScreen.Jobs, + ) + }, + bottomBar = { + // Don't show bottom bar when navigating to any product screens + val showBottomBar by remember(currentRoute) { + derivedStateOf { + bottomNavItems.any { it.route.contains(currentRoute?.destination?.route ?: "") } + } + } + if (showBottomBar) { + BottomBar( + bottomNavItems = bottomNavItems, + bottomNavSelection = bottomNavSelection, + pendingJobCount = uiState.pendingJobCount, + ) { + navController.navigate(it.route) { + popUpTo(BottomNavigationScreen.Home.route) + launchSingleTop = true + } + } + } + }, + content = { + NavHost( + navController, + startScreen.route, + Modifier + .padding(it) + .consumeWindowInsets(it), + ) { + composable(BottomNavigationScreen.Home.route) { + LaunchedEffect(Unit) { viewModel.onHomeSelected() } + ProductSelectionScreen( + onProductSelected = { navController.navigate(it.route) }, ) - }, - bottomBar = { - // Don't show bottom bar when navigating to any product screens - val showBottomBar by remember(currentRoute) { - derivedStateOf { - bottomNavItems.any { - it.route.contains( - currentRoute?.destination?.route ?: "", - ) - } - } + } + composable(BottomNavigationScreen.Jobs.route) { + LaunchedEffect(Unit) { viewModel.onJobsSelected() } + OrchestratedJobsScreen(uiState.isProduction) + } + composable(BottomNavigationScreen.Resources.route) { + LaunchedEffect(Unit) { viewModel.onResourcesSelected() } + ResourcesScreen() + } + composable(BottomNavigationScreen.Settings.route) { + LaunchedEffect(Unit) { viewModel.onSettingsSelected() } + SettingsScreen() + } + composable(ProductScreen.SmartSelfieEnrollment.route) { + LaunchedEffect(Unit) { viewModel.onSmartSelfieEnrollmentSelected() } + val userId = rememberSaveable { randomUserId() } + val jobId = rememberSaveable { randomJobId() } + SmileID.SmartSelfieEnrollment( + userId = userId, + jobId = jobId, + allowAgentMode = true, + showInstructions = true, + ) { result -> + viewModel.onSmartSelfieEnrollmentResult(userId, jobId, result) + navController.popBackStack() } - if (showBottomBar) { - BottomBar( - bottomNavItems = bottomNavItems, - bottomNavSelection = bottomNavSelection, - pendingJobCount = uiState.pendingJobCount, - ) { - navController.navigate(it.route) { - popUpTo(BottomNavigationScreen.Home.route) - launchSingleTop = true - } - } + } + composable(ProductScreen.SmartSelfieAuthentication.route) { + LaunchedEffect(Unit) { viewModel.onSmartSelfieAuthenticationSelected() } + SmartSelfieAuthenticationUserIdInputDialog( + onDismiss = navController::popBackStack, + onConfirm = { userId -> + navController.navigate( + "${ProductScreen.SmartSelfieAuthentication.route}/$userId", + ) { popUpTo(BottomNavigationScreen.Home.route) } + }, + ) + } + composable(ProductScreen.SmartSelfieAuthentication.route + "/{userId}") { + LaunchedEffect(Unit) { viewModel.onSmartSelfieAuthenticationSelected() } + val userId = rememberSaveable { it.arguments?.getString("userId")!! } + val jobId = rememberSaveable { randomJobId() } + SmileID.SmartSelfieAuthentication( + userId = userId, + jobId = jobId, + allowAgentMode = true, + ) { result -> + viewModel.onSmartSelfieAuthenticationResult(userId, jobId, result) + navController.popBackStack() } - }, - content = { - NavHost( - navController, - startScreen.route, - Modifier - .padding(it) - .consumeWindowInsets(it), - ) { - composable(BottomNavigationScreen.Home.route) { - LaunchedEffect(Unit) { viewModel.onHomeSelected() } - ProductSelectionScreen { navController.navigate(it.route) } - } - composable(BottomNavigationScreen.Jobs.route) { - LaunchedEffect(Unit) { viewModel.onJobsSelected() } - OrchestratedJobsScreen(uiState.isProduction) - } - composable(BottomNavigationScreen.Resources.route) { - LaunchedEffect(Unit) { viewModel.onResourcesSelected() } - ResourcesScreen() - } - composable(BottomNavigationScreen.AboutUs.route) { - LaunchedEffect(Unit) { viewModel.onAboutUsSelected() } - AboutUsScreen() - } - composable(ProductScreen.SmartSelfieEnrollment.route) { - LaunchedEffect(Unit) { viewModel.onSmartSelfieEnrollmentSelected() } - val userId = rememberSaveable { randomUserId() } - val jobId = rememberSaveable { randomJobId() } - SmileID.SmartSelfieEnrollment( - userId = userId, - jobId = jobId, - allowAgentMode = true, - showInstructions = true, - ) { result -> - viewModel.onSmartSelfieEnrollmentResult(userId, jobId, result) - navController.popBackStack() - } - } - composable(ProductScreen.SmartSelfieAuthentication.route) { - LaunchedEffect(Unit) { viewModel.onSmartSelfieAuthenticationSelected() } - SmartSelfieAuthenticationUserIdInputDialog( - onDismiss = navController::popBackStack, - onConfirm = { userId -> - navController.navigate( - "${ProductScreen.SmartSelfieAuthentication.route}/$userId", - ) { popUpTo(BottomNavigationScreen.Home.route) } - }, - ) - } - composable(ProductScreen.SmartSelfieAuthentication.route + "/{userId}") { - LaunchedEffect(Unit) { viewModel.onSmartSelfieAuthenticationSelected() } - val userId = rememberSaveable { it.arguments?.getString("userId")!! } - val jobId = rememberSaveable { randomJobId() } - SmileID.SmartSelfieAuthentication( - userId = userId, - jobId = jobId, - allowAgentMode = true, - ) { result -> - viewModel.onSmartSelfieAuthenticationResult(userId, jobId, result) - navController.popBackStack() - } - } - composable(ProductScreen.EnhancedKyc.route) { - LaunchedEffect(Unit) { viewModel.onEnhancedKycSelected() } - OrchestratedEnhancedKycScreen { result -> - viewModel.onEnhancedKycResult(result) - navController.popBackStack() - } - } - composable(ProductScreen.BiometricKyc.route) { - LaunchedEffect(Unit) { viewModel.onBiometricKycSelected() } - var idInfo: IdInfo? by remember { mutableStateOf(null) } - if (idInfo == null) { - IdTypeSelectorAndFieldInputScreen( - jobType = JobType.BiometricKyc, - onResult = { idInfo = it }, - ) - } - idInfo?.let { - val url = remember { - URL("https://usesmileid.com/privacy-policy") - } - val userId = rememberSaveable { randomUserId() } - val jobId = rememberSaveable { randomJobId() } - SmileID.BiometricKYC( - idInfo = it, - userId = userId, - jobId = jobId, - partnerIcon = painterResource( - id = com.smileidentity.R.drawable.si_logo_with_text, - ), - partnerName = "Smile ID", - productName = it.idType, - partnerPrivacyPolicy = url, - ) { result -> - viewModel.onBiometricKycResult(userId, jobId, result) - navController.popBackStack() - } - } - } - composable(ProductScreen.DocumentVerification.route) { - LaunchedEffect(Unit) { viewModel.onDocumentVerificationSelected() } - DocumentVerificationIdTypeSelector { country, idType -> - navController.navigate( - "${ProductScreen.DocumentVerification.route}/$country/$idType", - ) { popUpTo(ProductScreen.DocumentVerification.route) } - } - } - composable( - ProductScreen.DocumentVerification.route + "/{countryCode}/{idType}", - ) { - LaunchedEffect(Unit) { viewModel.onDocumentVerificationSelected() } - val userId = rememberSaveable { randomUserId() } - val jobId = rememberSaveable { randomJobId() } - val documentType = remember(it) { - Document( - it.arguments?.getString("countryCode")!!, - it.arguments?.getString("idType")!!, - ) - } - SmileID.DocumentVerification( - userId = userId, - jobId = jobId, - idType = documentType, - showInstructions = true, - ) { result -> - viewModel.onDocumentVerificationResult(userId, jobId, result) - navController.popBackStack( - route = BottomNavigationScreen.Home.route, - inclusive = false, - ) - } + } + composable(ProductScreen.EnhancedKyc.route) { + LaunchedEffect(Unit) { viewModel.onEnhancedKycSelected() } + OrchestratedEnhancedKycScreen { result -> + viewModel.onEnhancedKycResult(result) + navController.popBackStack() + } + } + composable(ProductScreen.BiometricKyc.route) { + LaunchedEffect(Unit) { viewModel.onBiometricKycSelected() } + var idInfo: IdInfo? by remember { mutableStateOf(null) } + if (idInfo == null) { + IdTypeSelectorAndFieldInputScreen( + jobType = JobType.BiometricKyc, + onResult = { idInfo = it }, + ) + } + idInfo?.let { + val url = remember { URL("https://usesmileid.com/privacy-policy") } + val userId = rememberSaveable { randomUserId() } + val jobId = rememberSaveable { randomJobId() } + SmileID.BiometricKYC( + idInfo = it, + userId = userId, + jobId = jobId, + partnerIcon = painterResource( + id = com.smileidentity.R.drawable.si_logo_with_text, + ), + partnerName = "Smile ID", + productName = it.idType, + partnerPrivacyPolicy = url, + ) { result -> + viewModel.onBiometricKycResult(userId, jobId, result) + navController.popBackStack() } } - }, - ) - } - } + } + composable(ProductScreen.DocumentVerification.route) { + LaunchedEffect(Unit) { viewModel.onDocumentVerificationSelected() } + DocumentVerificationIdTypeSelector { country, idType -> + navController.navigate( + "${ProductScreen.DocumentVerification.route}/$country/$idType", + ) { popUpTo(ProductScreen.DocumentVerification.route) } + } + } + composable( + ProductScreen.DocumentVerification.route + "/{countryCode}/{idType}", + ) { + LaunchedEffect(Unit) { viewModel.onDocumentVerificationSelected() } + val userId = rememberSaveable { randomUserId() } + val jobId = rememberSaveable { randomJobId() } + val documentType = remember(it) { + Document( + it.arguments?.getString("countryCode")!!, + it.arguments?.getString("idType")!!, + ) + } + SmileID.DocumentVerification( + userId = userId, + jobId = jobId, + idType = documentType, + showInstructions = true, + allowGalleryUpload = true, + ) { result -> + viewModel.onDocumentVerificationResult(userId, jobId, result) + navController.popBackStack( + route = BottomNavigationScreen.Home.route, + inclusive = false, + ) + } + } + } + }, + ) } @OptIn(ExperimentalMaterial3Api::class) diff --git a/sample/src/main/java/com/smileidentity/sample/compose/ProductScreens.kt b/sample/src/main/java/com/smileidentity/sample/compose/ProductScreens.kt index e54eda73e..f2915d401 100644 --- a/sample/src/main/java/com/smileidentity/sample/compose/ProductScreens.kt +++ b/sample/src/main/java/com/smileidentity/sample/compose/ProductScreens.kt @@ -43,8 +43,8 @@ import com.smileidentity.sample.Screen @Composable fun ProductSelectionScreen( - modifier: Modifier = Modifier, onProductSelected: (Screen) -> Unit, + modifier: Modifier = Modifier, ) { val density = LocalDensity.current var desiredItemMinHeight by remember { mutableStateOf(0.dp) } @@ -131,7 +131,7 @@ private fun ProductSelectionScreenPreview() { SmileID.initialize(LocalContext.current, enableCrashReporting = false) SmileIDTheme { Surface(color = MaterialTheme.colorScheme.background) { - ProductSelectionScreen { } + ProductSelectionScreen(onProductSelected = {}) } } } diff --git a/sample/src/main/java/com/smileidentity/sample/compose/ResourcesScreen.kt b/sample/src/main/java/com/smileidentity/sample/compose/ResourcesScreen.kt index 8f6067ae1..965dca090 100644 --- a/sample/src/main/java/com/smileidentity/sample/compose/ResourcesScreen.kt +++ b/sample/src/main/java/com/smileidentity/sample/compose/ResourcesScreen.kt @@ -7,12 +7,22 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource @@ -23,6 +33,7 @@ import com.smileidentity.sample.R fun ResourcesScreen( modifier: Modifier = Modifier, ) { + var shouldShowWhoWeAreDialog by rememberSaveable { mutableStateOf(false) } val uriHandler = LocalUriHandler.current val resources = listOf( Triple( @@ -42,6 +53,17 @@ fun ResourcesScreen( stringResource(R.string.resources_supported_types_subtitle), ) { uriHandler.openUri("https://docs.usesmileid.com/supported-id-types") }, ) + val abouts = listOf( + Triple(R.string.about_us_who_we_are, Icons.Default.Info) { + shouldShowWhoWeAreDialog = true + }, + Triple(R.string.about_us_visit_our_website, Icons.Default.Star) { + uriHandler.openUri("https://usesmileid.com") + }, + Triple(R.string.about_us_contact_support, Icons.Default.Email) { + uriHandler.openUri("https://usesmileid.com/contact-us") + }, + ) Column( modifier = modifier .fillMaxSize() @@ -56,6 +78,43 @@ fun ResourcesScreen( ) Divider() } + abouts.forEach { + ListItem( + headlineContent = { Text(stringResource(it.first)) }, + leadingContent = { Icon(it.second, null) }, + trailingContent = { Icon(Icons.Default.ArrowForward, null) }, + modifier = Modifier.clickable { it.third() }, + ) + Divider() + } + } + if (shouldShowWhoWeAreDialog) { + WhoWeAreDialog { shouldShowWhoWeAreDialog = false } + } +} + +@Composable +fun WhoWeAreDialog(onDialogClose: () -> Unit = {}) { + AlertDialog( + onDismissRequest = onDialogClose, + title = { Text(text = stringResource(R.string.about_us_who_we_are)) }, + confirmButton = { + Button(onClick = onDialogClose) { Text(stringResource(R.string.okay)) } + }, + text = { + Text( + text = stringResource(R.string.about_us_who_we_are_content), + style = MaterialTheme.typography.bodyMedium, + ) + }, + ) +} + +@Preview +@Composable +private fun WhoWeAreDialogPreview() { + SmileIDTheme { + WhoWeAreDialog() } } diff --git a/sample/src/main/java/com/smileidentity/sample/compose/RootScreen.kt b/sample/src/main/java/com/smileidentity/sample/compose/RootScreen.kt new file mode 100644 index 000000000..e7fb25d6b --- /dev/null +++ b/sample/src/main/java/com/smileidentity/sample/compose/RootScreen.kt @@ -0,0 +1,97 @@ +package com.smileidentity.sample.compose + +import android.content.Context +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.chuckerteam.chucker.api.ChuckerInterceptor +import com.smileidentity.SmileID +import com.smileidentity.sample.BuildConfig +import com.smileidentity.sample.SmileIDApplication +import com.smileidentity.sample.compose.components.SmileConfigModalBottomSheet +import com.smileidentity.sample.viewmodel.RootViewModel +import com.smileidentity.viewmodel.viewModelFactory +import timber.log.Timber + +/** + * *****Note to Partners***** + * + * To enable runtime switching of the Smile Config, it is essential to have the RootScreen. + * For instructions on initializing the SDK, please refer to [SmileIDApplication]. + */ +@Composable +fun RootScreen( + modifier: Modifier = Modifier, + viewModel: RootViewModel = viewModel( + factory = viewModelFactory { RootViewModel() }, + ), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val runtimeConfig by viewModel.runtimeConfig.collectAsStateWithLifecycle() + var initialized by remember { mutableStateOf(false) } + val context = LocalContext.current + val client = remember { + SmileID.getOkHttpClientBuilder() + .addInterceptor(ChuckerInterceptor.Builder(context).build()) + .build() + } + SmileIDTheme { + Surface(modifier = modifier) { + if (runtimeConfig != null) { + // If a config has been set at runtime, it takes first priority + LaunchedEffect(runtimeConfig) { + initialized = false + SmileID.initialize( + context = context, + config = runtimeConfig!!, + useSandbox = true, + enableCrashReporting = !BuildConfig.DEBUG, + okHttpClient = client, + ) + initialized = true + } + } else if (context.isConfigDefineInAssets()) { + // Otherwise, fallback to the config defined in assets + LaunchedEffect(Unit) { + initialized = false + SmileID.initialize( + context = context, + useSandbox = true, + enableCrashReporting = !BuildConfig.DEBUG, + okHttpClient = client, + ) + initialized = true + } + } else { + // Otherwise, ask the user to input a config. Once input, the runtimeConfig will + // be updated and the LaunchedEffect above will be triggered. + SmileConfigModalBottomSheet( + onSaveSmileConfig = viewModel::updateSmileConfig, + onDismiss = { Timber.v("onDismiss") }, + errorMessage = uiState.smileConfigError, + hint = uiState.smileConfigHint, + dismissable = false, + ) + } + + key(runtimeConfig) { + if (initialized) { + MainScreen() + } + } + } + } +} + +private fun Context.isConfigDefineInAssets(): Boolean { + return assets.list("")?.contains("smile_config.json") ?: false +} diff --git a/sample/src/main/java/com/smileidentity/sample/compose/SettingsScreen.kt b/sample/src/main/java/com/smileidentity/sample/compose/SettingsScreen.kt new file mode 100644 index 000000000..4626b7f3c --- /dev/null +++ b/sample/src/main/java/com/smileidentity/sample/compose/SettingsScreen.kt @@ -0,0 +1,78 @@ +package com.smileidentity.sample.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.smileidentity.sample.R +import com.smileidentity.sample.compose.components.SmileConfigModalBottomSheet +import com.smileidentity.sample.viewmodel.SettingsViewModel +import com.smileidentity.viewmodel.viewModelFactory + +@Composable +fun SettingsScreen( + modifier: Modifier = Modifier, + viewModel: SettingsViewModel = viewModel( + factory = viewModelFactory { SettingsViewModel() }, + ), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val settings = listOf( + Triple( + R.string.settings_update_smile_config, + Icons.Default.Settings, + viewModel::showSmileConfigInput, + ), + ) + + if (uiState.showSmileConfigBottomSheet) { + SmileConfigModalBottomSheet( + onSaveSmileConfig = viewModel::updateSmileConfig, + onDismiss = viewModel::hideSmileConfigInput, + hint = uiState.smileConfigHint, + errorMessage = uiState.smileConfigError, + ) + } + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + settings.forEach { + ListItem( + headlineContent = { Text(stringResource(it.first)) }, + leadingContent = { Icon(it.second, null) }, + trailingContent = { Icon(Icons.Default.ArrowForward, null) }, + modifier = Modifier.clickable { it.third() }, + ) + Divider() + } + } +} + +@Preview +@Composable +private fun SettingsScreenPreview() { + SmileIDTheme { + Surface(color = MaterialTheme.colorScheme.background) { + SettingsScreen() + } + } +} diff --git a/sample/src/main/java/com/smileidentity/sample/compose/components/SmileConfigModalBottomSheet.kt b/sample/src/main/java/com/smileidentity/sample/compose/components/SmileConfigModalBottomSheet.kt new file mode 100644 index 000000000..1389fdd48 --- /dev/null +++ b/sample/src/main/java/com/smileidentity/sample/compose/components/SmileConfigModalBottomSheet.kt @@ -0,0 +1,84 @@ +package com.smileidentity.sample.compose.components + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.smileidentity.sample.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SmileConfigModalBottomSheet( + onSaveSmileConfig: (updatedConfig: String) -> Unit, + onDismiss: () -> Unit, + hint: String, + modifier: Modifier = Modifier, + @StringRes errorMessage: Int? = null, + dismissable: Boolean = true, +) { + var configInput by remember { mutableStateOf("") } + + ModalBottomSheet( + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { dismissable }, + ), + onDismissRequest = onDismiss, + modifier = modifier, + ) { + Column( + Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth(), + value = configInput, + onValueChange = { configInput = it }, + placeholder = { Text(hint, style = MaterialTheme.typography.bodySmall) }, + isError = errorMessage != null, + supportingText = { + if (errorMessage != null) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = errorMessage), + ) + } + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + minLines = 8, + maxLines = 12, + textStyle = MaterialTheme.typography.bodySmall, + ) + Spacer(modifier = Modifier.size(16.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { onSaveSmileConfig(configInput) }, + ) { + Text(stringResource(id = R.string.settings_update_smile_config)) + } + } + } +} diff --git a/sample/src/main/java/com/smileidentity/sample/repo/DataStoreRepository.kt b/sample/src/main/java/com/smileidentity/sample/repo/DataStoreRepository.kt index 79370b43f..a1e48c673 100644 --- a/sample/src/main/java/com/smileidentity/sample/repo/DataStoreRepository.kt +++ b/sample/src/main/java/com/smileidentity/sample/repo/DataStoreRepository.kt @@ -71,10 +71,18 @@ object DataStoreRepository { } /** - * Get the Smile [Config] from [mainDataStore], if one has been manually set. (It may not be set if - * the sample app is utilizing the assets/smile_config.json file instead). + * Get the Smile Config JSON representation from [mainDataStore], if one has been manually set. + * (It may not be set if the sample app is utilizing the assets/smile_config.json file instead). * - * If no config is set, this returns null + * If no config is set, this emits null + */ + fun getConfigJsonString(): Flow = mainDataStore.data.map { it[Keys.config] } + + /** + * Get the Smile [Config] from [mainDataStore], if one has been manually set. (It may not be set + * if the sample app is utilizing the assets/smile_config.json file instead). + * + * If no config is set, this emits null */ fun getConfig(): Flow = mainDataStore.data.map { it[Keys.config]?.let { SmileID.moshi.adapter(Config::class.java).fromJson(it) } diff --git a/sample/src/main/java/com/smileidentity/sample/viewmodel/EnhancedKycViewModel.kt b/sample/src/main/java/com/smileidentity/sample/viewmodel/EnhancedKycViewModel.kt index 8d52d0cd1..fce327446 100644 --- a/sample/src/main/java/com/smileidentity/sample/viewmodel/EnhancedKycViewModel.kt +++ b/sample/src/main/java/com/smileidentity/sample/viewmodel/EnhancedKycViewModel.kt @@ -12,6 +12,7 @@ import com.smileidentity.results.EnhancedKycResult import com.smileidentity.results.SmileIDCallback import com.smileidentity.results.SmileIDResult import com.smileidentity.util.getExceptionHandler +import com.smileidentity.util.randomJobId import com.smileidentity.util.randomUserId import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -48,6 +49,7 @@ class EnhancedKycViewModel : ViewModel() { jobType = JobType.EnhancedKyc, enrollment = false, userId = randomUserId(), + jobId = randomJobId(), ) val authResponse = SmileID.api.authenticate(authRequest) val enhancedKycRequest = EnhancedKycRequest( diff --git a/sample/src/main/java/com/smileidentity/sample/viewmodel/MainScreenViewModel.kt b/sample/src/main/java/com/smileidentity/sample/viewmodel/MainScreenViewModel.kt index 1993b8081..033c5e2e1 100644 --- a/sample/src/main/java/com/smileidentity/sample/viewmodel/MainScreenViewModel.kt +++ b/sample/src/main/java/com/smileidentity/sample/viewmodel/MainScreenViewModel.kt @@ -52,6 +52,7 @@ data class MainScreenUiState( val snackbarMessage: SnackbarMessage? = null, val bottomNavSelection: BottomNavigationScreen = startScreen, val pendingJobCount: Int = 0, + val showSmileConfigBottomSheet: Boolean = false, val clipboardText: AnnotatedString? = null, ) { @StringRes @@ -198,11 +199,11 @@ class MainScreenViewModel : ViewModel() { } } - fun onAboutUsSelected() { + fun onSettingsSelected() { _uiState.update { it.copy( - appBarTitle = BottomNavigationScreen.AboutUs.label, - bottomNavSelection = BottomNavigationScreen.AboutUs, + appBarTitle = BottomNavigationScreen.Settings.label, + bottomNavSelection = BottomNavigationScreen.Settings, ) } } diff --git a/sample/src/main/java/com/smileidentity/sample/viewmodel/RootViewModel.kt b/sample/src/main/java/com/smileidentity/sample/viewmodel/RootViewModel.kt new file mode 100644 index 000000000..f32a3b01a --- /dev/null +++ b/sample/src/main/java/com/smileidentity/sample/viewmodel/RootViewModel.kt @@ -0,0 +1,77 @@ +package com.smileidentity.sample.viewmodel + +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.smileidentity.SmileID +import com.smileidentity.models.Config +import com.smileidentity.sample.R +import com.smileidentity.sample.SmileIDApplication +import com.smileidentity.sample.repo.DataStoreRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +const val SMILE_CONFIG_DEFAULT_HINT = "Paste your Smile Config from the Portal here" + +/** + * *****Note to Partners***** + * + * To enable runtime switching of the Smile Config, it is essential to have the RootViewModel. + * For instructions on initializing the SDK, please refer to [SmileIDApplication]. + */ +data class RootUiState( + val showSmileConfigBottomSheet: Boolean = false, + val smileConfigHint: String = SMILE_CONFIG_DEFAULT_HINT, + @StringRes val smileConfigError: Int? = null, +) + +class RootViewModel : ViewModel() { + private val _uiState = MutableStateFlow(RootUiState()) + val uiState = _uiState.asStateFlow() + + val runtimeConfig = DataStoreRepository.getConfig() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) + + private val configAdapter = SmileID.moshi.adapter(Config::class.java) + private val currentConfig = DataStoreRepository.getConfigJsonString() + .map { it ?: SMILE_CONFIG_DEFAULT_HINT } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = "", + ) + + init { + viewModelScope.launch { + currentConfig.collect { config -> + _uiState.update { it.copy(smileConfigHint = config) } + } + } + } + + fun updateSmileConfig(updatedConfig: String) { + try { + val config = configAdapter.fromJson(updatedConfig) + if (config != null) { + _uiState.update { it.copy(smileConfigError = null) } + viewModelScope.launch { + DataStoreRepository.setConfig(config) + _uiState.update { it.copy(showSmileConfigBottomSheet = false) } + } + } else { + _uiState.update { it.copy(smileConfigError = R.string.settings_smile_config_error) } + } + } catch (e: Exception) { + _uiState.update { it.copy(smileConfigError = R.string.settings_smile_config_error) } + } + } +} diff --git a/sample/src/main/java/com/smileidentity/sample/viewmodel/SettingsViewModel.kt b/sample/src/main/java/com/smileidentity/sample/viewmodel/SettingsViewModel.kt new file mode 100644 index 000000000..0645103f9 --- /dev/null +++ b/sample/src/main/java/com/smileidentity/sample/viewmodel/SettingsViewModel.kt @@ -0,0 +1,68 @@ +package com.smileidentity.sample.viewmodel + +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.smileidentity.SmileID +import com.smileidentity.models.Config +import com.smileidentity.sample.R +import com.smileidentity.sample.repo.DataStoreRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class SettingsUiState( + val showSmileConfigBottomSheet: Boolean = false, + val smileConfigHint: String = SMILE_CONFIG_DEFAULT_HINT, + @StringRes val smileConfigError: Int? = null, +) + +class SettingsViewModel : ViewModel() { + private val _uiState = MutableStateFlow(SettingsUiState()) + val uiState = _uiState.asStateFlow() + private val configAdapter = SmileID.moshi.adapter(Config::class.java) + private val currentConfig = DataStoreRepository.getConfigJsonString() + .map { it ?: SMILE_CONFIG_DEFAULT_HINT } + .stateIn( + scope = viewModelScope, + started = WhileSubscribed(), + initialValue = "", + ) + + init { + viewModelScope.launch { + currentConfig.collect { config -> + _uiState.update { it.copy(smileConfigHint = config) } + } + } + } + + fun showSmileConfigInput() { + _uiState.update { it.copy(showSmileConfigBottomSheet = true) } + } + + fun hideSmileConfigInput() { + _uiState.update { it.copy(showSmileConfigBottomSheet = false) } + } + + fun updateSmileConfig(updatedConfig: String) { + try { + val config = configAdapter.fromJson(updatedConfig) + if (config != null) { + viewModelScope.launch { + DataStoreRepository.setConfig(config) + _uiState.update { it.copy(showSmileConfigBottomSheet = false) } + } + _uiState.update { it.copy(smileConfigError = null) } + } else { + _uiState.update { it.copy(smileConfigError = R.string.settings_smile_config_error) } + } + } catch (e: Exception) { + _uiState.update { it.copy(smileConfigError = R.string.settings_smile_config_error) } + } + } +} diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index dd33fe15a..02afa7fef 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -34,14 +34,16 @@ Explore frequently asked questions Supported ID types and documents See our coverage range across the continent - - - About Us Who we are Smile ID products are tools for developers to perform real-time digital KYC, identity verification, user onboarding, and user authentication across Africa Visit our website Contact support + + Settings + Update Smile Config + There is an error in your Smile Config + User ID Enter User ID