[#1528] Coinbase on-ramp integration

* [#1528] Coinbase integration

Closes #1528

* [#1528] CI hotfix

Closes #1528

* Remove duplicate lines

* Improve CI scripts + variable renaming

* Remove coinbase button in testnet build

* Update changelogs

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Milan 2024-09-12 18:10:54 +02:00 committed by GitHub
parent fa9ea0c03a
commit 35c01df313
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 217 additions and 39 deletions

View File

@ -63,9 +63,11 @@ jobs:
# GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER }} # GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER }}
GOOGLE_PLAY_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY }} GOOGLE_PLAY_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY }}
GOOGLE_PLAY_PUBLISHER_API_KEY: ${{ secrets.GOOGLE_PLAY_PUBLISHER_API_KEY }} GOOGLE_PLAY_PUBLISHER_API_KEY: ${{ secrets.GOOGLE_PLAY_PUBLISHER_API_KEY }}
COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
if: "${{ env.GOOGLE_PLAY_CLOUD_PROJECT != '' && if: "${{ env.GOOGLE_PLAY_CLOUD_PROJECT != '' &&
env.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY != '' && env.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY != '' &&
env.GOOGLE_PLAY_PUBLISHER_API_KEY != '' env.GOOGLE_PLAY_PUBLISHER_API_KEY != '' &&
env.COINBASE_APP_ID != ''
}}" }}"
run: echo "defined=true" >> $GITHUB_OUTPUT run: echo "defined=true" >> $GITHUB_OUTPUT
@ -154,6 +156,7 @@ jobs:
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS: ${{ secrets.UPLOAD_KEY_ALIAS }} 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_RELEASE_KEY_ALIAS_PASSWORD: ${{ secrets.UPLOAD_KEY_ALIAS_PASSWORD }}
ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
run: | run: |
./gradlew :app:publishToGooglePlay ./gradlew :app:publishToGooglePlay
- name: Collect Artifacts - name: Collect Artifacts

View File

