diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 143c511d..661f920b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -63,9 +63,11 @@ jobs: # GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER }} GOOGLE_PLAY_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_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 != '' && 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 @@ -154,6 +156,7 @@ jobs: 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_PASSWORD: ${{ secrets.UPLOAD_KEY_ALIAS_PASSWORD }} + ORG_GRADLE_PROJECT_ZCASH_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} run: | ./gradlew :app:publishToGooglePlay - name: Collect Artifacts diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6fad130f..ae0c60a1 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -63,6 +63,17 @@ jobs: if: "${{ env.EMULATOR_WTF_API_KEY != '' }}" 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: needs: validate_gradle_wrapper runs-on: ubuntu-latest @@ -252,8 +263,8 @@ jobs: # Emulator.wtf is preferred if it has an API key. test_android_modules_ftl: - if: needs.check_firebase_secrets.outputs.has-secrets == 'true' && needs.check_emulator_wtf_secrets.outputs.has-secrets == 'false' - needs: [validate_gradle_wrapper, check_firebase_secrets, check_emulator_wtf_secrets] + 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, check_coinbase_secrets] runs-on: ubuntu-latest permissions: 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 }} # 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 }} run: | ./gradlew runFlank - name: Collect Artifacts @@ -316,8 +328,8 @@ jobs: path: ~/artifacts test_android_modules_wtf_coverage: - if: needs.check_emulator_wtf_secrets.outputs.has-secrets == 'true' - needs: [ validate_gradle_wrapper, check_emulator_wtf_secrets ] + 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, check_coinbase_secrets ] runs-on: ubuntu-latest permissions: contents: read @@ -342,6 +354,7 @@ jobs: ORG_GRADLE_PROJECT_ZCASH_DEBUG_APP_NAME_SUFFIX: "" 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 }} run: | ./gradlew testDebugWithEmulatorWtf :ui-integration-test:testZcashmainnetDebugWithEmulatorWtf - name: Collect Artifacts @@ -363,8 +376,8 @@ jobs: path: ~/artifacts test_android_modules_wtf_no_coverage: - if: needs.check_emulator_wtf_secrets.outputs.has-secrets == 'true' - needs: [ validate_gradle_wrapper, check_emulator_wtf_secrets ] + 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, check_coinbase_secrets ] runs-on: ubuntu-latest permissions: contents: read @@ -389,6 +402,7 @@ jobs: ORG_GRADLE_PROJECT_ZCASH_DEBUG_APP_NAME_SUFFIX: "" 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 }} run: | ./gradlew :app:testZcashmainnetDebugWithEmulatorWtf :ui-screenshot-test:testZcashmainnetDebugWithEmulatorWtf - name: Collect Artifacts @@ -411,8 +425,8 @@ jobs: # Performs a button mash test on the debug build of the app with strict mode enabled test_robo_debug: - if: needs.check_firebase_secrets.outputs.has-secrets == 'true' - needs: [check_firebase_secrets] + if: needs.check_firebase_secrets.outputs.has-secrets == 'true' && needs.check_coinbase_secrets.outputs.has-secrets == 'true' + needs: [check_firebase_secrets, check_coinbase_secrets] runs-on: ubuntu-latest permissions: packages: read @@ -447,6 +461,7 @@ jobs: env: 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 }} run: | ./gradlew :app:assembleDebug - 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 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_COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} run: | ./gradlew :app:runFlankSanityConfigDebug 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 permissions: contents: read @@ -515,6 +532,7 @@ jobs: ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: android 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 }} run: | ./gradlew :app:assembleDebug :app:bundleRelease :app:packageZcashmainnetReleaseUniversalApk - name: Collect Artifacts @@ -538,8 +556,8 @@ jobs: # Performs a button mash test on the release build of the app test_robo_release: - if: needs.check_firebase_secrets.outputs.has-secrets == 'true' - needs: [build, check_firebase_secrets] + if: needs.check_firebase_secrets.outputs.has-secrets == 'true' && needs.check_coinbase_secrets.outputs.has-secrets == 'true' + needs: [build, check_firebase_secrets, check_coinbase_secrets] runs-on: ubuntu-latest permissions: packages: read @@ -579,6 +597,7 @@ jobs: # 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 }} 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: | unzip ${BINARIES_ZIP_PATH} ./gradlew :app:runFlankSanityConfigRelease diff --git a/CHANGELOG.md b/CHANGELOG.md index 03de4121..8fc2f82e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 +- Coinbase Onramp integration button has been added to the Advanced Settings screen ### Changed - Choose server screen has been redesigned diff --git a/build.gradle.kts b/build.gradle.kts index 80d4fc5d..7ec2f6e4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -121,6 +121,7 @@ tasks { "ZCASH_GOOGLE_PLAY_DEPLOY_TRACK" to "internal", "ZCASH_GOOGLE_PLAY_DEPLOY_STATUS" to "draft", + "ZCASH_COINBASE_APP_ID" to "", "SDK_INCLUDED_BUILD_PATH" to "", "BIP_39_INCLUDED_BUILD_PATH" to "" ) @@ -192,12 +193,16 @@ fladle { flankVersion.set(libs.versions.flank.get()) - filesToDownload.set(listOf( - ".*/matrix_.*/.*test_results_merged\\.xml", - ".*/matrix_.*/.*/artifacts/sdcard/googletest/test_outputfiles/.*\\.png" - )) + filesToDownload.set( + listOf( + ".*/matrix_.*/.*test_results_merged\\.xml", + ".*/matrix_.*/.*/artifacts/sdcard/googletest/test_outputfiles/.*\\.png" + ) + ) - directoriesToPull.set(listOf( - "/sdcard/googletest/test_outputfiles" - )) + directoriesToPull.set( + listOf( + "/sdcard/googletest/test_outputfiles" + ) + ) } diff --git a/docs/whatsNew/WHATS_NEW_EN.md b/docs/whatsNew/WHATS_NEW_EN.md index a13503ca..17620baf 100644 --- a/docs/whatsNew/WHATS_NEW_EN.md +++ b/docs/whatsNew/WHATS_NEW_EN.md @@ -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 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 +- Coinbase Onramp integration button has been added to the Advanced Settings screen ### Changed - Choose server screen has been redesigned diff --git a/gradle.properties b/gradle.properties index a9bce7e2..0373e475 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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. 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 # 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 @@ -184,6 +188,7 @@ ANDROIDX_STARTUP_VERSION=1.1.1 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 FIREBASE_BOM_VERSION_MATCHER=32.8.1 GOOGLE_AUTH_LIB_JAVA_VERSION=1.18.0 diff --git a/settings.gradle.kts b/settings.gradle.kts index 04c97daa..4ad4bd43 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -167,6 +167,7 @@ dependencyResolutionManagement { val androidxTestRunnerVersion = extra["ANDROIDX_TEST_RUNNER_VERSION"].toString() val androidxUiAutomatorVersion = extra["ANDROIDX_UI_AUTOMATOR_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 flankVersion = extra["FLANK_VERSION"].toString() val jacocoVersion = extra["JACOCO_VERSION"].toString() @@ -220,6 +221,7 @@ dependencyResolutionManagement { library("androidx-startup", "androidx.startup:startup-runtime:$androidxStartupVersion") library("androidx-viewmodel-compose", "androidx.lifecycle:lifecycle-viewmodel-compose:$androidxLifecycleVersion") 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("firebase-bom", "com.google.firebase:firebase-bom:${extra["FIREBASE_BOM_VERSION_MATCHER"]}") library("firebase-installations", "com.google.firebase", "firebase-installations").withoutVersion() 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 f48542c5..1420f21d 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 @@ -25,8 +25,28 @@ import androidx.compose.ui.unit.sp import co.electriccoin.zcash.ui.design.R import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.util.getValue 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 fun ZashiSettingsListItem( text: String, diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index 14d1cc3d..6424d4ff 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -74,6 +74,14 @@ androidComponents { 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 variant.buildConfigFields.put( "IS_SCREEN_ROTATION_ENABLED", @@ -96,6 +104,7 @@ dependencies { implementation(libs.androidx.splash) implementation(libs.androidx.workmanager) api(libs.bundles.androidx.biometric) + implementation(libs.androidx.browser) implementation(libs.bundles.androidx.camera) implementation(libs.bundles.androidx.compose.core) implementation(libs.bundles.androidx.compose.extended) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ProviderModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ProviderModule.kt index 36a1b320..12e98eeb 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ProviderModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ProviderModule.kt @@ -2,6 +2,7 @@ package co.electriccoin.zcash.di import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider 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.dsl.module @@ -9,4 +10,5 @@ val providerModule = module { factoryOf(::GetDefaultServersProvider) factoryOf(::GetVersionInfoProvider) + factoryOf(::GetZcashCurrencyProvider) } 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 1fe92ad6..e314b84f 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 @@ -3,6 +3,7 @@ package co.electriccoin.zcash.di import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase 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.ObserveFastestServersUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase @@ -27,4 +28,5 @@ val useCaseModule = singleOf(::GetSelectedEndpointUseCase) singleOf(::ObserveConfigurationUseCase) singleOf(::RescanBlockchainUseCase) + singleOf(::GetTransparentAddressUseCase) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/GetZcashCurrencyProvider.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/GetZcashCurrencyProvider.kt new file mode 100644 index 00000000..ca3074a8 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/GetZcashCurrencyProvider.kt @@ -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) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/WalletRepository.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/WalletRepository.kt index 4f4e7fed..60f833d2 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/WalletRepository.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/WalletRepository.kt @@ -6,10 +6,12 @@ import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.WalletCoordinator import cash.z.ecc.android.sdk.model.FastestServersResult 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 co.electriccoin.lightwallet.client.model.LightWalletEndpoint import co.electriccoin.zcash.preference.EncryptedPreferenceProvider 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.OnboardingState import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider @@ -48,6 +50,7 @@ interface WalletRepository { val secretState: StateFlow val fastestServers: StateFlow val persistableWallet: Flow + val addresses: StateFlow fun persistWallet(persistableWallet: PersistableWallet) @@ -160,6 +163,21 @@ class WalletRepositoryImpl( (it as? SecretState.Ready?)?.persistableWallet } + override val addresses: StateFlow = + 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. */ diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetTransparentAddressUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetTransparentAddressUseCase.kt new file mode 100644 index 00000000..9d2261bb --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetTransparentAddressUseCase.kt @@ -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() +} 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 4a57d42d..5cb23bca 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 @@ -223,20 +223,7 @@ class WalletViewModel( null ) - val addresses: StateFlow = - 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 - ) + val addresses: StateFlow = walletRepository.addresses @OptIn(ExperimentalCoroutinesApi::class) val transactionHistoryState = diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/AdvancedSettingsState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/AdvancedSettingsState.kt index 0778da0f..9f288092 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/AdvancedSettingsState.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/AdvancedSettingsState.kt @@ -1,5 +1,7 @@ package co.electriccoin.zcash.ui.screen.advancedsettings +import co.electriccoin.zcash.ui.design.component.ButtonState + data class AdvancedSettingsState( val onBack: () -> Unit, val onRecoveryPhraseClick: () -> Unit, @@ -7,4 +9,5 @@ data class AdvancedSettingsState( val onChooseServerClick: () -> Unit, val onCurrencyConversionClick: () -> Unit, val onDeleteZashiClick: () -> Unit, + val coinbaseButton: ButtonState?, ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/AndroidAdvancedSettings.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/AndroidAdvancedSettings.kt index 04f8cb48..022de5a7 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/AndroidAdvancedSettings.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/AndroidAdvancedSettings.kt @@ -2,11 +2,14 @@ package co.electriccoin.zcash.ui.screen.advancedsettings +import android.net.Uri import androidx.activity.compose.BackHandler +import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings @@ -20,6 +23,7 @@ internal fun WrapAdvancedSettings( goExportPrivateData: () -> Unit, goSeedRecovery: () -> Unit, ) { + val activity = LocalActivity.current val navController = LocalNavController.current val walletViewModel = koinActivityViewModel() val viewModel = koinViewModel() @@ -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) { viewModel.backNavigationCommand.collect { navController.popBackStack() diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/view/AdvancedSettingsView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/view/AdvancedSettingsView.kt index c10f2ead..12707750 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/view/AdvancedSettingsView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/view/AdvancedSettingsView.kt @@ -26,12 +26,14 @@ import androidx.compose.ui.unit.sp import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState 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.ZashiSmallTopAppBar import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.theme.ZcashTheme 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.AdvancedSettingsTag import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButton @@ -92,6 +94,13 @@ fun AdvancedSettings( R.drawable.ic_advanced_settings_currency_conversion_dark, 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.weight(1f)) Row( @@ -161,6 +170,11 @@ private fun AdvancedSettingsPreview() = onChooseServerClick = {}, onCurrencyConversionClick = {}, onDeleteZashiClick = {}, + coinbaseButton = + ButtonState( + text = stringRes("Coinbase"), + onClick = {} + ) ), topAppBarSubTitleState = TopAppBarSubTitleState.None, ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/viewmodel/AdvancedSettingsViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/viewmodel/AdvancedSettingsViewModel.kt index c33c2b48..be651667 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/viewmodel/AdvancedSettingsViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/viewmodel/AdvancedSettingsViewModel.kt @@ -2,16 +2,28 @@ package co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import co.electriccoin.zcash.ui.BuildConfig 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 kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -class AdvancedSettingsViewModel : ViewModel() { - val state: StateFlow = +class AdvancedSettingsViewModel( + getVersionInfo: GetVersionInfoProvider, + getZcashCurrency: GetZcashCurrencyProvider, + private val getTransparentAddress: GetTransparentAddressUseCase, +) : ViewModel() { + private val forceShowCoinbaseForDebug = getVersionInfo().let { it.isDebuggable && !it.isRunningUnderTestService } + + val state = MutableStateFlow( AdvancedSettingsState( onBack = ::onBack, @@ -19,12 +31,23 @@ class AdvancedSettingsViewModel : ViewModel() { onExportPrivateDataClick = {}, onChooseServerClick = ::onChooseServerClick, 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() val navigationCommand = MutableSharedFlow() val backNavigationCommand = MutableSharedFlow() + val coinbaseNavigationCommand = MutableSharedFlow() private fun onChooseServerClick() = viewModelScope.launch { @@ -36,6 +59,32 @@ class AdvancedSettingsViewModel : ViewModel() { 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() = viewModelScope.launch { backNavigationCommand.emit(Unit) 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 ad7fce40..50131686 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 @@ -94,7 +94,7 @@ class SettingsViewModel( combine(isLoading, troubleshootingState) { isLoading, troubleshootingState -> SettingsState( isLoading = isLoading, - version = stringRes(R.string.settings_version, getVersionInfo().versionName), + version = stringRes(R.string.settings_version, versionInfo.versionName), settingsTroubleshootingState = troubleshootingState, onBack = ::onBack, onAdvancedSettingsClick = ::onAdvancedSettingsClick, diff --git a/ui-lib/src/main/res/ui/settings/drawable/ic_settings_coinbase.xml b/ui-lib/src/main/res/ui/advanced_settings/drawable/ic_advanced_settings_coinbase.xml similarity index 100% rename from ui-lib/src/main/res/ui/settings/drawable/ic_settings_coinbase.xml rename to ui-lib/src/main/res/ui/advanced_settings/drawable/ic_advanced_settings_coinbase.xml diff --git a/ui-lib/src/main/res/ui/advanced_settings/values/strings.xml b/ui-lib/src/main/res/ui/advanced_settings/values/strings.xml index 7247f850..9c0a294c 100644 --- a/ui-lib/src/main/res/ui/advanced_settings/values/strings.xml +++ b/ui-lib/src/main/res/ui/advanced_settings/values/strings.xml @@ -4,7 +4,7 @@ Export Private Data Choose a Server Currency Conversion - Buy ZEC with Coinbase + Buy %1$s with Coinbase You will be asked to confirm on the next screen Delete Zashi