diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b627e39c..c310eed6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -157,6 +157,7 @@ jobs: ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS: ${{ secrets.UPLOAD_KEY_ALIAS }} ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS_PASSWORD: ${{ secrets.UPLOAD_KEY_ALIAS_PASSWORD }} ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} + ORG_GRADLE_PROJECT_ZCASH_FLEXA_KEY: ${{ secrets.FLEXA_PUBLISHABLE_KEY }} run: | ./gradlew :app:publishToGooglePlay - name: Collect Artifacts diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d3092f04..9c2692dc 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -307,6 +307,7 @@ jobs: # Because Fulladle doesn't allow Test Orchestrator to be enabled/disabled for a specific submodule, it must be enabled for all modules ORG_GRADLE_PROJECT_IS_USE_TEST_ORCHESTRATOR: true ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} + ORG_GRADLE_PROJECT_ZCASH_FLEXA_KEY: ${{ secrets.FLEXA_PUBLISHABLE_KEY }} run: | ./gradlew runFlank - name: Collect Artifacts @@ -355,6 +356,7 @@ jobs: ORG_GRADLE_PROJECT_ZCASH_EMULATOR_WTF_API_KEY: ${{ secrets.EMULATOR_WTF_API_KEY }} ORG_GRADLE_PROJECT_IS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED: true ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} + ORG_GRADLE_PROJECT_ZCASH_FLEXA_KEY: ${{ secrets.FLEXA_PUBLISHABLE_KEY }} run: | ./gradlew testDebugWithEmulatorWtf :ui-integration-test:testZcashmainnetDebugWithEmulatorWtf - name: Collect Artifacts @@ -403,6 +405,7 @@ jobs: ORG_GRADLE_PROJECT_ZCASH_EMULATOR_WTF_API_KEY: ${{ secrets.EMULATOR_WTF_API_KEY }} ORG_GRADLE_PROJECT_IS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED: false ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} + ORG_GRADLE_PROJECT_ZCASH_FLEXA_KEY: ${{ secrets.FLEXA_PUBLISHABLE_KEY }} run: | ./gradlew :app:testZcashmainnetDebugWithEmulatorWtf :ui-screenshot-test:testZcashmainnetDebugWithEmulatorWtf - name: Collect Artifacts @@ -462,6 +465,7 @@ jobs: ORG_GRADLE_PROJECT_ZCASH_SUPPORT_EMAIL_ADDRESS: ${{ vars.SUPPORT_EMAIL_ADDRESS }} ORG_GRADLE_PROJECT_IS_CRASH_ON_STRICT_MODE_VIOLATION: true ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} + ORG_GRADLE_PROJECT_ZCASH_FLEXA_KEY: ${{ secrets.FLEXA_PUBLISHABLE_KEY }} run: | ./gradlew :app:assembleDebug - name: Authenticate to Google Cloud for Firebase Test Lab @@ -533,6 +537,7 @@ jobs: ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS: androiddebugkey ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS_PASSWORD: android ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} + ORG_GRADLE_PROJECT_ZCASH_FLEXA_KEY: ${{ secrets.FLEXA_PUBLISHABLE_KEY }} run: | ./gradlew :app:assembleDebug :app:bundleRelease :app:packageZcashmainnetReleaseUniversalApk - name: Collect Artifacts @@ -598,6 +603,7 @@ jobs: GOOGLE_CLOUD_PROJECT: ${{ vars.FIREBASE_TEST_LAB_PROJECT }} ORG_GRADLE_PROJECT_ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH: ${{ steps.auth_test_lab.outputs.credentials_file_path }} ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} + ORG_GRADLE_PROJECT_ZCASH_FLEXA_KEY: ${{ secrets.FLEXA_PUBLISHABLE_KEY }} run: | unzip ${BINARIES_ZIP_PATH} ./gradlew :app:runFlankSanityConfigRelease diff --git a/CHANGELOG.md b/CHANGELOG.md index bc34b6e9..859cd6a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2 ### Added - The device authentication feature on the Zashi app launch has been added +- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI ## [1.2.1 (760)] - 2024-10-22 diff --git a/app/proguard-project.txt b/app/proguard-project.txt index fdc604de..89dd7e56 100644 --- a/app/proguard-project.txt +++ b/app/proguard-project.txt @@ -10,12 +10,13 @@ -printconfiguration build/outputs/proguard-config.txt # This is generated automatically by the Android Gradle plugin. --dontwarn com.google.j2objc.annotations.ReflectionSupport --dontwarn com.google.j2objc.annotations.RetainedWith -dontwarn androidx.compose.ui.util.MathHelpersKt -dontwarn com.google.common.util.concurrent.ListenableFuture -dontwarn com.google.errorprone.annotations.InlineMe -dontwarn com.google.errorprone.annotations.MustBeClosed +-dontwarn com.google.j2objc.annotations.ReflectionSupport +-dontwarn com.google.j2objc.annotations.ReflectionSupport$Level +-dontwarn com.google.j2objc.annotations.RetainedWith -dontwarn javax.naming.directory.Attribute -dontwarn javax.naming.directory.Attributes -dontwarn javax.naming.directory.DirContext diff --git a/app/src/main/java/co/electriccoin/zcash/app/ZcashApplication.kt b/app/src/main/java/co/electriccoin/zcash/app/ZcashApplication.kt index 4d0820f3..46b992fa 100644 --- a/app/src/main/java/co/electriccoin/zcash/app/ZcashApplication.kt +++ b/app/src/main/java/co/electriccoin/zcash/app/ZcashApplication.kt @@ -10,6 +10,7 @@ import co.electriccoin.zcash.di.viewModelModule import co.electriccoin.zcash.preference.StandardPreferenceProvider import co.electriccoin.zcash.spackle.StrictModeCompat import co.electriccoin.zcash.spackle.Twig +import co.electriccoin.zcash.ui.common.repository.FlexaRepository import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import kotlinx.coroutines.launch import org.koin.android.ext.android.inject @@ -20,6 +21,7 @@ import org.koin.core.context.startKoin @Suppress("unused") class ZcashApplication : CoroutineApplication() { private val standardPreferenceProvider by inject() + private val flexaRepository by inject() override fun onCreate() { super.onCreate() @@ -44,6 +46,8 @@ class ZcashApplication : CoroutineApplication() { // Since analytics will need disk IO internally, we want this to be registered after strict // mode is configured to ensure none of that IO happens on the main thread configureAnalytics() + + flexaRepository.init() } private fun configureLogging() { diff --git a/build.gradle.kts b/build.gradle.kts index 7ec2f6e4..5d109ffa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,6 +45,7 @@ buildscript { } } } + maven("${rootProject.projectDir}/maven") // url to a local maven in this repository } dependencies { @@ -121,6 +122,7 @@ tasks { "ZCASH_GOOGLE_PLAY_DEPLOY_TRACK" to "internal", "ZCASH_GOOGLE_PLAY_DEPLOY_STATUS" to "draft", + "ZCASH_FLEXA_KEY" to "", "ZCASH_COINBASE_APP_ID" to "", "SDK_INCLUDED_BUILD_PATH" to "", "BIP_39_INCLUDED_BUILD_PATH" to "" diff --git a/docs/whatsNew/WHATS_NEW_EN.md b/docs/whatsNew/WHATS_NEW_EN.md index 3598ea12..7dc1fafe 100644 --- a/docs/whatsNew/WHATS_NEW_EN.md +++ b/docs/whatsNew/WHATS_NEW_EN.md @@ -11,6 +11,7 @@ directly impact users rather than highlighting other key architectural updates.* ### Added - The device authentication feature on the Zashi app launch has been added +- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI ## [1.2.1 (760)] - 2024-10-22 diff --git a/gradle.properties b/gradle.properties index e61f8fbd..9aff154d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -88,6 +88,9 @@ IS_SCREEN_ROTATION_ENABLED=false # set it up. ZCASH_COINBASE_APP_ID= +# Set the flexa publishable key to setup local integration. Replaced by CI action. +ZCASH_FLEXA_KEY= + # Set keystore details to enable build signing. Typically these # are overridden via ~/.gradle/gradle.properties to allow secure injection. # Debug keystore is useful if using Google Maps or Firebase, which require API keys to be linked @@ -167,7 +170,7 @@ ANDROIDX_ANNOTATION_VERSION=1.7.1 ANDROIDX_BIOMETRIC_VERSION=1.2.0-alpha05 ANDROIDX_CAMERA_VERSION=1.3.2 ANDROIDX_COMPOSE_COMPILER_VERSION=1.5.11 -ANDROIDX_COMPOSE_MATERIAL3_VERSION=1.2.1 +ANDROIDX_COMPOSE_MATERIAL3_VERSION=1.3.1 ANDROIDX_COMPOSE_MATERIAL_ICONS_VERSION=1.6.5 ANDROIDX_COMPOSE_VERSION=1.6.6 ANDROIDX_CONSTRAINTLAYOUT_VERSION=1.0.1 @@ -189,7 +192,7 @@ ANDROIDX_TEST_SERVICE_VERSION=1.4.2 ANDROIDX_UI_AUTOMATOR_VERSION=2.3.0 ANDROIDX_WORK_MANAGER_VERSION=2.9.0 ANDROIDX_BROWSER_VERSION=1.8.0 -CORE_LIBRARY_DESUGARING_VERSION=2.0.4 +CORE_LIBRARY_DESUGARING_VERSION=2.1.2 FIREBASE_BOM_VERSION_MATCHER=33.1.1 GOOGLE_API_CLIENT_ANDROID_VERSION=1.26.0 GOOGLE_API_SERVICES_DRIVE_VERSION=v3-rev136-1.25.0 @@ -213,6 +216,8 @@ ZXING_VERSION=3.5.3 ZIP_321_VERSION = 0.0.6 ZCASH_BIP39_VERSION=1.0.8 +FLEXA_VERSION=1.0.5 + # WARNING: Ensure a non-snapshot version is used before releasing to production ZCASH_SDK_VERSION=2.2.5 diff --git a/maven/com/flexa/core/1.0.5/core-1.0.5.aar b/maven/com/flexa/core/1.0.5/core-1.0.5.aar new file mode 100644 index 00000000..5ffeeb91 Binary files /dev/null and b/maven/com/flexa/core/1.0.5/core-1.0.5.aar differ diff --git a/maven/com/flexa/core/1.0.5/core-1.0.5.pom b/maven/com/flexa/core/1.0.5/core-1.0.5.pom new file mode 100644 index 00000000..b233e8e4 --- /dev/null +++ b/maven/com/flexa/core/1.0.5/core-1.0.5.pom @@ -0,0 +1,120 @@ + + + 4.0.0 + com.flexa + core + 1.0.5 + aar + + + androidx.core + core-ktx + 1.13.1 + + + androidx.compose.ui + ui + 1.7.1 + + + androidx.compose.ui + ui-tooling-preview + 1.7.1 + + + androidx.compose.material + material-icons-extended + 1.7.1 + + + androidx.compose.material3 + material3 + 1.3.0 + + + androidx.activity + activity-compose + 1.9.2 + + + androidx.compose.runtime + runtime-livedata + 1.7.1 + + + androidx.navigation + navigation-compose + 2.8.0 + + + androidx.webkit + webkit + 1.12.1 + + + androidx.lifecycle + lifecycle-runtime-compose + 2.8.5 + + + androidx.lifecycle + lifecycle-viewmodel-compose + 2.8.5 + + + androidx.appcompat + appcompat + 1.7.0 + + + io.coil-kt + coil-compose + 2.7.0 + + + joda-time + joda-time + 2.12.7 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + com.squareup.okhttp3 + logging-interceptor + 4.12.0 + + + com.squareup.okhttp3 + okhttp-sse + 4.12.0 + + + androidx.security + security-crypto + 1.0.0 + + + com.google.crypto.tink + tink-android + 1.8.0 + + + com.google.code.gson + gson + 2.11.0 + + + org.jetbrains.kotlinx + kotlinx-serialization-json + 1.6.3 + + + androidx.room + room-runtime + 2.6.1 + + + diff --git a/maven/com/flexa/core/maven-metadata-local.xml b/maven/com/flexa/core/maven-metadata-local.xml new file mode 100644 index 00000000..83c6ba6b --- /dev/null +++ b/maven/com/flexa/core/maven-metadata-local.xml @@ -0,0 +1,13 @@ + + + com.flexa + core + + 1.0.5 + 1.0.5 + + 1.0.5 + + 20241030092850 + + diff --git a/maven/com/flexa/spend/1.0.5/spend-1.0.5.aar b/maven/com/flexa/spend/1.0.5/spend-1.0.5.aar new file mode 100644 index 00000000..04f325a0 Binary files /dev/null and b/maven/com/flexa/spend/1.0.5/spend-1.0.5.aar differ diff --git a/maven/com/flexa/spend/1.0.5/spend-1.0.5.pom b/maven/com/flexa/spend/1.0.5/spend-1.0.5.pom new file mode 100644 index 00000000..163e30e3 --- /dev/null +++ b/maven/com/flexa/spend/1.0.5/spend-1.0.5.pom @@ -0,0 +1,105 @@ + + + 4.0.0 + com.flexa + spend + 1.0.5 + aar + + + com.flexa + core + 1.0.5 + + + androidx.core + core-ktx + 1.13.1 + + + androidx.compose.ui + ui + 1.7.1 + + + androidx.compose.ui + ui-util + 1.7.1 + + + androidx.compose.ui + ui-tooling-preview + 1.7.1 + + + androidx.compose.material + material-icons-extended + 1.7.1 + + + androidx.compose.material3 + material3 + 1.3.0 + + + androidx.activity + activity-compose + 1.9.2 + + + androidx.lifecycle + lifecycle-viewmodel-compose + 2.8.5 + + + androidx.navigation + navigation-compose + 2.8.0 + + + androidx.compose.runtime + runtime-livedata + 1.7.1 + + + androidx.webkit + webkit + 1.12.1 + + + io.coil-kt + coil-compose + 2.7.0 + + + io.coil-kt + coil-svg + 2.7.0 + + + com.caverock + androidsvg-aar + 1.4 + + + com.google.zxing + core + 3.5.3 + + + commons-codec + commons-codec + 1.15 + + + androidx.work + work-runtime-ktx + 2.9.1 + + + com.flexa + core + 1.0.5 + + + diff --git a/maven/com/flexa/spend/maven-metadata-local.xml b/maven/com/flexa/spend/maven-metadata-local.xml new file mode 100644 index 00000000..bc45ceb0 --- /dev/null +++ b/maven/com/flexa/spend/maven-metadata-local.xml @@ -0,0 +1,13 @@ + + + com.flexa + spend + + 1.0.5 + 1.0.5 + + 1.0.5 + + 20241030092854 + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 8820bd0a..24e3250b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -135,6 +135,7 @@ dependencyResolutionManagement { } } } + maven("${rootProject.projectDir}/maven") // url to a local maven in this repository } @Suppress("MaxLineLength") @@ -190,6 +191,7 @@ dependencyResolutionManagement { val googleApiClientAndroidVersion = extra["GOOGLE_API_CLIENT_ANDROID_VERSION"].toString() val googleApiServicesDriveVersion = extra["GOOGLE_API_SERVICES_DRIVE_VERSION"].toString() val playServicesAuthVersion = extra["PLAY_SERVICES_AUTH_VERSION"].toString() + val flexaVersion = extra["FLEXA_VERSION"].toString() // Standalone versions @@ -259,6 +261,8 @@ dependencyResolutionManagement { library("zxing", "com.google.zxing:core:$zxingVersion") library("koin", "io.insert-koin:koin-android:$koinVersion") library("koin-compose", "io.insert-koin:koin-androidx-compose:$koinVersion") + library("flexa-core", "com.flexa:core:$flexaVersion") + library("flexa-spend", "com.flexa:spend:$flexaVersion") // Test libraries library("androidx-compose-test-junit", "androidx.compose.ui:ui-test-junit4:$androidxComposeVersion") diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Dialog.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Dialog.kt index 1c062abd..b14f2997 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Dialog.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Dialog.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors import co.electriccoin.zcash.ui.design.util.StringResource import co.electriccoin.zcash.ui.design.util.getValue @@ -109,8 +110,9 @@ fun AppAlertDialog( text = text, icon = icon?.let { { Icon(imageVector = icon, null) } }, properties = properties, - titleContentColor = ZcashTheme.colors.textPrimary, - textContentColor = ZcashTheme.colors.textPrimary, + containerColor = ZashiColors.Surfaces.bgPrimary, + titleContentColor = ZashiColors.Text.textPrimary, + textContentColor = ZashiColors.Text.textPrimary, modifier = modifier, ) } diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Progress.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Progress.kt index 527eb9c9..763407e0 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Progress.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Progress.kt @@ -92,10 +92,12 @@ fun SmallLinearProgressIndicator( modifier: Modifier = Modifier ) { LinearProgressIndicator( + drawStopIndicator = {}, progress = { progress }, color = ZcashTheme.colors.linearProgressBarBackground, trackColor = ZcashTheme.colors.linearProgressBarTrack, strokeCap = StrokeCap.Butt, + gapSize = 0.dp, modifier = Modifier .fillMaxWidth() diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiRadioButton.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiRadioButton.kt index c7dabdb9..55cf49c9 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiRadioButton.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiRadioButton.kt @@ -18,8 +18,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -56,7 +56,7 @@ fun RadioButton( modifier .clip(RoundedCornerShape(12.dp)) .clickable( - indication = rememberRipple(), + indication = ripple(), interactionSource = remember { MutableInteractionSource() }, onClick = state.onClick, role = Role.Button, diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSettingsListItem.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSettingsListItem.kt index f2eb6327..1f3d0864 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSettingsListItem.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSettingsListItem.kt @@ -13,9 +13,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -145,7 +146,7 @@ fun ZashiSettingsListContentItem( titleIcons.forEach { Spacer(Modifier.width(6.dp)) Image( - modifier = Modifier.size(20.dp), + modifier = Modifier.size(20.dp).clip(CircleShape), painter = painterResource(it), contentDescription = null, ) @@ -176,7 +177,7 @@ fun ZashiSettingsListItem( .clip(RoundedCornerShape(12.dp)) then if (onClick != null) { Modifier.clickable( - indication = rememberRipple(), + indication = ripple(), interactionSource = remember { MutableInteractionSource() }, onClick = onClick, role = Role.Button, diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/theme/ZcashTheme.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/theme/ZcashTheme.kt index 9e5d4582..b26839bc 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/theme/ZcashTheme.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/theme/ZcashTheme.kt @@ -1,7 +1,12 @@ package co.electriccoin.zcash.ui.design.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RippleConfiguration +import androidx.compose.material3.RippleDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal @@ -26,6 +31,7 @@ import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypographyInternal * @param forceDarkMode Set this to true to force the app to use the dark mode theme, which is helpful, e.g., * for the compose previews. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ZcashTheme( forceDarkMode: Boolean = false, @@ -39,7 +45,8 @@ fun ZcashTheme( CompositionLocalProvider( LocalExtendedColors provides extendedColors, LocalZashiColors provides zashiColors, - LocalZashiTypography provides ZashiTypographyInternal + LocalZashiTypography provides ZashiTypographyInternal, + LocalRippleConfiguration provides MaterialRippleConfig, ) { ProvideDimens { MaterialTheme( @@ -71,3 +78,8 @@ object ZcashTheme { @Composable get() = localDimens.current } + +@OptIn(ExperimentalMaterial3Api::class) +private val MaterialRippleConfig: RippleConfiguration + @Composable + get() = RippleConfiguration(color = LocalContentColor.current, rippleAlpha = RippleDefaults.RippleAlpha) diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index 3cfabbd3..153a27ad 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -82,6 +82,14 @@ androidComponents { comment = "Whether is the SecureScreen sensitive data protection enabled" ) ) + variant.buildConfigFields.put( + "ZCASH_FLEXA_KEY", + BuildConfigField( + type = "String", + value = "\"${project.property("ZCASH_FLEXA_KEY")?.toString().orEmpty()}\"", + comment = "Publishable key of the Flexa integration" + ) + ) variant.buildConfigFields.put( "ZCASH_COINBASE_APP_ID", BuildConfigField( @@ -129,6 +137,9 @@ dependencies { implementation(libs.zcash.bip39) implementation(libs.zxing) + api(libs.flexa.core) + api(libs.flexa.spend) + implementation(projects.buildInfoLib) implementation(projects.configurationApiLib) implementation(projects.crashAndroidLib) diff --git a/ui-lib/src/main/AndroidManifest.xml b/ui-lib/src/main/AndroidManifest.xml index 571211a7..32b8da86 100644 --- a/ui-lib/src/main/AndroidManifest.xml +++ b/ui-lib/src/main/AndroidManifest.xml @@ -22,6 +22,11 @@ android:label="@string/app_name" android:theme="@style/Theme.App.Starting" /> + + diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/RepositoryModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/RepositoryModule.kt index 0f23c5dd..fc25ca91 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/RepositoryModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/RepositoryModule.kt @@ -4,10 +4,14 @@ import co.electriccoin.zcash.ui.common.repository.AddressBookRepository import co.electriccoin.zcash.ui.common.repository.AddressBookRepositoryImpl import co.electriccoin.zcash.ui.common.repository.BalanceRepository import co.electriccoin.zcash.ui.common.repository.BalanceRepositoryImpl +import co.electriccoin.zcash.ui.common.repository.BiometricRepository +import co.electriccoin.zcash.ui.common.repository.BiometricRepositoryImpl import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository import co.electriccoin.zcash.ui.common.repository.ConfigurationRepositoryImpl import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepositoryImpl +import co.electriccoin.zcash.ui.common.repository.FlexaRepository +import co.electriccoin.zcash.ui.common.repository.FlexaRepositoryImpl import co.electriccoin.zcash.ui.common.repository.WalletRepository import co.electriccoin.zcash.ui.common.repository.WalletRepositoryImpl import org.koin.core.module.dsl.singleOf @@ -21,4 +25,6 @@ val repositoryModule = singleOf(::ExchangeRateRepositoryImpl) bind ExchangeRateRepository::class singleOf(::BalanceRepositoryImpl) bind BalanceRepository::class singleOf(::AddressBookRepositoryImpl) bind AddressBookRepository::class + singleOf(::FlexaRepositoryImpl) bind FlexaRepository::class + singleOf(::BiometricRepositoryImpl) bind BiometricRepository::class } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt index 6852b6c0..33c998e4 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt @@ -11,6 +11,7 @@ import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase +import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase @@ -60,6 +61,7 @@ val useCaseModule = singleOf(::ObserveContactPickedUseCase) singleOf(::GetAddressesUseCase) singleOf(::CopyToClipboardUseCase) + singleOf(::IsFlexaAvailableUseCase) singleOf(::ShareImageUseCase) singleOf(::Zip321BuildUriUseCase) singleOf(::Zip321ProposalFromUriUseCase) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt index c2c35720..a6aab523 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt @@ -79,4 +79,5 @@ val viewModelModule = zip321ParseUriValidationUseCase = get(), ) } + viewModelOf(::IntegrationsViewModel) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/BiometricActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/BiometricActivity.kt new file mode 100644 index 00000000..66d835b0 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/BiometricActivity.kt @@ -0,0 +1,70 @@ +package co.electriccoin.zcash.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import co.electriccoin.zcash.ui.common.repository.BiometricRepository +import co.electriccoin.zcash.ui.common.repository.BiometricResult +import org.koin.android.ext.android.inject + +class BiometricActivity : FragmentActivity() { + private val biometricRepository by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val requestCode = intent.getStringExtra(EXTRA_REQUEST_CODE).orEmpty() + val subtitle = intent.getStringExtra(EXTRA_SUBTITLE).orEmpty() + + val biometricPrompt = + BiometricPrompt( + this, + ContextCompat.getMainExecutor(application), + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) { + super.onAuthenticationError(errorCode, errString) + biometricRepository.onBiometricResult(BiometricResult.Failure(requestCode)) + finish() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + biometricRepository.onBiometricResult(BiometricResult.Success(requestCode)) + finish() + } + } + ) + + val promptInfo = + BiometricPrompt.PromptInfo.Builder() + .setTitle( + getString(R.string.authentication_system_ui_title, getString(R.string.app_name)) + ) + .setSubtitle(subtitle) + .setAllowedAuthenticators(biometricRepository.allowedAuthenticators) + .build() + + biometricPrompt.authenticate(promptInfo) + } + + companion object { + private const val EXTRA_REQUEST_CODE = "EXTRA_REQUEST_CODE" + private const val EXTRA_SUBTITLE = "EXTRA_SUBTITLE" + + fun createIntent( + context: Context, + requestCode: String, + subtitle: String + ) = Intent(context, BiometricActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(EXTRA_REQUEST_CODE, requestCode) + putExtra(EXTRA_SUBTITLE, subtitle) + } + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/BalanceWidget.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/BalanceWidget.kt index 8fb62e1f..24ea7eee 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/BalanceWidget.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/BalanceWidget.kt @@ -73,6 +73,7 @@ private fun BalanceWidgetNotAvailableYetPreview() { balanceState = BalanceState.Loading( totalBalance = Zatoshi(value = 0L), + spendableBalance = Zatoshi(value = 0L), exchangeRate = ObserveFiatCurrencyResultFixture.new() ), isHideBalances = false, @@ -96,6 +97,7 @@ private fun BalanceWidgetHiddenAmountPreview() { balanceState = BalanceState.Loading( totalBalance = Zatoshi(0L), + spendableBalance = Zatoshi(0L), exchangeRate = ObserveFiatCurrencyResultFixture.new() ), isHideBalances = true, @@ -109,23 +111,26 @@ private fun BalanceWidgetHiddenAmountPreview() { sealed interface BalanceState { val totalBalance: Zatoshi + val spendableBalance: Zatoshi val exchangeRate: ExchangeRateState data class None( override val exchangeRate: ExchangeRateState ) : BalanceState { override val totalBalance: Zatoshi = Zatoshi(0L) + override val spendableBalance: Zatoshi = Zatoshi(0L) } data class Loading( override val totalBalance: Zatoshi, + override val spendableBalance: Zatoshi, override val exchangeRate: ExchangeRateState ) : BalanceState data class Available( override val totalBalance: Zatoshi, - override val exchangeRate: ExchangeRateState, - val spendableBalance: Zatoshi + override val spendableBalance: Zatoshi, + override val exchangeRate: ExchangeRateState ) : BalanceState } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/BalanceRepository.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/BalanceRepository.kt index 4cb085f7..e545c08e 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/BalanceRepository.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/BalanceRepository.kt @@ -48,6 +48,7 @@ class BalanceRepositoryImpl( ) -> { BalanceState.Loading( totalBalance = snapshot.totalBalance(), + spendableBalance = snapshot.spendableBalance(), exchangeRate = exchangeRateUsd ) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/BiometricRepository.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/BiometricRepository.kt new file mode 100644 index 00000000..c724b157 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/BiometricRepository.kt @@ -0,0 +1,108 @@ +package co.electriccoin.zcash.ui.common.repository + +import android.content.Context +import androidx.biometric.BiometricManager +import co.electriccoin.zcash.spackle.AndroidApiVersion +import co.electriccoin.zcash.ui.BiometricActivity +import co.electriccoin.zcash.ui.design.util.StringResource +import co.electriccoin.zcash.ui.design.util.getString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import java.util.UUID + +interface BiometricRepository { + val allowedAuthenticators: Int + + fun onBiometricResult(result: BiometricResult) + + @Throws(BiometricsFailureException::class, BiometricsCancelledException::class) + suspend fun requestBiometrics(request: BiometricRequest) +} + +data class BiometricRequest( + val message: StringResource, + val requestCode: String = UUID.randomUUID().toString(), +) + +sealed interface BiometricResult { + val requestCode: String + + data class Success(override val requestCode: String) : BiometricResult + + data class Failure(override val requestCode: String) : BiometricResult + + data class Cancelled(override val requestCode: String) : BiometricResult +} + +class BiometricsFailureException : Exception() + +class BiometricsCancelledException : Exception() + +class BiometricRepositoryImpl( + private val context: Context, + private val biometricManager: BiometricManager +) : BiometricRepository { + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + + private val onBiometricsResult = MutableSharedFlow() + + override val allowedAuthenticators: Int + get() = + when { + // Android SDK version == 27 + (AndroidApiVersion.isExactlyO) -> + BiometricManager.Authenticators.BIOMETRIC_STRONG or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + // Android SDK version >= 30 + (AndroidApiVersion.isAtLeastR) -> + BiometricManager.Authenticators.BIOMETRIC_STRONG or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + // Android SDK version == 28 || 29 + (AndroidApiVersion.isExactlyP || AndroidApiVersion.isExactlyQ) -> + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + + else -> error("Unsupported Android SDK version") + } + + override fun onBiometricResult(result: BiometricResult) { + scope.launch { + onBiometricsResult.emit(result) + } + } + + override suspend fun requestBiometrics(request: BiometricRequest) { + if (!canAuthenticate()) { + // do nothing + return + } + + context.startActivity( + BiometricActivity.createIntent( + context = context, + requestCode = request.requestCode, + subtitle = request.message.getString(context) + ) + ) + when ( + onBiometricsResult.filter { it.requestCode == request.requestCode }.first() + ) { + is BiometricResult.Cancelled -> throw BiometricsCancelledException() + is BiometricResult.Failure -> throw BiometricsFailureException() + is BiometricResult.Success -> { + // do nothing + } + } + } + + private fun canAuthenticate(): Boolean = + when (biometricManager.canAuthenticate(allowedAuthenticators)) { + BiometricManager.BIOMETRIC_SUCCESS -> true + else -> false + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/FlexaRepository.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/FlexaRepository.kt new file mode 100644 index 00000000..f84195e2 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/FlexaRepository.kt @@ -0,0 +1,117 @@ +package co.electriccoin.zcash.ui.common.repository + +import android.app.Application +import cash.z.ecc.android.sdk.ext.convertZatoshiToZec +import cash.z.ecc.android.sdk.internal.Twig +import co.electriccoin.zcash.ui.BuildConfig +import com.flexa.core.Flexa +import com.flexa.core.shared.AssetAccount +import com.flexa.core.shared.AvailableAsset +import com.flexa.core.shared.CustodyModel +import com.flexa.core.shared.FlexaClientConfiguration +import com.flexa.core.theme.FlexaTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import java.security.MessageDigest +import java.util.UUID + +interface FlexaRepository { + fun init() +} + +class FlexaRepositoryImpl( + private val balanceRepository: BalanceRepository, + private val application: Application, +) : FlexaRepository { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private val publishableKey: String? + get() = BuildConfig.ZCASH_FLEXA_KEY.takeIf { it.isNotEmpty() } + + override fun init() { + scope.launch { + val configuration = getFlexaClientConfiguration() + if (configuration != null) { + Flexa.init(configuration) + Twig.info { "Flexa initialized" } + + balanceRepository.state + .map { it.spendableBalance } + .collect { + Flexa.updateAssetAccounts( + arrayListOf( + createFlexaAccount( + zecBalance = it.convertZatoshiToZec().toDouble() + ) + ) + ) + + Twig.info { "Flexa updated by ${it.convertZatoshiToZec().toDouble()}" } + } + } + } + } + + /** + * @return an instance of [FlexaClientConfiguration] or null if no publishable key set up + */ + private fun getFlexaClientConfiguration() = + publishableKey?.let { publishableKey -> + FlexaClientConfiguration( + context = application, + publishableKey = publishableKey, + theme = + FlexaTheme( + useDynamicColorScheme = true, + ), + assetAccounts = arrayListOf(createFlexaAccount(DEFAULT_ZEC_BALANCE)), + webViewThemeConfig = + "{\n" + + " \"android\": {\n" + + " \"light\": {\n" + + " \"backgroundColor\": \"#100e29\",\n" + + " \"sortTextColor\": \"#ed7f60\",\n" + + " \"titleColor\": \"#ffffff\",\n" + + " \"cardColor\": \"#2a254e\",\n" + + " \"borderRadius\": \"15px\",\n" + + " \"textColor\": \"#ffffff\"\n" + + " },\n" + + " \"dark\": {\n" + + " \"backgroundColor\": \"#100e29\",\n" + + " \"sortTextColor\": \"#ed7f60\",\n" + + " \"titleColor\": \"#ffffff\",\n" + + " \"cardColor\": \"#2a254e\",\n" + + " \"borderRadius\": \"15px\",\n" + + " \"textColor\": \"#ffffff\"\n" + + " }\n" + + " }\n" + + "}" + ) + } + + private fun createFlexaAccount(zecBalance: Double) = + AssetAccount( + displayName = "", + icon = "https://flexa.network/static/4bbb1733b3ef41240ca0f0675502c4f7/d8419/flexa-logo%403x.png", + availableAssets = + listOf( + AvailableAsset( + assetId = "bip122:00040fe8ec8471911baa1db1266ea15d/slip44:133", + balance = zecBalance, + symbol = "ZEC", + ) + ), + custodyModel = CustodyModel.LOCAL, + assetAccountHash = UUID.randomUUID().toString().toSha256() + ) + + private fun String.toSha256() = + MessageDigest.getInstance("SHA-256") + .digest(toByteArray()) + .fold("") { str, value -> str + "%02x".format(value) } +} + +private const val DEFAULT_ZEC_BALANCE = .0 diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/IsFlexaAvailableUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/IsFlexaAvailableUseCase.kt new file mode 100644 index 00000000..6bda2baa --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/IsFlexaAvailableUseCase.kt @@ -0,0 +1,14 @@ +package co.electriccoin.zcash.ui.common.usecase + +import co.electriccoin.zcash.ui.BuildConfig +import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider + +class IsFlexaAvailableUseCase( + private val getVersionInfo: GetVersionInfoProvider, +) { + operator fun invoke(): Boolean { + val versionInfo = getVersionInfo() + val isDebug = versionInfo.let { it.isDebuggable && !it.isRunningUnderTestService } + return !versionInfo.isTestnet && (BuildConfig.ZCASH_FLEXA_KEY.isNotEmpty() || isDebug) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt index f22b0911..af302a40 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt @@ -29,11 +29,15 @@ import co.electriccoin.zcash.ui.common.repository.BalanceRepository import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository import co.electriccoin.zcash.ui.common.repository.WalletRepository import co.electriccoin.zcash.ui.common.usecase.DeleteAddressBookUseCase +import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState +import com.flexa.core.Flexa +import com.flexa.identity.buildIdentity import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -45,8 +49,11 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine // To make this more multiplatform compatible, we need to remove the dependency on Context // for loading the preferences. @@ -63,6 +70,7 @@ class WalletViewModel( private val standardPreferenceProvider: StandardPreferenceProvider, private val getAvailableServers: GetDefaultServersProvider, private val deleteAddressBookUseCase: DeleteAddressBookUseCase, + private val isFlexaAvailable: IsFlexaAvailableUseCase ) : AndroidViewModel(application) { val navigationCommand = exchangeRateRepository.navigationCommand @@ -234,29 +242,27 @@ class WalletViewModel( fun deleteWalletFlow(activity: Activity): Flow = callbackFlow { Twig.info { "Delete wallet: Requested" } - + disconnectFlexa() val synchronizer = synchronizer.value if (null != synchronizer) { - viewModelScope.launch { - (synchronizer as SdkSynchronizer).closeFlow().collect { - Twig.info { "Delete wallet: SDK closed" } + (synchronizer as SdkSynchronizer).closeFlow().collect { + Twig.info { "Delete wallet: SDK closed" } - walletCoordinator.deleteSdkDataFlow().collect { isSdkErased -> - Twig.info { "Delete wallet: Erase SDK result: $isSdkErased" } - if (!isSdkErased) { + walletCoordinator.deleteSdkDataFlow().collect { isSdkErased -> + Twig.info { "Delete wallet: Erase SDK result: $isSdkErased" } + if (!isSdkErased) { + trySend(false) + } + + clearAppStateFlow().collect { isAppErased -> + Twig.info { "Delete wallet: Erase App result: $isAppErased" } + if (!isAppErased) { trySend(false) - } - - clearAppStateFlow().collect { isAppErased -> - Twig.info { "Delete wallet: Erase App result: $isAppErased" } - if (!isAppErased) { - trySend(false) - } else { - trySend(true) - activity.run { - finish() - startActivity(Intent(this, MainActivity::class.java)) - } + } else { + trySend(true) + activity.run { + finish() + startActivity(Intent(this, MainActivity::class.java)) } } } @@ -266,6 +272,16 @@ class WalletViewModel( awaitClose { // Nothing to close } + }.flowOn(Dispatchers.Main) + + private suspend fun disconnectFlexa() = + suspendCoroutine { cont -> + if (isFlexaAvailable()) { + Flexa.buildIdentity().build().disconnect() + cont.resume(Unit) + } else { + cont.resume(Unit) + } } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/view/HistoryView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/view/HistoryView.kt index 166bcb30..91af9984 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/view/HistoryView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/view/HistoryView.kt @@ -18,11 +18,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.DividerDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -628,7 +628,7 @@ private fun HistoryItemExpandedAddressPart( Modifier .clickable( role = Role.Button, - indication = rememberRipple(radius = 2.dp, color = ZashiColors.Text.textTertiary), + indication = ripple(radius = 2.dp, color = ZashiColors.Text.textTertiary), interactionSource = remember { MutableInteractionSource() } ) { onAction(TrxItemAction.AddressClick(recipient)) } ) @@ -646,7 +646,7 @@ private fun HistoryItemExpandedAddressPart( .weight(1f) .clickable( role = Role.Button, - indication = rememberRipple(radius = 2.dp, color = ZashiColors.Text.textTertiary), + indication = ripple(radius = 2.dp, color = ZashiColors.Text.textTertiary), interactionSource = remember { MutableInteractionSource() } ) { onAction(TrxItemAction.AddToAddressBookClick(recipient)) } ) @@ -781,7 +781,7 @@ private fun HistoryItemTransactionIdPart( Modifier .clickable( role = Role.Button, - indication = rememberRipple(radius = 2.dp, color = ZashiColors.Text.textTertiary), + indication = ripple(radius = 2.dp, color = ZashiColors.Text.textTertiary), interactionSource = remember { MutableInteractionSource() } ) { onAction(TrxItemAction.TransactionIdClick(txIdString)) } ) @@ -944,7 +944,7 @@ private fun HistoryItemMessagePart( .clickable( onClick = { onAction(TrxItemAction.MessageClick(message)) }, role = Role.Button, - indication = rememberRipple(radius = 2.dp, color = ZashiColors.Text.textTertiary), + indication = ripple(radius = 2.dp, color = ZashiColors.Text.textTertiary), interactionSource = remember { MutableInteractionSource() } ) ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exchangerate/widget/StyledExchangeBalance.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exchangerate/widget/StyledExchangeBalance.kt index 3a4a73f2..b52b764f 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exchangerate/widget/StyledExchangeBalance.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exchangerate/widget/StyledExchangeBalance.kt @@ -253,7 +253,7 @@ private fun ExchangeRateButton( if (isEnabled && enableBorder) { ZashiColors.Surfaces.bgPrimary orDark ZashiColors.Surfaces.bgTertiary } else { - Color.Unspecified + Color.Transparent }, disabledContainerColor = Color.Transparent, disabledContentColor = textColor, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt index 83524681..6b4fc9f7 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt @@ -112,7 +112,6 @@ private fun HomeContent( key = { index -> subScreens[index].title }, - beyondBoundsPageCount = 1, modifier = Modifier.constrainAs(pager) { top.linkTo(parent.top) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/AndroidIntegrations.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/AndroidIntegrations.kt index ebf0d141..cb417c1a 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/AndroidIntegrations.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/AndroidIntegrations.kt @@ -13,6 +13,8 @@ import co.electriccoin.zcash.ui.common.compose.LocalNavController import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.integrations.view.Integrations import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel +import com.flexa.core.Flexa +import com.flexa.spend.buildSpend import org.koin.androidx.compose.koinViewModel @Composable @@ -42,6 +44,17 @@ internal fun WrapIntegrations() { } } + LaunchedEffect(Unit) { + viewModel.flexaNavigationCommand.collect { + Flexa.buildSpend() + .onTransactionRequest { + viewModel.onFlexaResultCallback(it) + } + .build() + .open(activity) + } + } + BackHandler { viewModel.onBack() } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/model/IntegrationsState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/model/IntegrationsState.kt index 39ee843f..c5c31deb 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/model/IntegrationsState.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/model/IntegrationsState.kt @@ -8,5 +8,5 @@ data class IntegrationsState( val version: StringResource, val disabledInfo: StringResource?, val onBack: () -> Unit, - val items: ImmutableList + val items: ImmutableList, ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/viewmodel/IntegrationsViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/viewmodel/IntegrationsViewModel.kt index 96f95b51..88ac50ce 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/viewmodel/IntegrationsViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/integrations/viewmodel/IntegrationsViewModel.kt @@ -1,19 +1,41 @@ package co.electriccoin.zcash.ui.screen.integrations.viewmodel +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import cash.z.ecc.android.sdk.SdkSynchronizer +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.model.Proposal +import cash.z.ecc.android.sdk.model.TransactionSubmitResult +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey +import cash.z.ecc.android.sdk.model.WalletAddress +import cash.z.ecc.android.sdk.model.ZecSend +import cash.z.ecc.android.sdk.model.ZecSendExt +import cash.z.ecc.android.sdk.model.proposeSend +import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT +import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.ui.BuildConfig import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider +import co.electriccoin.zcash.ui.common.repository.BiometricRepository +import co.electriccoin.zcash.ui.common.repository.BiometricRequest +import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase +import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase +import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState +import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState +import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult +import com.flexa.core.Flexa +import com.flexa.spend.Transaction +import com.flexa.spend.buildSpend import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted @@ -26,10 +48,16 @@ class IntegrationsViewModel( getVersionInfo: GetVersionInfoProvider, getZcashCurrency: GetZcashCurrencyProvider, observeWalletState: ObserveWalletStateUseCase, + private val getSynchronizer: GetSynchronizerUseCase, private val getTransparentAddress: GetTransparentAddressUseCase, + private val isFlexaAvailable: IsFlexaAvailableUseCase, private val isCoinbaseAvailable: IsCoinbaseAvailableUseCase, + private val getSpendingKey: GetSpendingKeyUseCase, + private val context: Context, + private val biometricRepository: BiometricRepository ) : ViewModel() { val backNavigationCommand = MutableSharedFlow() + val flexaNavigationCommand = MutableSharedFlow() val coinbaseNavigationCommand = MutableSharedFlow() private val versionInfo = getVersionInfo() @@ -60,7 +88,21 @@ class IntegrationsViewModel( getZcashCurrency.getLocalizedName() ), onClick = ::onBuyWithCoinbaseClicked - ).takeIf { isCoinbaseAvailable() } + ).takeIf { isCoinbaseAvailable() }, + ZashiSettingsListItemState( + // Set the wallet currency by app build is more future-proof, although we hide it from + // the UI in the Testnet build + isEnabled = isEnabled, + icon = + if (isEnabled) { + R.drawable.ic_integrations_flexa + } else { + R.drawable.ic_integrations_flexa_disabled + }, + text = stringRes(R.string.integrations_flexa), + subtitle = stringRes(R.string.integrations_flexa_subtitle), + onClick = ::onFlexaClicked + ).takeIf { isFlexaAvailable() } ).toImmutableList() ) }.stateIn( @@ -98,4 +140,168 @@ class IntegrationsViewModel( } } } + + private fun onFlexaClicked() = + viewModelScope.launch { + flexaNavigationCommand.emit(Unit) + } + + fun onFlexaResultCallback(transaction: Result) = + viewModelScope.launch { + runCatching { + biometricRepository.requestBiometrics( + BiometricRequest(message = stringRes(R.string.integrations_biometric_message)) + ) + Twig.debug { "Getting send transaction proposal" } + getSynchronizer() + .proposeSend( + account = getSpendingKey().account, + send = getZecSend(transaction.getOrNull()) + ) + }.onSuccess { proposal -> + Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" } + val result = submitTransactions(proposal = proposal, spendingKey = getSpendingKey()) + when (result.first) { + SubmitResult.Success -> { + Twig.debug { "Transaction successful $result" } + Flexa.buildSpend() + .transactionSent( + commerceSessionId = transaction.getOrNull()?.commerceSessionId.orEmpty(), + txSignature = result.second.orEmpty() + ) + } + + else -> { + Twig.error { "Transaction submission failed" } + } + } + }.onFailure { + Twig.error(it) { "Transaction proposal failed" } + } + } + + private suspend fun submitTransactions( + proposal: Proposal, + spendingKey: UnifiedSpendingKey + ): Pair { + Twig.debug { "Sending transactions..." } + + val result = + runCreateTransactions( + synchronizer = getSynchronizer(), + spendingKey = spendingKey, + proposal = proposal + ) + + // Triggering the transaction history and balances refresh to be notified immediately + // about the wallet's updated state + (getSynchronizer() as SdkSynchronizer).run { + refreshTransactions() + refreshAllBalances() + } + + return result + } + + private suspend fun runCreateTransactions( + synchronizer: Synchronizer, + spendingKey: UnifiedSpendingKey, + proposal: Proposal + ): Pair { + val submitResults = mutableListOf() + + return runCatching { + synchronizer.createProposedTransactions( + proposal = proposal, + usk = spendingKey + ).collect { submitResult -> + Twig.info { "Transaction submit result: $submitResult" } + submitResults.add(submitResult) + } + if (submitResults.find { it is TransactionSubmitResult.Failure } != null) { + if (submitResults.size == 1) { + // The first transaction submission failed - user might just be able to re-submit the transaction + // proposal. Simple error pop up is fine then + val result = (submitResults[0] as TransactionSubmitResult.Failure) + if (result.grpcError) { + SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc(result) to null + } else { + SubmitResult.SimpleTrxFailure.SimpleTrxFailureSubmit(result) to null + } + } else { + // Any subsequent transaction submission failed - user needs to resolve this manually. Multiple + // transaction failure screen presented + SubmitResult.MultipleTrxFailure to null + } + } else { + // All transaction submissions were successful + SubmitResult.Success to + submitResults.filterIsInstance() + .map { it.txIdString() }.firstOrNull() + } + }.onSuccess { + Twig.debug { "Transactions submitted successfully" } + }.onFailure { + Twig.error(it) { "Transactions submission failed" } + }.getOrElse { + SubmitResult.SimpleTrxFailure.SimpleTrxFailureOther(it) to null + } + } + + @Suppress("TooGenericExceptionThrown") + private suspend fun getZecSend(transaction: Transaction?): ZecSend { + if (transaction == null) throw NullPointerException("Transaction is null") + + val address = transaction.destinationAddress.split(":").last() + + val recipientAddressState = + RecipientAddressState.new( + address = address, + // TODO [#342]: Verify Addresses without Synchronizer + // TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342 + type = getSynchronizer().validateAddress(address) + ) + + return when ( + val zecSendValidation = + ZecSendExt.new( + context = context, + destinationString = address, + zecString = transaction.amount, + // Take memo for a valid non-transparent receiver only + memoString = "" + ) + ) { + is ZecSendExt.ZecSendValidation.Valid -> + zecSendValidation.zecSend.copy( + destination = + when (recipientAddressState.type) { + is AddressType.Invalid -> + WalletAddress.Unified.new(recipientAddressState.address) + + AddressType.Shielded -> + WalletAddress.Unified.new(recipientAddressState.address) + + AddressType.Tex -> + WalletAddress.Tex.new(recipientAddressState.address) + + AddressType.Transparent -> + WalletAddress.Transparent.new(recipientAddressState.address) + + AddressType.Unified -> + WalletAddress.Unified.new(recipientAddressState.address) + + null -> WalletAddress.Unified.new(recipientAddressState.address) + } + ) + + is ZecSendExt.ZecSendValidation.Invalid -> { + // We do not expect this validation to fail, so logging is enough here + // An error popup could be reasonable here as well + Twig.warn { "Send failed with: ${zecSendValidation.validationErrors}" } + + throw RuntimeException("Validation failed") + } + } + } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt index 39cd7fed..68ae1b9a 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt @@ -596,7 +596,7 @@ private fun SeedGridWithText( keyboardOptions = KeyboardOptions( KeyboardCapitalization.None, - autoCorrect = false, + autoCorrectEnabled = false, imeAction = ImeAction.Done, keyboardType = KeyboardType.Password ), @@ -809,7 +809,7 @@ private fun RestoreBirthdayMainContent( keyboardOptions = KeyboardOptions( KeyboardCapitalization.None, - autoCorrect = false, + autoCorrectEnabled = false, imeAction = ImeAction.Done, keyboardType = KeyboardType.Number ), diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt index 3cbf5c47..200a9473 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -74,6 +73,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.LocalLifecycleOwner import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt index e6647807..081d7444 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt @@ -23,9 +23,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.IconButton import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -609,7 +609,7 @@ fun SendFormAddressTextField( Modifier.clickable( onClick = sendAddressBookState.onButtonClick, role = Role.Button, - indication = rememberRipple(radius = 4.dp), + indication = ripple(radius = 4.dp), interactionSource = remember { MutableInteractionSource() } ), painter = painterResource(sendAddressBookState.mode.icon), @@ -623,7 +623,7 @@ fun SendFormAddressTextField( Modifier.clickable( onClick = onQrScannerOpen, role = Role.Button, - indication = rememberRipple(radius = 4.dp), + indication = ripple(radius = 4.dp), interactionSource = remember { MutableInteractionSource() } ), painter = painterResource(R.drawable.qr_code_icon), diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt index 06e30528..a5fcce54 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt @@ -11,6 +11,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider +import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase import co.electriccoin.zcash.ui.configuration.ConfigurationEntries @@ -22,6 +23,7 @@ import co.electriccoin.zcash.ui.screen.settings.model.SettingsState import co.electriccoin.zcash.ui.screen.settings.model.SettingsTroubleshootingState import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingItemState import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -38,7 +40,8 @@ class SettingsViewModel( observeConfiguration: ObserveConfigurationUseCase, private val standardPreferenceProvider: StandardPreferenceProvider, private val getVersionInfo: GetVersionInfoProvider, - private val rescanBlockchain: RescanBlockchainUseCase + private val rescanBlockchain: RescanBlockchainUseCase, + private val isFlexaAvailable: IsFlexaAvailableUseCase ) : ViewModel() { private val versionInfo by lazy { getVersionInfo() } @@ -112,7 +115,11 @@ class SettingsViewModel( text = stringRes(R.string.settings_integrations), icon = R.drawable.ic_settings_integrations, onClick = ::onIntegrationsClick, - titleIcons = persistentListOf(R.drawable.ic_integrations_coinbase) + titleIcons = + listOfNotNull( + R.drawable.ic_integrations_coinbase, + R.drawable.ic_integrations_flexa.takeIf { isFlexaAvailable() } + ).toImmutableList() ), ZashiSettingsListItemState( text = stringRes(R.string.settings_advanced_settings), diff --git a/ui-lib/src/main/res/ui/common/values/themes.xml b/ui-lib/src/main/res/ui/common/values/themes.xml index 1ff6e0c9..8c40b10f 100644 --- a/ui-lib/src/main/res/ui/common/values/themes.xml +++ b/ui-lib/src/main/res/ui/common/values/themes.xml @@ -5,4 +5,9 @@ @drawable/no_icon_splash_logo @android:style/Theme.Material.NoActionBar + + diff --git a/ui-lib/src/main/res/ui/integrations/drawable/ic_integrations_flexa.png b/ui-lib/src/main/res/ui/integrations/drawable/ic_integrations_flexa.png new file mode 100644 index 00000000..8ae4f318 Binary files /dev/null and b/ui-lib/src/main/res/ui/integrations/drawable/ic_integrations_flexa.png differ diff --git a/ui-lib/src/main/res/ui/integrations/drawable/ic_integrations_flexa_disabled.png b/ui-lib/src/main/res/ui/integrations/drawable/ic_integrations_flexa_disabled.png new file mode 100644 index 00000000..d04920ad Binary files /dev/null and b/ui-lib/src/main/res/ui/integrations/drawable/ic_integrations_flexa_disabled.png differ diff --git a/ui-lib/src/main/res/ui/integrations/values/strings.xml b/ui-lib/src/main/res/ui/integrations/values/strings.xml index 9d305b1e..0f1552fc 100644 --- a/ui-lib/src/main/res/ui/integrations/values/strings.xml +++ b/ui-lib/src/main/res/ui/integrations/values/strings.xml @@ -2,6 +2,9 @@ Integrations Buy %1$s with Coinbase A hassle-free way to buy %1$s and get it directly into your Zashi wallet. - Version %s + Pay with Flexa + Pay with Flexa payment clips and explore a new way of spending Zcash. + Version %1$s During the Restore process, it is not possible to use payment integrations. + Authenticate yourself to pay with Flexa diff --git a/ui-lib/src/main/res/ui/settings/drawable/ic_settings_integrations_dark.xml b/ui-lib/src/main/res/ui/settings/drawable/ic_settings_integrations_dark.xml new file mode 100644 index 00000000..3975f950 --- /dev/null +++ b/ui-lib/src/main/res/ui/settings/drawable/ic_settings_integrations_dark.xml @@ -0,0 +1,16 @@ + + + +