@ -63,6 +63,17 @@ jobs:
if: "${{ env.EMULATOR_WTF_API_KEY != '' }}" if: "${{ env.EMULATOR_WTF_API_KEY != '' }}"
run: echo "defined=true" >> $GITHUB_OUTPUT run: echo "defined=true" >> $GITHUB_OUTPUT
check_coinbase_secrets:
runs-on: ubuntu-latest
outputs:
has-secrets: ${{ steps.check_coinbase_secrets.outputs.defined }}
steps:
- id: check_coinbase_secrets
env:
COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
if: "${{ env.COINBASE_APP_ID != '' }}"
run: echo "defined=true" >> $GITHUB_OUTPUT
check_properties: check_properties:
needs: validate_gradle_wrapper needs: validate_gradle_wrapper
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -252,8 +263,8 @@ jobs:
# Emulator.wtf is preferred if it has an API key. # Emulator.wtf is preferred if it has an API key.
test_android_modules_ftl: test_android_modules_ftl:
if: needs.check_firebase_secrets.outputs.has-secrets == 'true' && needs.check_emulator_wtf_secrets.outputs.has-secrets == 'false' if: needs.check_firebase_secrets.outputs.has-secrets == 'true' && needs.check_emulator_wtf_secrets.outputs.has-secrets == 'false' && needs.check_coinbase_secrets.outputs.has-secrets == 'true'
needs: [validate_gradle_wrapper, check_firebase_secrets, check_emulator_wtf_secrets] needs: [validate_gradle_wrapper, check_firebase_secrets, check_emulator_wtf_secrets, check_coinbase_secrets]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@ -295,6 +306,7 @@ jobs:
ORG_GRADLE_PROJECT_ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH: ${{ steps.auth_test_lab.outputs.credentials_file_path }} ORG_GRADLE_PROJECT_ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH: ${{ steps.auth_test_lab.outputs.credentials_file_path }}
# Because Fulladle doesn't allow Test Orchestrator to be enabled/disabled for a specific submodule, it must be enabled for all modules # 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_IS_USE_TEST_ORCHESTRATOR: true
ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
run: | run: |
./gradlew runFlank ./gradlew runFlank
- name: Collect Artifacts - name: Collect Artifacts
@ -316,8 +328,8 @@ jobs:
path: ~/artifacts path: ~/artifacts
test_android_modules_wtf_coverage: test_android_modules_wtf_coverage:
if: needs.check_emulator_wtf_secrets.outputs.has-secrets == 'true' if: needs.check_emulator_wtf_secrets.outputs.has-secrets == 'true' && needs.check_coinbase_secrets.outputs.has-secrets == 'true'
needs: [ validate_gradle_wrapper, check_emulator_wtf_secrets ] needs: [ validate_gradle_wrapper, check_emulator_wtf_secrets, check_coinbase_secrets ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@ -342,6 +354,7 @@ jobs:
ORG_GRADLE_PROJECT_ZCASH_DEBUG_APP_NAME_SUFFIX: "" ORG_GRADLE_PROJECT_ZCASH_DEBUG_APP_NAME_SUFFIX: ""
ORG_GRADLE_PROJECT_ZCASH_EMULATOR_WTF_API_KEY: ${{ secrets.EMULATOR_WTF_API_KEY }} 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_IS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED: true
ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
run: | run: |
./gradlew testDebugWithEmulatorWtf :ui-integration-test:testZcashmainnetDebugWithEmulatorWtf ./gradlew testDebugWithEmulatorWtf :ui-integration-test:testZcashmainnetDebugWithEmulatorWtf
- name: Collect Artifacts - name: Collect Artifacts
@ -363,8 +376,8 @@ jobs:
path: ~/artifacts path: ~/artifacts
test_android_modules_wtf_no_coverage: test_android_modules_wtf_no_coverage:
if: needs.check_emulator_wtf_secrets.outputs.has-secrets == 'true' if: needs.check_emulator_wtf_secrets.outputs.has-secrets == 'true' && needs.check_coinbase_secrets.outputs.has-secrets == 'true'
needs: [ validate_gradle_wrapper, check_emulator_wtf_secrets ] needs: [ validate_gradle_wrapper, check_emulator_wtf_secrets, check_coinbase_secrets ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@ -389,6 +402,7 @@ jobs:
ORG_GRADLE_PROJECT_ZCASH_DEBUG_APP_NAME_SUFFIX: "" ORG_GRADLE_PROJECT_ZCASH_DEBUG_APP_NAME_SUFFIX: ""
ORG_GRADLE_PROJECT_ZCASH_EMULATOR_WTF_API_KEY: ${{ secrets.EMULATOR_WTF_API_KEY }} 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_IS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED: false
ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
run: | run: |
./gradlew :app:testZcashmainnetDebugWithEmulatorWtf :ui-screenshot-test:testZcashmainnetDebugWithEmulatorWtf ./gradlew :app:testZcashmainnetDebugWithEmulatorWtf :ui-screenshot-test:testZcashmainnetDebugWithEmulatorWtf
- name: Collect Artifacts - name: Collect Artifacts
@ -411,8 +425,8 @@ jobs:
# Performs a button mash test on the debug build of the app with strict mode enabled # Performs a button mash test on the debug build of the app with strict mode enabled
test_robo_debug: test_robo_debug:
if: needs.check_firebase_secrets.outputs.has-secrets == 'true' if: needs.check_firebase_secrets.outputs.has-secrets == 'true' && needs.check_coinbase_secrets.outputs.has-secrets == 'true'
needs: [check_firebase_secrets] needs: [check_firebase_secrets, check_coinbase_secrets]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: read packages: read
@ -447,6 +461,7 @@ jobs:
env: env:
ORG_GRADLE_PROJECT_ZCASH_SUPPORT_EMAIL_ADDRESS: ${{ vars.SUPPORT_EMAIL_ADDRESS }} ORG_GRADLE_PROJECT_ZCASH_SUPPORT_EMAIL_ADDRESS: ${{ vars.SUPPORT_EMAIL_ADDRESS }}
ORG_GRADLE_PROJECT_IS_CRASH_ON_STRICT_MODE_VIOLATION: true ORG_GRADLE_PROJECT_IS_CRASH_ON_STRICT_MODE_VIOLATION: true
ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
run: | run: |
./gradlew :app:assembleDebug ./gradlew :app:assembleDebug
- name: Authenticate to Google Cloud for Firebase Test Lab - name: Authenticate to Google Cloud for Firebase Test Lab
@ -464,11 +479,13 @@ jobs:
# This first environment variable is used by Flank, since the temporary token is missing the project name # This first environment variable is used by Flank, since the temporary token is missing the project name
GOOGLE_CLOUD_PROJECT: ${{ vars.FIREBASE_TEST_LAB_PROJECT }} 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_FIREBASE_TEST_LAB_API_KEY_PATH: ${{ steps.auth_test_lab.outputs.credentials_file_path }}
ORG_GRADLE_PROJECT_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
run: | run: |
./gradlew :app:runFlankSanityConfigDebug ./gradlew :app:runFlankSanityConfigDebug
build: build:
needs: validate_gradle_wrapper if: needs.check_coinbase_secrets.outputs.has-secrets == 'true'
needs: [validate_gradle_wrapper, check_coinbase_secrets]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@ -515,6 +532,7 @@ jobs:
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: android ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: android
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS: androiddebugkey ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS: androiddebugkey
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS_PASSWORD: android ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS_PASSWORD: android
ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }}
run: | run: |
./gradlew :app:assembleDebug :app:bundleRelease :app:packageZcashmainnetReleaseUniversalApk ./gradlew :app:assembleDebug :app:bundleRelease :app:packageZcashmainnetReleaseUniversalApk
- name: Collect Artifacts - name: Collect Artifacts
@ -538,8 +556,8 @@ jobs:
# Performs a button mash test on the release build of the app # Performs a button mash test on the release build of the app
test_robo_release: test_robo_release:
if: needs.check_firebase_secrets.outputs.has-secrets == 'true' if: needs.check_firebase_secrets.outputs.has-secrets == 'true' && needs.check_coinbase_secrets.outputs.has-secrets == 'true'
needs: [build, check_firebase_secrets] needs: [build, check_firebase_secrets, check_coinbase_secrets]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: read packages: read
@ -579,6 +597,7 @@ jobs:
# This first environment variable is used by Flank, since the temporary token is missing the project name # This first environment variable is used by Flank, since the temporary token is missing the project name
GOOGLE_CLOUD_PROJECT: ${{ vars.FIREBASE_TEST_LAB_PROJECT }} 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_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 }}
run: | run: |
unzip ${BINARIES_ZIP_PATH} unzip ${BINARIES_ZIP_PATH}
./gradlew :app:runFlankSanityConfigRelease ./gradlew :app:runFlankSanityConfigRelease

View File

@ -10,6 +10,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
- Transaction resubmission feature has been added. It periodically searches for unmined sent transactions that - Transaction resubmission feature has been added. It periodically searches for unmined sent transactions that
are still within their expiry window and resubmits them if there are any. are still within their expiry window and resubmits them if there are any.
- The Choose server screen now provides a new search for the three fastest servers feature - The Choose server screen now provides a new search for the three fastest servers feature
- Coinbase Onramp integration button has been added to the Advanced Settings screen
### Changed ### Changed
- Choose server screen has been redesigned - Choose server screen has been redesigned

View File

@ -121,6 +121,7 @@ tasks {
"ZCASH_GOOGLE_PLAY_DEPLOY_TRACK" to "internal", "ZCASH_GOOGLE_PLAY_DEPLOY_TRACK" to "internal",
"ZCASH_GOOGLE_PLAY_DEPLOY_STATUS" to "draft", "ZCASH_GOOGLE_PLAY_DEPLOY_STATUS" to "draft",
"ZCASH_COINBASE_APP_ID" to "",
"SDK_INCLUDED_BUILD_PATH" to "", "SDK_INCLUDED_BUILD_PATH" to "",
"BIP_39_INCLUDED_BUILD_PATH" to "" "BIP_39_INCLUDED_BUILD_PATH" to ""
) )
@ -192,12 +193,16 @@ fladle {
flankVersion.set(libs.versions.flank.get()) flankVersion.set(libs.versions.flank.get())
filesToDownload.set(listOf( filesToDownload.set(
".*/matrix_.*/.*test_results_merged\\.xml", listOf(
".*/matrix_.*/.*/artifacts/sdcard/googletest/test_outputfiles/.*\\.png" ".*/matrix_.*/.*test_results_merged\\.xml",
)) ".*/matrix_.*/.*/artifacts/sdcard/googletest/test_outputfiles/.*\\.png"
)
)
directoriesToPull.set(listOf( directoriesToPull.set(
"/sdcard/googletest/test_outputfiles" listOf(
)) "/sdcard/googletest/test_outputfiles"
)
)
} }

View File

@ -13,6 +13,7 @@ directly impact users rather than highlighting other key architectural updates.*
- Transaction resubmission feature has been added. It periodically searches for unmined sent transactions that - Transaction resubmission feature has been added. It periodically searches for unmined sent transactions that
are still within their expiry window and resubmits them if there are any. are still within their expiry window and resubmits them if there are any.
- The Choose server screen now provides a new search for the three fastest servers feature - The Choose server screen now provides a new search for the three fastest servers feature
- Coinbase Onramp integration button has been added to the Advanced Settings screen
### Changed ### Changed
- Choose server screen has been redesigned - Choose server screen has been redesigned

View File

@ -84,6 +84,10 @@ IS_SECURE_SCREEN_PROTECTION_ACTIVE=true
# Set whether the screen rotation is enabled or the screen orientation is locked in the portrait mode. # Set whether the screen rotation is enabled or the screen orientation is locked in the portrait mode.
IS_SCREEN_ROTATION_ENABLED=false IS_SCREEN_ROTATION_ENABLED=false
# Set the Coinbase app project ID to test the Coinbase Onramp integrations locally. Keep it empty as our CI actions
# set it up.
ZCASH_COINBASE_APP_ID=
# Set keystore details to enable build signing. Typically these # Set keystore details to enable build signing. Typically these
# are overridden via ~/.gradle/gradle.properties to allow secure injection. # 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 # Debug keystore is useful if using Google Maps or Firebase, which require API keys to be linked
@ -184,6 +188,7 @@ ANDROIDX_STARTUP_VERSION=1.1.1
ANDROIDX_TEST_SERVICE_VERSION=1.4.2 ANDROIDX_TEST_SERVICE_VERSION=1.4.2
ANDROIDX_UI_AUTOMATOR_VERSION=2.3.0 ANDROIDX_UI_AUTOMATOR_VERSION=2.3.0
ANDROIDX_WORK_MANAGER_VERSION=2.9.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.0.4
FIREBASE_BOM_VERSION_MATCHER=32.8.1 FIREBASE_BOM_VERSION_MATCHER=32.8.1
GOOGLE_AUTH_LIB_JAVA_VERSION=1.18.0 GOOGLE_AUTH_LIB_JAVA_VERSION=1.18.0

View File

@ -167,6 +167,7 @@ dependencyResolutionManagement {
val androidxTestRunnerVersion = extra["ANDROIDX_TEST_RUNNER_VERSION"].toString() val androidxTestRunnerVersion = extra["ANDROIDX_TEST_RUNNER_VERSION"].toString()
val androidxUiAutomatorVersion = extra["ANDROIDX_UI_AUTOMATOR_VERSION"].toString() val androidxUiAutomatorVersion = extra["ANDROIDX_UI_AUTOMATOR_VERSION"].toString()
val androidxWorkManagerVersion = extra["ANDROIDX_WORK_MANAGER_VERSION"].toString() val androidxWorkManagerVersion = extra["ANDROIDX_WORK_MANAGER_VERSION"].toString()
val androidxBrowserVersion = extra["ANDROIDX_BROWSER_VERSION"].toString()
val coreLibraryDesugaringVersion = extra["CORE_LIBRARY_DESUGARING_VERSION"].toString() val coreLibraryDesugaringVersion = extra["CORE_LIBRARY_DESUGARING_VERSION"].toString()
val flankVersion = extra["FLANK_VERSION"].toString() val flankVersion = extra["FLANK_VERSION"].toString()
val jacocoVersion = extra["JACOCO_VERSION"].toString() val jacocoVersion = extra["JACOCO_VERSION"].toString()
@ -220,6 +221,7 @@ dependencyResolutionManagement {
library("androidx-startup", "androidx.startup:startup-runtime:$androidxStartupVersion") library("androidx-startup", "androidx.startup:startup-runtime:$androidxStartupVersion")
library("androidx-viewmodel-compose", "androidx.lifecycle:lifecycle-viewmodel-compose:$androidxLifecycleVersion") library("androidx-viewmodel-compose", "androidx.lifecycle:lifecycle-viewmodel-compose:$androidxLifecycleVersion")
library("androidx-workmanager", "androidx.work:work-runtime-ktx:$androidxWorkManagerVersion") library("androidx-workmanager", "androidx.work:work-runtime-ktx:$androidxWorkManagerVersion")
library("androidx-browser", "androidx.browser:browser:$androidxBrowserVersion")
library("desugaring", "com.android.tools:desugar_jdk_libs:$coreLibraryDesugaringVersion") library("desugaring", "com.android.tools:desugar_jdk_libs:$coreLibraryDesugaringVersion")
library("firebase-bom", "com.google.firebase:firebase-bom:${extra["FIREBASE_BOM_VERSION_MATCHER"]}") library("firebase-bom", "com.google.firebase:firebase-bom:${extra["FIREBASE_BOM_VERSION_MATCHER"]}")
library("firebase-installations", "com.google.firebase", "firebase-installations").withoutVersion() library("firebase-installations", "com.google.firebase", "firebase-installations").withoutVersion()

View File

@ -25,8 +25,28 @@ import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.design.R import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.orDark import co.electriccoin.zcash.ui.design.util.orDark
@Composable
fun ZashiSettingsListItem(
state: ButtonState,
@DrawableRes icon: Int,
trailing: @Composable () -> Unit = {
Image(
painter = painterResource(R.drawable.ic_chevron_right orDark R.drawable.ic_chevron_right_dark),
contentDescription = state.text.getValue(),
)
}
) {
ZashiSettingsListItem(
text = state.text.getValue(),
icon = icon,
trailing = trailing,
onClick = state.onClick
)
}
@Composable @Composable
fun ZashiSettingsListItem( fun ZashiSettingsListItem(
text: String, text: String,

View File

@ -74,6 +74,14 @@ androidComponents {
comment = "Whether is the SecureScreen sensitive data protection enabled" comment = "Whether is the SecureScreen sensitive data protection enabled"
) )
) )
variant.buildConfigFields.put(
"ZCASH_COINBASE_APP_ID",
BuildConfigField(
type = "String",
value = "\"${project.property("ZCASH_COINBASE_APP_ID")?.toString().orEmpty()}\"",
comment = "App ID of the Coinbase Onramp integration"
)
)
// To configure screen orientation in runtime // To configure screen orientation in runtime
variant.buildConfigFields.put( variant.buildConfigFields.put(
"IS_SCREEN_ROTATION_ENABLED", "IS_SCREEN_ROTATION_ENABLED",
@ -96,6 +104,7 @@ dependencies {
implementation(libs.androidx.splash) implementation(libs.androidx.splash)
implementation(libs.androidx.workmanager) implementation(libs.androidx.workmanager)
api(libs.bundles.androidx.biometric) api(libs.bundles.androidx.biometric)
implementation(libs.androidx.browser)
implementation(libs.bundles.androidx.camera) implementation(libs.bundles.androidx.camera)
implementation(libs.bundles.androidx.compose.core) implementation(libs.bundles.androidx.compose.core)
implementation(libs.bundles.androidx.compose.extended) implementation(libs.bundles.androidx.compose.extended)

View File

@ -2,6 +2,7 @@ package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module import org.koin.dsl.module
@ -9,4 +10,5 @@ val providerModule =
module { module {
factoryOf(::GetDefaultServersProvider) factoryOf(::GetDefaultServersProvider)
factoryOf(::GetVersionInfoProvider) factoryOf(::GetVersionInfoProvider)
factoryOf(::GetZcashCurrencyProvider)
} }

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase
@ -27,4 +28,5 @@ val useCaseModule =
singleOf(::GetSelectedEndpointUseCase) singleOf(::GetSelectedEndpointUseCase)
singleOf(::ObserveConfigurationUseCase) singleOf(::ObserveConfigurationUseCase)
singleOf(::RescanBlockchainUseCase) singleOf(::RescanBlockchainUseCase)
singleOf(::GetTransparentAddressUseCase)
} }

View File

@ -0,0 +1,10 @@
package co.electriccoin.zcash.ui.common.provider
import android.app.Application
import cash.z.ecc.sdk.type.ZcashCurrency
class GetZcashCurrencyProvider(private val application: Application) {
operator fun invoke() = ZcashCurrency.fromResources(application)
fun getLocalizedName() = ZcashCurrency.getLocalizedName(application)
}

View File

@ -6,10 +6,12 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletCoordinator import cash.z.ecc.android.sdk.WalletCoordinator
import cash.z.ecc.android.sdk.model.FastestServersResult import cash.z.ecc.android.sdk.model.FastestServersResult
import cash.z.ecc.android.sdk.model.PersistableWallet import cash.z.ecc.android.sdk.model.PersistableWallet
import cash.z.ecc.android.sdk.model.WalletAddresses
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.StandardPreferenceProvider import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.model.FastestServersState import co.electriccoin.zcash.ui.common.model.FastestServersState
import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
@ -48,6 +50,7 @@ interface WalletRepository {
val secretState: StateFlow<SecretState?> val secretState: StateFlow<SecretState?>
val fastestServers: StateFlow<FastestServersState> val fastestServers: StateFlow<FastestServersState>
val persistableWallet: Flow<PersistableWallet?> val persistableWallet: Flow<PersistableWallet?>
val addresses: StateFlow<WalletAddresses?>
fun persistWallet(persistableWallet: PersistableWallet) fun persistWallet(persistableWallet: PersistableWallet)
@ -160,6 +163,21 @@ class WalletRepositoryImpl(
(it as? SecretState.Ready?)?.persistableWallet (it as? SecretState.Ready?)?.persistableWallet
} }
override val addresses: StateFlow<WalletAddresses?> =
synchronizer
.filterNotNull()
.map {
runCatching {
WalletAddresses.new(it)
}.onFailure {
Twig.warn { "Wait until the SDK starts providing the addresses" }
}.getOrNull()
}.stateIn(
scope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null
)
/** /**
* Persists a wallet asynchronously. Clients observe [secretState] to see the side effects. * Persists a wallet asynchronously. Clients observe [secretState] to see the side effects.
*/ */

View File

@ -0,0 +1,12 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.repository.WalletRepository
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
class GetTransparentAddressUseCase(
private val walletRepository: WalletRepository
) {
suspend operator fun invoke() = walletRepository.addresses.filterNotNull().map { it.transparent }.first()
}

View File

@ -223,20 +223,7 @@ class WalletViewModel(
null null
) )
val addresses: StateFlow<WalletAddresses?> = val addresses: StateFlow<WalletAddresses?> = walletRepository.addresses
synchronizer
.filterNotNull()
.map {
runCatching {
WalletAddresses.new(it)
}.onFailure {
Twig.warn { "Wait until the SDK starts providing the addresses" }
}.getOrNull()
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null
)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val transactionHistoryState = val transactionHistoryState =

View File

@ -1,5 +1,7 @@
package co.electriccoin.zcash.ui.screen.advancedsettings package co.electriccoin.zcash.ui.screen.advancedsettings
import co.electriccoin.zcash.ui.design.component.ButtonState
data class AdvancedSettingsState( data class AdvancedSettingsState(
val onBack: () -> Unit, val onBack: () -> Unit,
val onRecoveryPhraseClick: () -> Unit, val onRecoveryPhraseClick: () -> Unit,
@ -7,4 +9,5 @@ data class AdvancedSettingsState(
val onChooseServerClick: () -> Unit, val onChooseServerClick: () -> Unit,
val onCurrencyConversionClick: () -> Unit, val onCurrencyConversionClick: () -> Unit,
val onDeleteZashiClick: () -> Unit, val onDeleteZashiClick: () -> Unit,
val coinbaseButton: ButtonState?,
) )

View File

@ -2,11 +2,14 @@
package co.electriccoin.zcash.ui.screen.advancedsettings package co.electriccoin.zcash.ui.screen.advancedsettings
import android.net.Uri
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.compose.LocalNavController import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings
@ -20,6 +23,7 @@ internal fun WrapAdvancedSettings(
goExportPrivateData: () -> Unit, goExportPrivateData: () -> Unit,
goSeedRecovery: () -> Unit, goSeedRecovery: () -> Unit,
) { ) {
val activity = LocalActivity.current
val navController = LocalNavController.current val navController = LocalNavController.current
val walletViewModel = koinActivityViewModel<WalletViewModel>() val walletViewModel = koinActivityViewModel<WalletViewModel>()
val viewModel = koinViewModel<AdvancedSettingsViewModel>() val viewModel = koinViewModel<AdvancedSettingsViewModel>()
@ -41,6 +45,18 @@ internal fun WrapAdvancedSettings(
} }
} }
LaunchedEffect(Unit) {
viewModel.coinbaseNavigationCommand.collect { uri ->
val intent =
CustomTabsIntent.Builder()
.setUrlBarHidingEnabled(true)
.setShowTitle(true)
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
.build()
intent.launchUrl(activity, Uri.parse(uri))
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.backNavigationCommand.collect { viewModel.backNavigationCommand.collect {
navController.popBackStack() navController.popBackStack()

View File

@ -26,12 +26,14 @@ import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.orDark import co.electriccoin.zcash.ui.design.util.orDark
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsTag import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsTag
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButton import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButton
@ -92,6 +94,13 @@ fun AdvancedSettings(
R.drawable.ic_advanced_settings_currency_conversion_dark, R.drawable.ic_advanced_settings_currency_conversion_dark,
onClick = state.onCurrencyConversionClick onClick = state.onCurrencyConversionClick
) )
if (state.coinbaseButton != null) {
HorizontalDivider(color = ZcashTheme.zashiColors.divider)
ZashiSettingsListItem(
icon = R.drawable.ic_advanced_settings_coinbase,
state = state.coinbaseButton
)
}
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Row( Row(
@ -161,6 +170,11 @@ private fun AdvancedSettingsPreview() =
onChooseServerClick = {}, onChooseServerClick = {},
onCurrencyConversionClick = {}, onCurrencyConversionClick = {},
onDeleteZashiClick = {}, onDeleteZashiClick = {},
coinbaseButton =
ButtonState(
text = stringRes("Coinbase"),
onClick = {}
)
), ),
topAppBarSubTitleState = TopAppBarSubTitleState.None, topAppBarSubTitleState = TopAppBarSubTitleState.None,
) )

View File

@ -2,16 +2,28 @@ package co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.NavigationTargets import co.electriccoin.zcash.ui.NavigationTargets
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AdvancedSettingsViewModel : ViewModel() { class AdvancedSettingsViewModel(
val state: StateFlow<AdvancedSettingsState> = getVersionInfo: GetVersionInfoProvider,
getZcashCurrency: GetZcashCurrencyProvider,
private val getTransparentAddress: GetTransparentAddressUseCase,
) : ViewModel() {
private val forceShowCoinbaseForDebug = getVersionInfo().let { it.isDebuggable && !it.isRunningUnderTestService }
val state =
MutableStateFlow( MutableStateFlow(
AdvancedSettingsState( AdvancedSettingsState(
onBack = ::onBack, onBack = ::onBack,
@ -19,12 +31,23 @@ class AdvancedSettingsViewModel : ViewModel() {
onExportPrivateDataClick = {}, onExportPrivateDataClick = {},
onChooseServerClick = ::onChooseServerClick, onChooseServerClick = ::onChooseServerClick,
onCurrencyConversionClick = ::onCurrencyConversionClick, onCurrencyConversionClick = ::onCurrencyConversionClick,
onDeleteZashiClick = {} onDeleteZashiClick = {},
coinbaseButton =
ButtonState(
// Set the wallet currency by app build is more future-proof, although we hide it from the UI
// in the Testnet build
text = stringRes(R.string.advanced_settings_coinbase, getZcashCurrency.getLocalizedName()),
onClick = { onBuyWithCoinbaseClicked() }
).takeIf {
!getVersionInfo().isTestnet &&
(BuildConfig.ZCASH_COINBASE_APP_ID.isNotEmpty() || forceShowCoinbaseForDebug)
}
) )
).asStateFlow() ).asStateFlow()
val navigationCommand = MutableSharedFlow<String>() val navigationCommand = MutableSharedFlow<String>()
val backNavigationCommand = MutableSharedFlow<Unit>() val backNavigationCommand = MutableSharedFlow<Unit>()
val coinbaseNavigationCommand = MutableSharedFlow<String>()
private fun onChooseServerClick() = private fun onChooseServerClick() =
viewModelScope.launch { viewModelScope.launch {
@ -36,6 +59,32 @@ class AdvancedSettingsViewModel : ViewModel() {
navigationCommand.emit(NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN) navigationCommand.emit(NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN)
} }
private fun onBuyWithCoinbaseClicked() {
viewModelScope.launch {
val appId = BuildConfig.ZCASH_COINBASE_APP_ID
when {
appId.isEmpty() && forceShowCoinbaseForDebug ->
coinbaseNavigationCommand.emit("https://www.coinbase.com") // fallback debug url
appId.isEmpty() && forceShowCoinbaseForDebug -> {
// should not happen
}
appId.isNotEmpty() -> {
val address = getTransparentAddress().address
val url =
"https://pay.coinbase.com/buy/select-asset?appId=$appId&addresses={\"${address}\":[\"zcash\"]}"
coinbaseNavigationCommand.emit(url)
}
else -> {
// should not happen
}
}
}
}
fun onBack() = fun onBack() =
viewModelScope.launch { viewModelScope.launch {
backNavigationCommand.emit(Unit) backNavigationCommand.emit(Unit)

View File

@ -94,7 +94,7 @@ class SettingsViewModel(
combine(isLoading, troubleshootingState) { isLoading, troubleshootingState -> combine(isLoading, troubleshootingState) { isLoading, troubleshootingState ->
SettingsState( SettingsState(
isLoading = isLoading, isLoading = isLoading,
version = stringRes(R.string.settings_version, getVersionInfo().versionName), version = stringRes(R.string.settings_version, versionInfo.versionName),
settingsTroubleshootingState = troubleshootingState, settingsTroubleshootingState = troubleshootingState,
onBack = ::onBack, onBack = ::onBack,
onAdvancedSettingsClick = ::onAdvancedSettingsClick, onAdvancedSettingsClick = ::onAdvancedSettingsClick,

View File

@ -4,7 +4,7 @@
<string name="advanced_settings_export">Export Private Data</string> <string name="advanced_settings_export">Export Private Data</string>
<string name="advanced_settings_choose_server">Choose a Server</string> <string name="advanced_settings_choose_server">Choose a Server</string>
<string name="advanced_settings_currency_conversion">Currency Conversion</string> <string name="advanced_settings_currency_conversion">Currency Conversion</string>
<string name="advanced_settings_coinbase">Buy ZEC with Coinbase</string> <string name="advanced_settings_coinbase">Buy <xliff:g id="currency" example="ZEC">%1$s</xliff:g> with Coinbase</string>
<string name="advanced_settings_info">You will be asked to confirm on the next screen</string> <string name="advanced_settings_info">You will be asked to confirm on the next screen</string>
<string name="advanced_settings_delete_button">Delete Zashi</string> <string name="advanced_settings_delete_button">Delete Zashi</string>
</resources> </resources>