* Payment Request screen logic update * Scan view model implementation * Zip321 Uri parsing and passing around screens * Pass PaymentRequestArguments * Fixed screens navigation * Screen balances UI part * Address UI part * Address UI part + logic * Memo UI part * Amounts UI part * Add stages and send logic with authentication * Send transaction error handling * Code analysis warnings fix * Tests update * Changelogs * [#1595] QR code design update * Address review comments --------- Co-authored-by: Milan Cerovsky <milan@z.cash>
This commit is contained in:
parent
711feb4251
commit
2129adaa8d
|
@ -15,6 +15,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
|
||||||
- Confirmation screen redesigned & added a contact name to the transaction if the contact is in address book
|
- Confirmation screen redesigned & added a contact name to the transaction if the contact is in address book
|
||||||
- History item redesigned & added an option to create a contact from unknown address
|
- History item redesigned & added an option to create a contact from unknown address
|
||||||
- Address Book, Create/Update/Delete Contact, Create Contact by QR screens added
|
- Address Book, Create/Update/Delete Contact, Create Contact by QR screens added
|
||||||
|
- The Scan QR code screen now supports scanning of ZIP 321 Uris
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Address book local storage support
|
- Address book local storage support
|
||||||
|
|
|
@ -18,6 +18,7 @@ directly impact users rather than highlighting other key architectural updates.*
|
||||||
- Confirmation screen redesigned & added a contact name to the transaction if the contact is in address book
|
- Confirmation screen redesigned & added a contact name to the transaction if the contact is in address book
|
||||||
- History item redesigned & added an option to create a contact from unknown address
|
- History item redesigned & added an option to create a contact from unknown address
|
||||||
- Address Book, Create/Update/Delete Contact, Create Contact by QR screens added
|
- Address Book, Create/Update/Delete Contact, Create Contact by QR screens added
|
||||||
|
- The Scan QR code screen now supports scanning of ZIP 321 Uris
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- New Integrations screen in settings
|
- New Integrations screen in settings
|
||||||
|
|
|
@ -210,7 +210,7 @@ PLAY_PUBLISHER_API_VERSION=v3-rev20231030-2.0.0
|
||||||
PLAY_SERVICES_AUTH_VERSION=21.2.0
|
PLAY_SERVICES_AUTH_VERSION=21.2.0
|
||||||
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
||||||
ZXING_VERSION=3.5.3
|
ZXING_VERSION=3.5.3
|
||||||
ZIP_321_VERSION = 0.0.3
|
ZIP_321_VERSION = 0.0.6
|
||||||
ZCASH_BIP39_VERSION=1.0.8
|
ZCASH_BIP39_VERSION=1.0.8
|
||||||
|
|
||||||
# WARNING: Ensure a non-snapshot version is used before releasing to production
|
# WARNING: Ensure a non-snapshot version is used before releasing to production
|
||||||
|
|
|
@ -83,7 +83,7 @@ fun ZashiButton(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Loading() {
|
override fun Loading() {
|
||||||
if (enabled && isLoading) {
|
if (isLoading) {
|
||||||
LottieProgress(
|
LottieProgress(
|
||||||
loadingRes =
|
loadingRes =
|
||||||
if (isSystemInDarkTheme()) {
|
if (isSystemInDarkTheme()) {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity
|
import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
||||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -36,15 +36,15 @@ class ScanViewIntegrationTest : UiTestPrerequisites() {
|
||||||
testSetup.DefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(testSetup.getScanState(), ScanState.Permission)
|
assertEquals(testSetup.getScanState(), ScanScreenState.Permission)
|
||||||
|
|
||||||
testSetup.grantPermission()
|
testSetup.grantPermission()
|
||||||
|
|
||||||
assertEquals(testSetup.getScanState(), ScanState.Scanning)
|
assertEquals(testSetup.getScanState(), ScanScreenState.Scanning)
|
||||||
|
|
||||||
restorationTester.emulateSavedInstanceStateRestore()
|
restorationTester.emulateSavedInstanceStateRestore()
|
||||||
|
|
||||||
assertEquals(testSetup.getScanState(), ScanState.Scanning)
|
assertEquals(testSetup.getScanState(), ScanScreenState.Scanning)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -15,7 +15,7 @@ import co.electriccoin.zcash.ui.integration.test.common.getStringResource
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.getStringResourceWithArgs
|
import co.electriccoin.zcash.ui.integration.test.common.getStringResourceWithArgs
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.waitForDeviceIdle
|
import co.electriccoin.zcash.ui.integration.test.common.waitForDeviceIdle
|
||||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
@ -62,7 +62,7 @@ class ScanViewTest : UiTestPrerequisites() {
|
||||||
@Test
|
@Test
|
||||||
@LargeTest
|
@LargeTest
|
||||||
fun grant_camera_permission() {
|
fun grant_camera_permission() {
|
||||||
assertEquals(ScanState.Permission, testSetup.getScanState())
|
assertEquals(ScanScreenState.Permission, testSetup.getScanState())
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(ScanTag.CAMERA_VIEW).also {
|
composeTestRule.onNodeWithTag(ScanTag.CAMERA_VIEW).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
|
@ -78,7 +78,7 @@ class ScanViewTest : UiTestPrerequisites() {
|
||||||
it.assertIsDisplayed()
|
it.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(ScanState.Scanning, testSetup.getScanState())
|
assertEquals(ScanScreenState.Scanning, testSetup.getScanState())
|
||||||
|
|
||||||
// we need to actively wait for the camera preview initialization
|
// we need to actively wait for the camera preview initialization
|
||||||
waitForDeviceIdle(timeout = 5000.milliseconds)
|
waitForDeviceIdle(timeout = 5000.milliseconds)
|
||||||
|
@ -91,11 +91,11 @@ class ScanViewTest : UiTestPrerequisites() {
|
||||||
@Test
|
@Test
|
||||||
@LargeTest
|
@LargeTest
|
||||||
fun deny_camera_permission() {
|
fun deny_camera_permission() {
|
||||||
assertEquals(ScanState.Permission, testSetup.getScanState())
|
assertEquals(ScanScreenState.Permission, testSetup.getScanState())
|
||||||
|
|
||||||
testSetup.denyPermission()
|
testSetup.denyPermission()
|
||||||
|
|
||||||
assertEquals(ScanState.Permission, testSetup.getScanState())
|
assertEquals(ScanScreenState.Permission, testSetup.getScanState())
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(ScanTag.CAMERA_VIEW).also {
|
composeTestRule.onNodeWithTag(ScanTag.CAMERA_VIEW).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
|
|
|
@ -3,12 +3,12 @@ package co.electriccoin.zcash.ui.integration.test.screen.scan.view
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
import cash.z.ecc.android.sdk.type.AddressType
|
|
||||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionNegativeButtonUiObject
|
import co.electriccoin.zcash.ui.integration.test.common.getPermissionNegativeButtonUiObject
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||||
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
@ -19,14 +19,14 @@ class ScanViewTestSetup(
|
||||||
private val composeTestRule: ComposeContentTestRule
|
private val composeTestRule: ComposeContentTestRule
|
||||||
) {
|
) {
|
||||||
private val onOpenSettingsCount = AtomicInteger(0)
|
private val onOpenSettingsCount = AtomicInteger(0)
|
||||||
private val scanState = AtomicReference(ScanState.Permission)
|
private val scanState = AtomicReference(ScanScreenState.Permission)
|
||||||
|
|
||||||
fun getOnOpenSettingsCount(): Int {
|
fun getOnOpenSettingsCount(): Int {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
return onOpenSettingsCount.get()
|
return onOpenSettingsCount.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getScanState(): ScanState {
|
fun getScanState(): ScanScreenState {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
return scanState.get()
|
return scanState.get()
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ class ScanViewTestSetup(
|
||||||
scanState.set(it)
|
scanState.set(it)
|
||||||
},
|
},
|
||||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||||
addressValidationResult = AddressType.Unified
|
validationResult = ScanValidationState.VALID
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ android {
|
||||||
"src/main/res/ui/integrations",
|
"src/main/res/ui/integrations",
|
||||||
"src/main/res/ui/new_wallet_recovery",
|
"src/main/res/ui/new_wallet_recovery",
|
||||||
"src/main/res/ui/onboarding",
|
"src/main/res/ui/onboarding",
|
||||||
|
"src/main/res/ui/payment_request",
|
||||||
"src/main/res/ui/qr_code",
|
"src/main/res/ui/qr_code",
|
||||||
"src/main/res/ui/request",
|
"src/main/res/ui/request",
|
||||||
"src/main/res/ui/receive",
|
"src/main/res/ui/receive",
|
||||||
|
|
|
@ -12,7 +12,7 @@ import androidx.test.rule.GrantPermissionRule
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -80,7 +80,7 @@ class ScanViewBasicTest : UiTestPrerequisites() {
|
||||||
fun scan_state() {
|
fun scan_state() {
|
||||||
val testSetup = newTestSetup()
|
val testSetup = newTestSetup()
|
||||||
|
|
||||||
assertEquals(ScanState.Scanning, testSetup.getScanState())
|
assertEquals(ScanScreenState.Scanning, testSetup.getScanState())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newTestSetup() =
|
private fun newTestSetup() =
|
||||||
|
|
|
@ -3,10 +3,10 @@ package co.electriccoin.zcash.ui.screen.scan.view
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
import cash.z.ecc.android.sdk.type.AddressType
|
|
||||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
@ -14,14 +14,14 @@ class ScanViewBasicTestSetup(
|
||||||
private val composeTestRule: ComposeContentTestRule
|
private val composeTestRule: ComposeContentTestRule
|
||||||
) {
|
) {
|
||||||
private val onBackCount = AtomicInteger(0)
|
private val onBackCount = AtomicInteger(0)
|
||||||
private val scanState = AtomicReference(ScanState.Permission)
|
private val scanState = AtomicReference(ScanScreenState.Permission)
|
||||||
|
|
||||||
fun getOnBackCount(): Int {
|
fun getOnBackCount(): Int {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
return onBackCount.get()
|
return onBackCount.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getScanState(): ScanState {
|
fun getScanState(): ScanScreenState {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
return scanState.get()
|
return scanState.get()
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ class ScanViewBasicTestSetup(
|
||||||
@Suppress("TestFunctionName")
|
@Suppress("TestFunctionName")
|
||||||
fun DefaultContent() {
|
fun DefaultContent() {
|
||||||
Scan(
|
Scan(
|
||||||
addressValidationResult = AddressType.Shielded,
|
validationResult = ScanValidationState.VALID,
|
||||||
onBack = {
|
onBack = {
|
||||||
onBackCount.incrementAndGet()
|
onBackCount.incrementAndGet()
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,6 +39,7 @@ class SendViewIntegrationTest {
|
||||||
goBack = {},
|
goBack = {},
|
||||||
goBalances = {},
|
goBalances = {},
|
||||||
goSettings = {},
|
goSettings = {},
|
||||||
|
goPaymentRequest = { _, _ -> },
|
||||||
goSendConfirmation = {},
|
goSendConfirmation = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase
|
import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.Zip321ProposalFromUriUseCase
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
@ -58,9 +60,11 @@ val useCaseModule =
|
||||||
singleOf(::ObserveContactPickedUseCase)
|
singleOf(::ObserveContactPickedUseCase)
|
||||||
singleOf(::GetAddressesUseCase)
|
singleOf(::GetAddressesUseCase)
|
||||||
singleOf(::CopyToClipboardUseCase)
|
singleOf(::CopyToClipboardUseCase)
|
||||||
|
singleOf(::ShareImageUseCase)
|
||||||
|
singleOf(::Zip321BuildUriUseCase)
|
||||||
|
singleOf(::Zip321ProposalFromUriUseCase)
|
||||||
|
singleOf(::Zip321ParseUriValidationUseCase)
|
||||||
singleOf(::ObserveWalletStateUseCase)
|
singleOf(::ObserveWalletStateUseCase)
|
||||||
singleOf(::IsCoinbaseAvailableUseCase)
|
singleOf(::IsCoinbaseAvailableUseCase)
|
||||||
singleOf(::GetSpendingKeyUseCase)
|
singleOf(::GetSpendingKeyUseCase)
|
||||||
singleOf(::ShareImageUseCase)
|
|
||||||
singleOf(::Zip321BuildUriUseCase)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,14 @@ import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel
|
import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.viewmodel.PaymentRequestViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel
|
import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel
|
import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
|
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
|
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel
|
import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
|
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
|
||||||
|
@ -67,6 +70,14 @@ val viewModelModule =
|
||||||
viewModelOf(::UpdateContactViewModel)
|
viewModelOf(::UpdateContactViewModel)
|
||||||
viewModelOf(::ReceiveViewModel)
|
viewModelOf(::ReceiveViewModel)
|
||||||
viewModelOf(::QrCodeViewModel)
|
viewModelOf(::QrCodeViewModel)
|
||||||
viewModelOf(::IntegrationsViewModel)
|
|
||||||
viewModelOf(::RequestViewModel)
|
viewModelOf(::RequestViewModel)
|
||||||
|
viewModelOf(::PaymentRequestViewModel)
|
||||||
|
viewModelOf(::IntegrationsViewModel)
|
||||||
|
viewModel { (args: ScanNavigationArgs) ->
|
||||||
|
ScanViewModel(
|
||||||
|
args = args,
|
||||||
|
getSynchronizer = get(),
|
||||||
|
zip321ParseUriValidationUseCase = get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,18 @@ import co.electriccoin.zcash.spackle.Twig
|
||||||
import co.electriccoin.zcash.spackle.getSerializableCompat
|
import co.electriccoin.zcash.spackle.getSerializableCompat
|
||||||
import co.electriccoin.zcash.ui.NavigationArgs.ADDRESS_TYPE
|
import co.electriccoin.zcash.ui.NavigationArgs.ADDRESS_TYPE
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.MULTIPLE_SUBMISSION_CLEAR_FORM
|
import co.electriccoin.zcash.ui.NavigationArguments.MULTIPLE_SUBMISSION_CLEAR_FORM
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments.PAYMENT_REQUEST_ADDRESS
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments.PAYMENT_REQUEST_AMOUNT
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments.PAYMENT_REQUEST_MEMO
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments.PAYMENT_REQUEST_PROPOSAL
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments.PAYMENT_REQUEST_URI
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_INITIAL_STAGE
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_INITIAL_STAGE
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_MEMO
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_MEMO
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_PROPOSAL
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_PROPOSAL
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_RECIPIENT_ADDRESS
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_RECIPIENT_ADDRESS
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_ZIP_321_URI
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
|
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.CHOOSE_SERVER
|
import co.electriccoin.zcash.ui.NavigationTargets.CHOOSE_SERVER
|
||||||
|
@ -37,6 +43,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.EXPORT_PRIVATE_DATA
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.HOME
|
import co.electriccoin.zcash.ui.NavigationTargets.HOME
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS
|
import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.NOT_ENOUGH_SPACE
|
import co.electriccoin.zcash.ui.NavigationTargets.NOT_ENOUGH_SPACE
|
||||||
|
import co.electriccoin.zcash.ui.NavigationTargets.PAYMENT_REQUEST
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.QR_CODE
|
import co.electriccoin.zcash.ui.NavigationTargets.QR_CODE
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
|
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
|
import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
|
||||||
|
@ -71,6 +78,8 @@ import co.electriccoin.zcash.ui.screen.exchangerate.settings.AndroidSettingsExch
|
||||||
import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData
|
import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData
|
||||||
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
||||||
import co.electriccoin.zcash.ui.screen.integrations.WrapIntegrations
|
import co.electriccoin.zcash.ui.screen.integrations.WrapIntegrations
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.WrapPaymentRequest
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestArguments
|
||||||
import co.electriccoin.zcash.ui.screen.qrcode.WrapQrCode
|
import co.electriccoin.zcash.ui.screen.qrcode.WrapQrCode
|
||||||
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
|
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
|
||||||
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
||||||
|
@ -250,28 +259,7 @@ internal fun MainActivity.Navigation() {
|
||||||
backStackEntry.arguments
|
backStackEntry.arguments
|
||||||
?.getSerializableCompat<ScanNavigationArgs>(ScanNavigationArgs.KEY) ?: ScanNavigationArgs.DEFAULT
|
?.getSerializableCompat<ScanNavigationArgs>(ScanNavigationArgs.KEY) ?: ScanNavigationArgs.DEFAULT
|
||||||
|
|
||||||
WrapScanValidator(
|
WrapScanValidator(args = mode)
|
||||||
onScanValid = { scanResult ->
|
|
||||||
when (mode) {
|
|
||||||
ScanNavigationArgs.DEFAULT -> {
|
|
||||||
navController.previousBackStackEntry?.savedStateHandle?.apply {
|
|
||||||
set(
|
|
||||||
SEND_SCAN_RECIPIENT_ADDRESS,
|
|
||||||
Json.encodeToString(SerializableAddress.serializer(), scanResult)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
navController.popBackStackJustOnce(ScanNavigationArgs.ROUTE)
|
|
||||||
}
|
|
||||||
|
|
||||||
ScanNavigationArgs.ADDRESS_BOOK -> {
|
|
||||||
val address = scanResult.address
|
|
||||||
navController.popBackStack()
|
|
||||||
navController.navigate(AddContactArgs(address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
goBack = { navController.popBackStackJustOnce(ScanNavigationArgs.ROUTE) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable(EXPORT_PRIVATE_DATA) {
|
composable(EXPORT_PRIVATE_DATA) {
|
||||||
WrapExportPrivateData(
|
WrapExportPrivateData(
|
||||||
|
@ -357,6 +345,13 @@ internal fun MainActivity.Navigation() {
|
||||||
val addressType = backStackEntry.arguments?.getInt(ADDRESS_TYPE) ?: ReceiveAddressType.Unified.ordinal
|
val addressType = backStackEntry.arguments?.getInt(ADDRESS_TYPE) ?: ReceiveAddressType.Unified.ordinal
|
||||||
WrapRequest(addressType)
|
WrapRequest(addressType)
|
||||||
}
|
}
|
||||||
|
composable(PAYMENT_REQUEST) {
|
||||||
|
navController.previousBackStackEntry?.let { backStackEntry ->
|
||||||
|
WrapPaymentRequest(
|
||||||
|
arguments = PaymentRequestArguments.fromSavedStateHandle(backStackEntry.savedStateHandle)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,6 +371,12 @@ private fun MainActivity.NavigationHome(
|
||||||
}
|
}
|
||||||
navController.navigateJustOnce(SEND_CONFIRMATION)
|
navController.navigateJustOnce(SEND_CONFIRMATION)
|
||||||
},
|
},
|
||||||
|
goPaymentRequest = { zecSend, zip321Uri ->
|
||||||
|
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
|
||||||
|
fillInHandleForPaymentRequest(handle, zecSend, zip321Uri)
|
||||||
|
}
|
||||||
|
navController.navigateJustOnce(PAYMENT_REQUEST)
|
||||||
|
},
|
||||||
goSettings = { navController.navigateJustOnce(SETTINGS) },
|
goSettings = { navController.navigateJustOnce(SETTINGS) },
|
||||||
goMultiTrxSubmissionFailure = {
|
goMultiTrxSubmissionFailure = {
|
||||||
// Ultimately we could approach reworking the MultipleTrxFailure screen into a separate
|
// Ultimately we could approach reworking the MultipleTrxFailure screen into a separate
|
||||||
|
@ -391,11 +392,13 @@ private fun MainActivity.NavigationHome(
|
||||||
backStack.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
|
backStack.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
|
||||||
Json.decodeFromString<SerializableAddress>(it).toRecipient()
|
Json.decodeFromString<SerializableAddress>(it).toRecipient()
|
||||||
},
|
},
|
||||||
|
zip321Uri = backStack.savedStateHandle.get<String>(SEND_SCAN_ZIP_321_URI),
|
||||||
clearForm = backStack.savedStateHandle.get<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM) ?: false
|
clearForm = backStack.savedStateHandle.get<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM) ?: false
|
||||||
).also {
|
).also {
|
||||||
// Remove Send screen arguments passed from the Scan or MultipleSubmissionFailure screens if
|
// Remove Send screen arguments passed from the Scan or MultipleSubmissionFailure screens if
|
||||||
// some exist after we use them
|
// some exist after we use them
|
||||||
backStack.savedStateHandle.remove<String>(SEND_SCAN_RECIPIENT_ADDRESS)
|
backStack.savedStateHandle.remove<String>(SEND_SCAN_RECIPIENT_ADDRESS)
|
||||||
|
backStack.savedStateHandle.remove<String>(SEND_SCAN_ZIP_321_URI)
|
||||||
backStack.savedStateHandle.remove<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM)
|
backStack.savedStateHandle.remove<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -487,6 +490,22 @@ private fun fillInHandleForConfirmation(
|
||||||
handle[SEND_CONFIRM_INITIAL_STAGE] = initialStage.toStringName()
|
handle[SEND_CONFIRM_INITIAL_STAGE] = initialStage.toStringName()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fillInHandleForPaymentRequest(
|
||||||
|
handle: SavedStateHandle,
|
||||||
|
zecSend: ZecSend,
|
||||||
|
zip321: String
|
||||||
|
) {
|
||||||
|
handle[PAYMENT_REQUEST_ADDRESS] =
|
||||||
|
Json.encodeToString(
|
||||||
|
serializer = SerializableAddress.serializer(),
|
||||||
|
value = zecSend.destination.toSerializableAddress()
|
||||||
|
)
|
||||||
|
handle[PAYMENT_REQUEST_AMOUNT] = zecSend.amount.value
|
||||||
|
handle[PAYMENT_REQUEST_MEMO] = zecSend.memo.value
|
||||||
|
handle[PAYMENT_REQUEST_PROPOSAL] = zecSend.proposal?.toByteArray() ?: byteArrayOf()
|
||||||
|
handle[PAYMENT_REQUEST_URI] = zip321
|
||||||
|
}
|
||||||
|
|
||||||
private fun NavHostController.navigateJustOnce(
|
private fun NavHostController.navigateJustOnce(
|
||||||
route: String,
|
route: String,
|
||||||
navOptionsBuilder: (NavOptionsBuilder.() -> Unit)? = null
|
navOptionsBuilder: (NavOptionsBuilder.() -> Unit)? = null
|
||||||
|
@ -508,7 +527,7 @@ private fun NavHostController.navigateJustOnce(
|
||||||
*
|
*
|
||||||
* @param currentRouteToBePopped current screen which should be popped up.
|
* @param currentRouteToBePopped current screen which should be popped up.
|
||||||
*/
|
*/
|
||||||
private fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: String) {
|
fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: String) {
|
||||||
if (currentDestination?.route != currentRouteToBePopped) {
|
if (currentDestination?.route != currentRouteToBePopped) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -517,6 +536,7 @@ private fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: Strin
|
||||||
|
|
||||||
object NavigationArguments {
|
object NavigationArguments {
|
||||||
const val SEND_SCAN_RECIPIENT_ADDRESS = "send_scan_recipient_address"
|
const val SEND_SCAN_RECIPIENT_ADDRESS = "send_scan_recipient_address"
|
||||||
|
const val SEND_SCAN_ZIP_321_URI = "send_scan_zip_321_uri"
|
||||||
|
|
||||||
const val SEND_CONFIRM_RECIPIENT_ADDRESS = "send_confirm_recipient_address"
|
const val SEND_CONFIRM_RECIPIENT_ADDRESS = "send_confirm_recipient_address"
|
||||||
const val SEND_CONFIRM_AMOUNT = "send_confirm_amount"
|
const val SEND_CONFIRM_AMOUNT = "send_confirm_amount"
|
||||||
|
@ -525,6 +545,12 @@ object NavigationArguments {
|
||||||
const val SEND_CONFIRM_INITIAL_STAGE = "send_confirm_initial_stage"
|
const val SEND_CONFIRM_INITIAL_STAGE = "send_confirm_initial_stage"
|
||||||
|
|
||||||
const val MULTIPLE_SUBMISSION_CLEAR_FORM = "multiple_submission_clear_form"
|
const val MULTIPLE_SUBMISSION_CLEAR_FORM = "multiple_submission_clear_form"
|
||||||
|
|
||||||
|
const val PAYMENT_REQUEST_ADDRESS = "payment_request_address"
|
||||||
|
const val PAYMENT_REQUEST_AMOUNT = "payment_request_amount"
|
||||||
|
const val PAYMENT_REQUEST_MEMO = "payment_request_memo"
|
||||||
|
const val PAYMENT_REQUEST_PROPOSAL = "payment_request_proposal"
|
||||||
|
const val PAYMENT_REQUEST_URI = "payment_request_uri"
|
||||||
}
|
}
|
||||||
|
|
||||||
object NavigationTargets {
|
object NavigationTargets {
|
||||||
|
@ -535,7 +561,9 @@ object NavigationTargets {
|
||||||
const val EXPORT_PRIVATE_DATA = "export_private_data"
|
const val EXPORT_PRIVATE_DATA = "export_private_data"
|
||||||
const val HOME = "home"
|
const val HOME = "home"
|
||||||
const val CHOOSE_SERVER = "choose_server"
|
const val CHOOSE_SERVER = "choose_server"
|
||||||
|
const val INTEGRATIONS = "integrations"
|
||||||
const val NOT_ENOUGH_SPACE = "not_enough_space"
|
const val NOT_ENOUGH_SPACE = "not_enough_space"
|
||||||
|
const val PAYMENT_REQUEST = "payment_request"
|
||||||
const val QR_CODE = "qr_code"
|
const val QR_CODE = "qr_code"
|
||||||
const val REQUEST = "request"
|
const val REQUEST = "request"
|
||||||
const val SEED_RECOVERY = "seed_recovery"
|
const val SEED_RECOVERY = "seed_recovery"
|
||||||
|
@ -544,7 +572,6 @@ object NavigationTargets {
|
||||||
const val SETTINGS_EXCHANGE_RATE_OPT_IN = "settings_exchange_rate_opt_in"
|
const val SETTINGS_EXCHANGE_RATE_OPT_IN = "settings_exchange_rate_opt_in"
|
||||||
const val SUPPORT = "support"
|
const val SUPPORT = "support"
|
||||||
const val WHATS_NEW = "whats_new"
|
const val WHATS_NEW = "whats_new"
|
||||||
const val INTEGRATIONS = "integrations"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object NavigationArgs {
|
object NavigationArgs {
|
||||||
|
|
|
@ -42,15 +42,13 @@ class Zip321BuildUriUseCase {
|
||||||
|
|
||||||
val paymentRequest = PaymentRequest(payments = listOf(payment))
|
val paymentRequest = PaymentRequest(payments = listOf(payment))
|
||||||
|
|
||||||
// TODO [#1636]: Use fixed ZIP321 library version
|
|
||||||
// TODO [#1636]: https://github.com/Electric-Coin-Company/zashi-android/issues/1636
|
|
||||||
val zip321Uri =
|
val zip321Uri =
|
||||||
ZIP321.uriString(
|
ZIP321.uriString(
|
||||||
paymentRequest,
|
paymentRequest,
|
||||||
ZIP321.FormattingOptions.UseEmptyParamIndex(omitAddressLabel = true)
|
ZIP321.FormattingOptions.UseEmptyParamIndex(omitAddressLabel = true)
|
||||||
)
|
)
|
||||||
|
|
||||||
Twig.info { "Request Zip321 uri: $zip321Uri" }
|
Twig.debug { "Request Zip321 uri: $zip321Uri" }
|
||||||
|
|
||||||
return zip321Uri
|
return zip321Uri
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package co.electriccoin.zcash.ui.common.usecase
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.zecdev.zip321.ZIP321
|
||||||
|
|
||||||
|
internal class Zip321ParseUriValidationUseCase(
|
||||||
|
private val getSynchronizerUseCase: GetSynchronizerUseCase
|
||||||
|
) {
|
||||||
|
operator fun invoke(zip321Uri: String) = validateZip321Uri(zip321Uri)
|
||||||
|
|
||||||
|
private fun validateZip321Uri(zip321Uri: String): Zip321ParseUriValidation {
|
||||||
|
val paymentRequest =
|
||||||
|
runCatching {
|
||||||
|
ZIP321.request(
|
||||||
|
uriString = zip321Uri,
|
||||||
|
validatingRecipients = { address ->
|
||||||
|
// We should be fine with the blocking implementation here
|
||||||
|
runBlocking {
|
||||||
|
getSynchronizerUseCase().validateAddress(address).let { validation ->
|
||||||
|
when (validation) {
|
||||||
|
is AddressType.Invalid -> {
|
||||||
|
Twig.error { "Address from Zip321 validation failed: ${validation.reason}" }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
validation is AddressType.Valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
Twig.error(it) { "Failed to validate address" }
|
||||||
|
}.getOrElse {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
Twig.info { "Payment Request Zip321 validation result: $paymentRequest." }
|
||||||
|
|
||||||
|
return when (paymentRequest) {
|
||||||
|
is ZIP321.ParserResult.Request -> Zip321ParseUriValidation.Valid(zip321Uri)
|
||||||
|
// null or [ZIP321.ParserResult.SingleAddress] is not valid for our ZIP 321 Uri to Proposal use case
|
||||||
|
else -> Zip321ParseUriValidation.Invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class Zip321ParseUriValidation {
|
||||||
|
data class Valid(val zip321Uri: String) : Zip321ParseUriValidation()
|
||||||
|
|
||||||
|
data object Invalid : Zip321ParseUriValidation()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package co.electriccoin.zcash.ui.common.usecase
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
|
import cash.z.ecc.android.sdk.model.Proposal
|
||||||
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
|
|
||||||
|
class Zip321ProposalFromUriUseCase(
|
||||||
|
private val getSynchronizerUseCase: GetSynchronizerUseCase
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(zip321Uri: String) = getProposal(zip321Uri)
|
||||||
|
|
||||||
|
private suspend fun getProposal(zip321Uri: String): Proposal {
|
||||||
|
val proposal = getSynchronizerUseCase.invoke().proposeFulfillingPaymentUri(Account.DEFAULT, zip321Uri)
|
||||||
|
|
||||||
|
Twig.info { "Request Zip321 proposal: $proposal" }
|
||||||
|
|
||||||
|
return proposal
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ internal fun WrapHome(
|
||||||
goMultiTrxSubmissionFailure: () -> Unit,
|
goMultiTrxSubmissionFailure: () -> Unit,
|
||||||
goScan: () -> Unit,
|
goScan: () -> Unit,
|
||||||
goSendConfirmation: (ZecSend) -> Unit,
|
goSendConfirmation: (ZecSend) -> Unit,
|
||||||
|
goPaymentRequest: (ZecSend, String) -> Unit,
|
||||||
sendArguments: SendArguments
|
sendArguments: SendArguments
|
||||||
) {
|
) {
|
||||||
val homeViewModel = koinActivityViewModel<HomeViewModel>()
|
val homeViewModel = koinActivityViewModel<HomeViewModel>()
|
||||||
|
@ -87,6 +88,7 @@ internal fun WrapHome(
|
||||||
WrapHome(
|
WrapHome(
|
||||||
goScan = goScan,
|
goScan = goScan,
|
||||||
goSendConfirmation = goSendConfirmation,
|
goSendConfirmation = goSendConfirmation,
|
||||||
|
goPaymentRequest = goPaymentRequest,
|
||||||
goSettings = goSettings,
|
goSettings = goSettings,
|
||||||
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
|
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
|
||||||
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
|
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
|
||||||
|
@ -105,6 +107,7 @@ internal fun WrapHome(
|
||||||
goMultiTrxSubmissionFailure: () -> Unit,
|
goMultiTrxSubmissionFailure: () -> Unit,
|
||||||
goScan: () -> Unit,
|
goScan: () -> Unit,
|
||||||
goSendConfirmation: (ZecSend) -> Unit,
|
goSendConfirmation: (ZecSend) -> Unit,
|
||||||
|
goPaymentRequest: (ZecSend, String) -> Unit,
|
||||||
isKeepScreenOnWhileSyncing: Boolean?,
|
isKeepScreenOnWhileSyncing: Boolean?,
|
||||||
isShowingRestoreSuccess: Boolean,
|
isShowingRestoreSuccess: Boolean,
|
||||||
sendArguments: SendArguments,
|
sendArguments: SendArguments,
|
||||||
|
@ -182,6 +185,7 @@ internal fun WrapHome(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goSendConfirmation = goSendConfirmation,
|
goSendConfirmation = goSendConfirmation,
|
||||||
|
goPaymentRequest = goPaymentRequest,
|
||||||
goSettings = goSettings,
|
goSettings = goSettings,
|
||||||
sendArguments = sendArguments
|
sendArguments = sendArguments
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
|
package co.electriccoin.zcash.ui.screen.paymentrequest
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import cash.z.ecc.android.sdk.model.Proposal
|
||||||
|
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||||
|
import co.electriccoin.zcash.ui.MainActivity
|
||||||
|
import co.electriccoin.zcash.ui.NavigationTargets
|
||||||
|
import co.electriccoin.zcash.ui.NavigationTargets.HOME
|
||||||
|
import co.electriccoin.zcash.ui.R
|
||||||
|
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.authentication.AuthenticationUseCase
|
||||||
|
import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication
|
||||||
|
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestArguments
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestStage
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestState
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.view.PaymentRequestView
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.viewmodel.PaymentRequestViewModel
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun WrapPaymentRequest(arguments: PaymentRequestArguments) {
|
||||||
|
val activity = LocalActivity.current as MainActivity
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||||
|
val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val paymentRequestViewModel = koinViewModel<PaymentRequestViewModel> { parametersOf(arguments) }
|
||||||
|
val paymentRequestState by paymentRequestViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val authenticateForProposal = rememberSaveable { mutableStateOf<Proposal?>(null) }
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
val onBackAction = {
|
||||||
|
when (paymentRequestState) {
|
||||||
|
PaymentRequestState.Loading -> {}
|
||||||
|
is PaymentRequestState.Prepared -> {
|
||||||
|
val state = (paymentRequestState as PaymentRequestState.Prepared)
|
||||||
|
when (state.stage) {
|
||||||
|
PaymentRequestStage.Initial,
|
||||||
|
PaymentRequestStage.Confirmed -> navController.popBackStack()
|
||||||
|
PaymentRequestStage.Sending -> {
|
||||||
|
// No action - wait until the sending is done
|
||||||
|
}
|
||||||
|
is PaymentRequestStage.Failure -> paymentRequestViewModel.setStage(PaymentRequestStage.Initial)
|
||||||
|
is PaymentRequestStage.FailureGrpc -> {
|
||||||
|
paymentRequestViewModel.setStage(PaymentRequestStage.Confirmed)
|
||||||
|
navController.navigate(HOME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler { onBackAction() }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
paymentRequestViewModel.backNavigationCommand.collect {
|
||||||
|
onBackAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
paymentRequestViewModel.closeNavigationCommand.collect {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
paymentRequestViewModel.addContactNavigationCommand.collect {
|
||||||
|
navController.navigate(AddContactArgs(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
paymentRequestViewModel.authenticationNavigationCommand.collect {
|
||||||
|
authenticateForProposal.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
paymentRequestViewModel.homeNavigationCommand.collect {
|
||||||
|
navController.navigate(HOME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
paymentRequestViewModel.sendReportFailedNavigationCommand.collect {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = activity.getString(R.string.payment_request_send_failed_report_unable_open_email)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentRequestView(
|
||||||
|
state = paymentRequestState,
|
||||||
|
topAppBarSubTitleState = walletState,
|
||||||
|
snackbarHostState = snackbarHostState
|
||||||
|
)
|
||||||
|
|
||||||
|
if (authenticateForProposal.value != null) {
|
||||||
|
activity.WrapAuthentication(
|
||||||
|
goSupport = {
|
||||||
|
authenticateForProposal.value = null
|
||||||
|
navController.navigate(NavigationTargets.SUPPORT)
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
paymentRequestViewModel.onSendAllowed(authenticateForProposal.value!!)
|
||||||
|
authenticateForProposal.value = null
|
||||||
|
},
|
||||||
|
onCancel = {
|
||||||
|
authenticateForProposal.value = null
|
||||||
|
},
|
||||||
|
onFailed = {
|
||||||
|
// No action needed
|
||||||
|
},
|
||||||
|
useCase = AuthenticationUseCase.SendFunds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.paymentrequest
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||||
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
|
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestArguments
|
||||||
|
|
||||||
|
internal object PaymentRequestArgumentsFixture {
|
||||||
|
fun new() =
|
||||||
|
PaymentRequestArguments(
|
||||||
|
address =
|
||||||
|
SerializableAddress(
|
||||||
|
WalletFixture.Alice.getAddresses(ZcashNetwork.Mainnet).unified,
|
||||||
|
AddressType.Unified
|
||||||
|
),
|
||||||
|
amount = 10000000,
|
||||||
|
memo = "For the coffee",
|
||||||
|
proposal = FirstClassByteArray(byteArrayOf()),
|
||||||
|
zip321Uri = "zcash:t1duiEGg7b39nfQee3XaTY4f5McqfyJKhBi?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBt",
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.paymentrequest.model
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
|
import cash.z.ecc.android.sdk.model.Memo
|
||||||
|
import cash.z.ecc.android.sdk.model.Proposal
|
||||||
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
|
import cash.z.ecc.android.sdk.model.ZecSend
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments
|
||||||
|
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
data class PaymentRequestArguments(
|
||||||
|
val address: SerializableAddress?,
|
||||||
|
val amount: Long?,
|
||||||
|
val memo: String?,
|
||||||
|
val proposal: FirstClassByteArray?,
|
||||||
|
val zip321Uri: String?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
internal fun fromSavedStateHandle(savedStateHandle: SavedStateHandle) =
|
||||||
|
PaymentRequestArguments(
|
||||||
|
address =
|
||||||
|
savedStateHandle.get<String>(NavigationArguments.PAYMENT_REQUEST_ADDRESS)?.let {
|
||||||
|
Json.decodeFromString<SerializableAddress>(it)
|
||||||
|
},
|
||||||
|
amount = savedStateHandle.get<Long>(NavigationArguments.PAYMENT_REQUEST_AMOUNT),
|
||||||
|
memo = savedStateHandle.get<String>(NavigationArguments.PAYMENT_REQUEST_MEMO),
|
||||||
|
proposal =
|
||||||
|
savedStateHandle.get<ByteArray>(NavigationArguments.PAYMENT_REQUEST_PROPOSAL)?.let {
|
||||||
|
FirstClassByteArray(it)
|
||||||
|
},
|
||||||
|
zip321Uri = savedStateHandle.get<String>(NavigationArguments.PAYMENT_REQUEST_URI),
|
||||||
|
).also {
|
||||||
|
// Remove the screen arguments passed from the other screen if some exist
|
||||||
|
savedStateHandle.remove<String>(NavigationArguments.PAYMENT_REQUEST_ADDRESS)
|
||||||
|
savedStateHandle.remove<Long>(NavigationArguments.PAYMENT_REQUEST_AMOUNT)
|
||||||
|
savedStateHandle.remove<String>(NavigationArguments.PAYMENT_REQUEST_MEMO)
|
||||||
|
savedStateHandle.remove<ByteArray>(NavigationArguments.PAYMENT_REQUEST_PROPOSAL)
|
||||||
|
savedStateHandle.remove<String>(NavigationArguments.PAYMENT_REQUEST_URI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun toZecSend() =
|
||||||
|
ZecSend(
|
||||||
|
destination = address?.toWalletAddress() ?: error("Address null"),
|
||||||
|
amount = amount?.let { Zatoshi(amount) } ?: error("Amount null"),
|
||||||
|
memo = memo?.let { Memo(memo) } ?: error("Memo null"),
|
||||||
|
proposal = proposal?.let { Proposal.fromByteArray(proposal.byteArray) } ?: error("Proposal null"),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.paymentrequest.model
|
||||||
|
|
||||||
|
sealed class PaymentRequestStage {
|
||||||
|
data object Initial : PaymentRequestStage()
|
||||||
|
|
||||||
|
data object Sending : PaymentRequestStage()
|
||||||
|
|
||||||
|
data object Confirmed : PaymentRequestStage()
|
||||||
|
|
||||||
|
data class Failure(
|
||||||
|
val error: String,
|
||||||
|
val stackTrace: String,
|
||||||
|
) : PaymentRequestStage()
|
||||||
|
|
||||||
|
data object FailureGrpc : PaymentRequestStage()
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.paymentrequest.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||||
|
import cash.z.ecc.android.sdk.model.Proposal
|
||||||
|
import cash.z.ecc.android.sdk.model.ZecSend
|
||||||
|
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||||
|
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||||
|
|
||||||
|
internal sealed class PaymentRequestState {
|
||||||
|
data object Loading : PaymentRequestState()
|
||||||
|
|
||||||
|
data class Prepared(
|
||||||
|
val arguments: PaymentRequestArguments,
|
||||||
|
val contact: AddressBookContact?,
|
||||||
|
val exchangeRateState: ExchangeRateState,
|
||||||
|
val monetarySeparators: MonetarySeparators,
|
||||||
|
val onAddToContacts: (String) -> Unit,
|
||||||
|
val onClose: () -> Unit,
|
||||||
|
val onBack: () -> Unit,
|
||||||
|
val onSend: (proposal: Proposal) -> Unit,
|
||||||
|
val zecSend: ZecSend,
|
||||||
|
val stage: PaymentRequestStage,
|
||||||
|
val onContactSupport: (String?) -> Unit,
|
||||||
|
) : PaymentRequestState()
|
||||||
|
}
|
|
@ -0,0 +1,545 @@
|
||||||
|
@file:Suppress("TooManyFunctions")
|
||||||
|
|
||||||
|
package co.electriccoin.zcash.ui.screen.paymentrequest.view
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||||
|
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||||
|
import cash.z.ecc.sdk.extension.toZecStringFull
|
||||||
|
import co.electriccoin.zcash.ui.R
|
||||||
|
import co.electriccoin.zcash.ui.common.compose.BalanceWidgetBigLineOnly
|
||||||
|
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
|
||||||
|
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||||
|
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||||
|
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||||
|
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||||
|
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||||
|
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||||
|
import co.electriccoin.zcash.ui.design.component.StyledBalance
|
||||||
|
import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
|
||||||
|
import co.electriccoin.zcash.ui.design.component.ZashiBottomBar
|
||||||
|
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||||
|
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
|
||||||
|
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||||
|
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
|
||||||
|
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeLabel
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.PaymentRequestArgumentsFixture
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestStage
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestState
|
||||||
|
import co.electriccoin.zcash.ui.screen.send.ext.abbreviated
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@PreviewScreens
|
||||||
|
private fun PaymentRequestLoadingPreview() =
|
||||||
|
ZcashTheme(forceDarkMode = true) {
|
||||||
|
PaymentRequestView(
|
||||||
|
state = PaymentRequestState.Loading,
|
||||||
|
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||||
|
snackbarHostState = SnackbarHostState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@PreviewScreens
|
||||||
|
private fun PaymentRequestPreview() =
|
||||||
|
ZcashTheme(forceDarkMode = false) {
|
||||||
|
PaymentRequestView(
|
||||||
|
state =
|
||||||
|
PaymentRequestState.Prepared(
|
||||||
|
arguments = PaymentRequestArgumentsFixture.new(),
|
||||||
|
contact = null,
|
||||||
|
exchangeRateState = ExchangeRateState.Data(onRefresh = {}),
|
||||||
|
monetarySeparators = MonetarySeparators.current(),
|
||||||
|
onAddToContacts = {},
|
||||||
|
onContactSupport = { _ -> },
|
||||||
|
onBack = {},
|
||||||
|
onClose = {},
|
||||||
|
onSend = {},
|
||||||
|
zecSend = PaymentRequestArgumentsFixture.new().toZecSend(),
|
||||||
|
stage = PaymentRequestStage.Initial,
|
||||||
|
),
|
||||||
|
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||||
|
snackbarHostState = SnackbarHostState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun PaymentRequestView(
|
||||||
|
state: PaymentRequestState,
|
||||||
|
topAppBarSubTitleState: TopAppBarSubTitleState,
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
PaymentRequestState.Loading -> {
|
||||||
|
CircularScreenProgressIndicator()
|
||||||
|
}
|
||||||
|
is PaymentRequestState.Prepared -> {
|
||||||
|
BlankBgScaffold(
|
||||||
|
topBar = {
|
||||||
|
PaymentRequestTopAppBar(
|
||||||
|
onClose = state.onClose,
|
||||||
|
subTitleState = topAppBarSubTitleState,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
PaymentRequestBottomBar(state = state)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
) { paddingValues ->
|
||||||
|
Box {
|
||||||
|
PaymentRequestContents(
|
||||||
|
state = state,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.verticalScroll(
|
||||||
|
rememberScrollState()
|
||||||
|
)
|
||||||
|
.scaffoldPadding(paddingValues),
|
||||||
|
)
|
||||||
|
when (state.stage) {
|
||||||
|
PaymentRequestStage.FailureGrpc -> {
|
||||||
|
PaymentRequestSendFailureGrpc(
|
||||||
|
onDone = state.onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PaymentRequestStage.Failure -> {
|
||||||
|
PaymentRequestSendFailure(
|
||||||
|
onDone = state.onBack,
|
||||||
|
onReport = { status -> state.onContactSupport(status.stackTrace) },
|
||||||
|
stage = state.stage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// No action needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestTopAppBar(
|
||||||
|
onClose: () -> Unit,
|
||||||
|
subTitleState: TopAppBarSubTitleState,
|
||||||
|
) {
|
||||||
|
ZashiSmallTopAppBar(
|
||||||
|
subtitle =
|
||||||
|
when (subTitleState) {
|
||||||
|
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
|
||||||
|
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
|
||||||
|
TopAppBarSubTitleState.None -> null
|
||||||
|
},
|
||||||
|
title = stringResource(id = R.string.payment_request_title),
|
||||||
|
navigationAction = {
|
||||||
|
IconButton(
|
||||||
|
onClick = onClose,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = ZcashTheme.dimens.spacingDefault)
|
||||||
|
// Making the size bigger by 3.dp so the rounded image corners are not stripped out
|
||||||
|
.size(43.dp),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter =
|
||||||
|
painterResource(
|
||||||
|
id = co.electriccoin.zcash.ui.design.R.drawable.ic_close_full
|
||||||
|
),
|
||||||
|
contentDescription = stringResource(id = R.string.payment_request_close_content_description),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(all = 3.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestBottomBar(
|
||||||
|
state: PaymentRequestState.Prepared,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ZashiBottomBar(modifier = modifier.fillMaxWidth()) {
|
||||||
|
ZashiButton(
|
||||||
|
text = stringResource(id = R.string.payment_request_send_btn),
|
||||||
|
onClick = { state.onSend(state.zecSend.proposal!!) },
|
||||||
|
enabled = state.stage != PaymentRequestStage.Sending && state.stage != PaymentRequestStage.Confirmed,
|
||||||
|
isLoading = state.stage == PaymentRequestStage.Sending,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestContents(
|
||||||
|
state: PaymentRequestState.Prepared,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingXl))
|
||||||
|
|
||||||
|
PaymentRequestBalances(state)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacing4xl))
|
||||||
|
|
||||||
|
PaymentRequestAddresses(state)
|
||||||
|
|
||||||
|
if (state.zecSend.memo.value.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacing4xl))
|
||||||
|
PaymentRequestMemo(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacing3xl))
|
||||||
|
|
||||||
|
PaymentRequestAmounts(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestBalances(
|
||||||
|
state: PaymentRequestState.Prepared,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
BalanceWidgetBigLineOnly(
|
||||||
|
parts = state.zecSend.amount.toZecStringFull().asZecAmountTriple(),
|
||||||
|
// We don't hide any balance in confirmation screen
|
||||||
|
isHideBalances = false
|
||||||
|
)
|
||||||
|
|
||||||
|
StyledExchangeLabel(
|
||||||
|
zatoshi = state.zecSend.amount,
|
||||||
|
state = state.exchangeRateState,
|
||||||
|
isHideBalances = false,
|
||||||
|
style = ZashiTypography.textMd.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
textColor = ZashiColors.Text.textPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestAddresses(
|
||||||
|
state: PaymentRequestState.Prepared,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var isShowingFullAddress by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
when (state.zecSend.destination) {
|
||||||
|
is WalletAddress.Transparent -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.payment_request_requested_by),
|
||||||
|
color = ZashiColors.Text.textTertiary,
|
||||||
|
style = ZashiTypography.textSm,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.contact != null) {
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingSm))
|
||||||
|
Text(
|
||||||
|
text = state.contact.name,
|
||||||
|
color = ZashiColors.Inputs.Filled.label,
|
||||||
|
style = ZashiTypography.textSm,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingSm))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
if (isShowingFullAddress) {
|
||||||
|
state.zecSend.destination.address
|
||||||
|
} else {
|
||||||
|
state.zecSend.destination.abbreviated()
|
||||||
|
},
|
||||||
|
color = ZashiColors.Text.textPrimary,
|
||||||
|
style = ZashiTypography.textXs,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.animateContentSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingXl))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
if (state.zecSend.destination !is WalletAddress.Transparent) {
|
||||||
|
if (isShowingFullAddress) {
|
||||||
|
PaymentRequestChipText(
|
||||||
|
text = stringResource(id = R.string.payment_request_btn_hide_address),
|
||||||
|
icon = painterResource(id = R.drawable.ic_chevron_up),
|
||||||
|
onClick = { isShowingFullAddress = false }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PaymentRequestChipText(
|
||||||
|
text = stringResource(id = R.string.payment_request_btn_show_address),
|
||||||
|
icon = painterResource(id = R.drawable.ic_chevron_down),
|
||||||
|
onClick = { isShowingFullAddress = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(ZashiDimensions.Spacing.spacingLg))
|
||||||
|
}
|
||||||
|
if (state.contact == null) {
|
||||||
|
PaymentRequestChipText(
|
||||||
|
text = stringResource(id = R.string.payment_request_btn_save_contact),
|
||||||
|
icon = painterResource(id = R.drawable.ic_user_plus),
|
||||||
|
onClick = { state.onAddToContacts(state.zecSend.destination.address) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestChipText(
|
||||||
|
text: String,
|
||||||
|
icon: Painter,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.background(
|
||||||
|
ZashiColors.Btns.Tertiary.btnTertiaryBg,
|
||||||
|
RoundedCornerShape(ZashiDimensions.Radius.radiusMd)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(ZashiDimensions.Radius.radiusMd))
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(
|
||||||
|
horizontal = ZashiDimensions.Spacing.spacingXl,
|
||||||
|
vertical = 10.dp
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(ZashiColors.Btns.Tertiary.btnTertiaryFg)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = ZashiColors.Btns.Tertiary.btnTertiaryFg,
|
||||||
|
style = ZashiTypography.textSm,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestMemo(
|
||||||
|
state: PaymentRequestState.Prepared,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.payment_request_memo),
|
||||||
|
color = ZashiColors.Text.textPrimary,
|
||||||
|
style = ZashiTypography.textSm,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingSm))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(ZashiColors.Inputs.Filled.bg, RoundedCornerShape(ZashiDimensions.Radius.radiusIg))
|
||||||
|
.padding(all = ZashiDimensions.Spacing.spacingXl),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.zecSend.memo.value,
|
||||||
|
color = ZashiColors.Inputs.Filled.text,
|
||||||
|
style = ZashiTypography.textXs,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestAmounts(
|
||||||
|
state: PaymentRequestState.Prepared,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.payment_request_fee),
|
||||||
|
color = ZashiColors.Text.textTertiary,
|
||||||
|
style = ZashiTypography.textSm,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(ZashiDimensions.Spacing.spacingMd))
|
||||||
|
StyledBalance(
|
||||||
|
balanceParts = state.zecSend.proposal!!.totalFeeRequired().toZecStringFull().asZecAmountTriple(),
|
||||||
|
textColor = ZashiColors.Text.textPrimary,
|
||||||
|
textStyle =
|
||||||
|
StyledBalanceDefaults.textStyles(
|
||||||
|
mostSignificantPart = ZashiTypography.textSm.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
leastSignificantPart = ZashiTypography.textXxs.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacing2xl))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.payment_request_total),
|
||||||
|
color = ZashiColors.Text.textTertiary,
|
||||||
|
style = ZashiTypography.textSm,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(ZashiDimensions.Spacing.spacingMd))
|
||||||
|
StyledBalance(
|
||||||
|
balanceParts = state.zecSend.amount.toZecStringFull().asZecAmountTriple(),
|
||||||
|
textColor = ZashiColors.Text.textPrimary,
|
||||||
|
textStyle =
|
||||||
|
StyledBalanceDefaults.textStyles(
|
||||||
|
mostSignificantPart = ZashiTypography.textSm.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
leastSignificantPart = ZashiTypography.textXxs.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview("SendConfirmationFailure")
|
||||||
|
private fun PreviewSendConfirmationFailure() {
|
||||||
|
ZcashTheme(forceDarkMode = false) {
|
||||||
|
BlankSurface {
|
||||||
|
PaymentRequestSendFailure(
|
||||||
|
onDone = {},
|
||||||
|
onReport = {},
|
||||||
|
stage = PaymentRequestStage.Failure("Failed - network error", "Failed stackTrace"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestSendFailure(
|
||||||
|
onDone: () -> Unit,
|
||||||
|
onReport: (PaymentRequestStage.Failure) -> Unit,
|
||||||
|
stage: PaymentRequestStage.Failure,
|
||||||
|
) {
|
||||||
|
// TODO [#1276]: Once we ensure that the reason contains a localized message, we can leverage it for the UI prompt
|
||||||
|
// TODO [#1276]: Consider adding support for a specific exception in AppAlertDialog
|
||||||
|
// TODO [#1276]: https://github.com/Electric-Coin-Company/zashi-android/issues/1276
|
||||||
|
|
||||||
|
AppAlertDialog(
|
||||||
|
title = stringResource(id = R.string.payment_request_dialog_error_title),
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.payment_request_dialog_error_text),
|
||||||
|
color = ZcashTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (stage.error.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stage.error,
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = ZcashTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButtonText = stringResource(id = R.string.payment_request_dialog_error_ok_btn),
|
||||||
|
onConfirmButtonClick = onDone,
|
||||||
|
dismissButtonText = stringResource(id = R.string.payment_request_dialog_error_report_btn),
|
||||||
|
onDismissButtonClick = { onReport(stage) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentRequestSendFailureGrpc(onDone: () -> Unit) {
|
||||||
|
AppAlertDialog(
|
||||||
|
title = stringResource(id = R.string.payment_request_dialog_error_grpc_title),
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.payment_request_dialog_error_grpc_text),
|
||||||
|
color = ZcashTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButtonText = stringResource(id = R.string.payment_request_dialog_error_grpc_btn),
|
||||||
|
onConfirmButtonClick = onDone
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.paymentrequest.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
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.UnifiedSpendingKey
|
||||||
|
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||||
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
|
import co.electriccoin.zcash.ui.R
|
||||||
|
import co.electriccoin.zcash.ui.common.provider.GetMonetarySeparatorProvider
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
|
||||||
|
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestArguments
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestStage
|
||||||
|
import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestState
|
||||||
|
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
|
||||||
|
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
|
||||||
|
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
|
||||||
|
import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType
|
||||||
|
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
|
||||||
|
import co.electriccoin.zcash.ui.util.EmailUtil
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
class PaymentRequestViewModel(
|
||||||
|
private val application: Application,
|
||||||
|
private val arguments: PaymentRequestArguments,
|
||||||
|
private val authenticationViewModel: AuthenticationViewModel,
|
||||||
|
private val createTransactionsViewModel: CreateTransactionsViewModel,
|
||||||
|
getMonetarySeparators: GetMonetarySeparatorProvider,
|
||||||
|
private val getSpendingKeyUseCase: GetSpendingKeyUseCase,
|
||||||
|
private val getSynchronizer: GetSynchronizerUseCase,
|
||||||
|
private val supportViewModel: SupportViewModel,
|
||||||
|
walletViewModel: WalletViewModel,
|
||||||
|
observeAddressBookContacts: ObserveAddressBookContactsUseCase,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val stage = MutableStateFlow<PaymentRequestStage>(PaymentRequestStage.Initial)
|
||||||
|
|
||||||
|
internal val state =
|
||||||
|
combine(
|
||||||
|
walletViewModel.exchangeRateUsd,
|
||||||
|
observeAddressBookContacts(),
|
||||||
|
stage,
|
||||||
|
supportViewModel.supportInfo.mapNotNull { it },
|
||||||
|
) { rate, contacts, currentStage, supportInfo ->
|
||||||
|
PaymentRequestState.Prepared(
|
||||||
|
arguments = arguments,
|
||||||
|
contact = contacts?.find { it.address == arguments.address?.address },
|
||||||
|
exchangeRateState = rate,
|
||||||
|
monetarySeparators = getMonetarySeparators(),
|
||||||
|
onAddToContacts = { onAddToContacts(it) },
|
||||||
|
onBack = ::onBack,
|
||||||
|
onClose = ::onClose,
|
||||||
|
onContactSupport = { message -> onContactSupport(message, supportInfo) },
|
||||||
|
onSend = { onSend(it) },
|
||||||
|
stage = currentStage,
|
||||||
|
zecSend = arguments.toZecSend(),
|
||||||
|
)
|
||||||
|
}.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||||
|
initialValue = PaymentRequestState.Loading
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val backNavigationCommand = MutableSharedFlow<Unit>()
|
||||||
|
|
||||||
|
internal val closeNavigationCommand = MutableSharedFlow<Unit>()
|
||||||
|
|
||||||
|
internal val addContactNavigationCommand = MutableSharedFlow<String>()
|
||||||
|
|
||||||
|
internal val homeNavigationCommand = MutableSharedFlow<Unit>()
|
||||||
|
|
||||||
|
internal val authenticationNavigationCommand = MutableSharedFlow<Proposal>()
|
||||||
|
|
||||||
|
internal val sendReportFailedNavigationCommand = MutableSharedFlow<Unit>()
|
||||||
|
|
||||||
|
internal fun onClose() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
closeNavigationCommand.emit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun onBack() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
backNavigationCommand.emit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onHome() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
homeNavigationCommand.emit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun setStage(newStage: PaymentRequestStage) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
stage.emit(newStage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAddToContacts(address: String) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
addContactNavigationCommand.emit(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSend(proposal: Proposal) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
authenticationViewModel.isSendFundsAuthenticationRequired
|
||||||
|
.filterNotNull()
|
||||||
|
.collect { isProtected ->
|
||||||
|
if (isProtected) {
|
||||||
|
authenticationNavigationCommand.emit(proposal)
|
||||||
|
} else {
|
||||||
|
onSendAllowed(proposal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun onSendAllowed(proposal: Proposal) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
runSendFundsAction(
|
||||||
|
createTransactionsViewModel = createTransactionsViewModel,
|
||||||
|
// The not-null assertion operator is necessary here even if we check its
|
||||||
|
// nullability before due to property is declared in different module. See more
|
||||||
|
// details on the Kotlin forum
|
||||||
|
proposal = proposal,
|
||||||
|
spendingKey = getSpendingKeyUseCase(),
|
||||||
|
synchronizer = getSynchronizer(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun runSendFundsAction(
|
||||||
|
createTransactionsViewModel: CreateTransactionsViewModel,
|
||||||
|
proposal: Proposal,
|
||||||
|
spendingKey: UnifiedSpendingKey,
|
||||||
|
synchronizer: Synchronizer,
|
||||||
|
) {
|
||||||
|
setStage(PaymentRequestStage.Sending)
|
||||||
|
|
||||||
|
val submitResult =
|
||||||
|
submitTransactions(
|
||||||
|
createTransactionsViewModel = createTransactionsViewModel,
|
||||||
|
proposal = proposal,
|
||||||
|
synchronizer = synchronizer,
|
||||||
|
spendingKey = spendingKey
|
||||||
|
)
|
||||||
|
|
||||||
|
Twig.debug { "Transactions submitted with result: $submitResult" }
|
||||||
|
|
||||||
|
processSubmissionResult(submitResult = submitResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun submitTransactions(
|
||||||
|
createTransactionsViewModel: CreateTransactionsViewModel,
|
||||||
|
proposal: Proposal,
|
||||||
|
synchronizer: Synchronizer,
|
||||||
|
spendingKey: UnifiedSpendingKey
|
||||||
|
): SubmitResult {
|
||||||
|
Twig.debug { "Sending transactions..." }
|
||||||
|
|
||||||
|
val result =
|
||||||
|
createTransactionsViewModel.runCreateTransactions(
|
||||||
|
synchronizer = synchronizer,
|
||||||
|
spendingKey = spendingKey,
|
||||||
|
proposal = proposal
|
||||||
|
)
|
||||||
|
|
||||||
|
// Triggering the transaction history and balances refresh to be notified immediately
|
||||||
|
// about the wallet's updated state
|
||||||
|
(synchronizer as SdkSynchronizer).run {
|
||||||
|
refreshTransactions()
|
||||||
|
refreshAllBalances()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processSubmissionResult(submitResult: SubmitResult) {
|
||||||
|
when (submitResult) {
|
||||||
|
SubmitResult.Success -> {
|
||||||
|
setStage(PaymentRequestStage.Confirmed)
|
||||||
|
onHome()
|
||||||
|
}
|
||||||
|
is SubmitResult.SimpleTrxFailure.SimpleTrxFailureSubmit -> {
|
||||||
|
setStage(PaymentRequestStage.Failure(submitResult.toErrorMessage(), submitResult.toErrorStacktrace()))
|
||||||
|
}
|
||||||
|
is SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc -> {
|
||||||
|
setStage(PaymentRequestStage.FailureGrpc)
|
||||||
|
}
|
||||||
|
is SubmitResult.SimpleTrxFailure.SimpleTrxFailureOther -> {
|
||||||
|
setStage(PaymentRequestStage.Failure(submitResult.toErrorMessage(), submitResult.toErrorStacktrace()))
|
||||||
|
}
|
||||||
|
is SubmitResult.MultipleTrxFailure -> {
|
||||||
|
Twig.error { "$submitResult is currently unsupported submission result" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onContactSupport(
|
||||||
|
message: String?,
|
||||||
|
supportInfo: SupportInfo
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
val fullMessage =
|
||||||
|
EmailUtil.formatMessage(
|
||||||
|
body = message,
|
||||||
|
supportInfo =
|
||||||
|
supportInfo.toSupportString(
|
||||||
|
SupportInfoType.entries.toSet()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val mailIntent =
|
||||||
|
EmailUtil.newMailActivityIntent(
|
||||||
|
application.applicationContext.getString(R.string.support_email_address),
|
||||||
|
application.applicationContext.getString(R.string.app_name),
|
||||||
|
fullMessage
|
||||||
|
).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
application.startActivity(mailIntent)
|
||||||
|
}.onSuccess {
|
||||||
|
setStage(PaymentRequestStage.Initial)
|
||||||
|
}.onFailure {
|
||||||
|
setStage(PaymentRequestStage.Initial)
|
||||||
|
sendReportFailedNavigationCommand.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,12 @@ import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
@ -435,6 +437,14 @@ private fun ColumnScope.QrCode(
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
|
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
|
||||||
)
|
)
|
||||||
|
.background(
|
||||||
|
if (isSystemInDarkTheme()) {
|
||||||
|
ZashiColors.Surfaces.bgAlt
|
||||||
|
} else {
|
||||||
|
ZashiColors.Surfaces.bgPrimary
|
||||||
|
},
|
||||||
|
RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
|
||||||
|
)
|
||||||
.padding(all = 12.dp)
|
.padding(all = 12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ package co.electriccoin.zcash.ui.screen.request.view
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
@ -133,6 +135,14 @@ private fun ColumnScope.QrCode(
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
|
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
|
||||||
)
|
)
|
||||||
|
.background(
|
||||||
|
if (isSystemInDarkTheme()) {
|
||||||
|
ZashiColors.Surfaces.bgAlt
|
||||||
|
} else {
|
||||||
|
ZashiColors.Surfaces.bgPrimary
|
||||||
|
},
|
||||||
|
RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
|
||||||
|
)
|
||||||
.padding(all = 12.dp)
|
.padding(all = 12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,68 +3,62 @@ package co.electriccoin.zcash.ui.screen.scan
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
|
||||||
import cash.z.ecc.android.sdk.type.AddressType
|
|
||||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_RECIPIENT_ADDRESS
|
||||||
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_ZIP_321_URI
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||||
|
import co.electriccoin.zcash.ui.popBackStackJustOnce
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanResultState
|
||||||
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
|
||||||
import co.electriccoin.zcash.ui.util.SettingsUtil
|
import co.electriccoin.zcash.ui.util.SettingsUtil
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import kotlinx.coroutines.sync.withLock
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun WrapScanValidator(
|
internal fun WrapScanValidator(args: ScanNavigationArgs) {
|
||||||
onScanValid: (address: SerializableAddress) -> Unit,
|
val navController = LocalNavController.current
|
||||||
goBack: () -> Unit
|
val context = LocalContext.current
|
||||||
) {
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||||
|
val viewModel = koinViewModel<ScanViewModel> { parametersOf(args) }
|
||||||
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
||||||
|
|
||||||
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
|
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
goBack()
|
navController.popBackStackJustOnce(ScanNavigationArgs.ROUTE)
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapScan(
|
LaunchedEffect(Unit) {
|
||||||
onScanValid = onScanValid,
|
viewModel.navigateBack.collect { scanResult ->
|
||||||
goBack = goBack,
|
navController.previousBackStackEntry?.savedStateHandle?.apply {
|
||||||
synchronizer = synchronizer,
|
when (scanResult) {
|
||||||
topAppBarSubTitleState = walletState,
|
is ScanResultState.Address -> set(SEND_SCAN_RECIPIENT_ADDRESS, scanResult.address)
|
||||||
)
|
is ScanResultState.Zip321Uri -> set(SEND_SCAN_ZIP_321_URI, scanResult.zip321Uri)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
navController.popBackStackJustOnce(ScanNavigationArgs.ROUTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
LaunchedEffect(Unit) {
|
||||||
fun WrapScan(
|
viewModel.navigateCommand.collect {
|
||||||
goBack: () -> Unit,
|
navController.popBackStack()
|
||||||
onScanValid: (address: SerializableAddress) -> Unit,
|
navController.navigate(it)
|
||||||
synchronizer: Synchronizer?,
|
}
|
||||||
topAppBarSubTitleState: TopAppBarSubTitleState,
|
}
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
var hasBeenScannedSuccessfully by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val mutex = remember { Mutex() }
|
|
||||||
|
|
||||||
var addressValidationResult by remember { mutableStateOf<AddressType?>(null) }
|
|
||||||
|
|
||||||
if (synchronizer == null) {
|
if (synchronizer == null) {
|
||||||
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
||||||
|
@ -74,24 +68,13 @@ fun WrapScan(
|
||||||
} else {
|
} else {
|
||||||
Scan(
|
Scan(
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
addressValidationResult = addressValidationResult,
|
validationResult = state,
|
||||||
onBack = goBack,
|
onBack = { navController.popBackStackJustOnce(ScanNavigationArgs.ROUTE) },
|
||||||
onScanned = { result ->
|
onScanned = {
|
||||||
scope.launch {
|
viewModel.onScanned(it)
|
||||||
mutex.withLock {
|
|
||||||
if (!hasBeenScannedSuccessfully) {
|
|
||||||
addressValidationResult = synchronizer.validateAddress(result)
|
|
||||||
val isAddressValid = addressValidationResult?.let { !it.isNotValid } ?: false
|
|
||||||
if (isAddressValid) {
|
|
||||||
hasBeenScannedSuccessfully = true
|
|
||||||
onScanValid(SerializableAddress(result, addressValidationResult!!))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onScanError = {
|
onScanError = {
|
||||||
addressValidationResult = AddressType.Invalid()
|
viewModel.onScannedError()
|
||||||
},
|
},
|
||||||
onOpenSettings = {
|
onOpenSettings = {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
@ -107,7 +90,7 @@ fun WrapScan(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onScanStateChanged = {},
|
onScanStateChanged = {},
|
||||||
topAppBarSubTitleState = topAppBarSubTitleState,
|
topAppBarSubTitleState = walletState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.model
|
||||||
|
|
||||||
|
sealed class ScanResultState {
|
||||||
|
data class Address(val address: String) : ScanResultState()
|
||||||
|
|
||||||
|
data class Zip321Uri(val zip321Uri: String) : ScanResultState()
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package co.electriccoin.zcash.ui.screen.scan.model
|
package co.electriccoin.zcash.ui.screen.scan.model
|
||||||
|
|
||||||
enum class ScanState {
|
enum class ScanScreenState {
|
||||||
Failed,
|
Failed,
|
||||||
Permission,
|
Permission,
|
||||||
Scanning
|
Scanning
|
|
@ -0,0 +1,7 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.model
|
||||||
|
|
||||||
|
enum class ScanValidationState {
|
||||||
|
NONE,
|
||||||
|
INVALID,
|
||||||
|
VALID
|
||||||
|
}
|
|
@ -74,7 +74,6 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import androidx.constraintlayout.compose.Dimension
|
import androidx.constraintlayout.compose.Dimension
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import cash.z.ecc.android.sdk.type.AddressType
|
|
||||||
import co.electriccoin.zcash.spackle.Twig
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
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
|
||||||
|
@ -86,7 +85,8 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||||
import co.electriccoin.zcash.ui.screen.scan.util.ImageUriToQrCodeConverter
|
import co.electriccoin.zcash.ui.screen.scan.util.ImageUriToQrCodeConverter
|
||||||
import co.electriccoin.zcash.ui.screen.scan.util.QrCodeAnalyzer
|
import co.electriccoin.zcash.ui.screen.scan.util.QrCodeAnalyzer
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
@ -111,9 +111,9 @@ fun Scan(
|
||||||
onScanned: (String) -> Unit,
|
onScanned: (String) -> Unit,
|
||||||
onScanError: () -> Unit,
|
onScanError: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onScanStateChanged: (ScanState) -> Unit,
|
onScanStateChanged: (ScanScreenState) -> Unit,
|
||||||
topAppBarSubTitleState: TopAppBarSubTitleState,
|
topAppBarSubTitleState: TopAppBarSubTitleState,
|
||||||
addressValidationResult: AddressType?
|
validationResult: ScanValidationState
|
||||||
) = ZcashTheme(forceDarkMode = true) { // forces dark theme for this screen
|
) = ZcashTheme(forceDarkMode = true) { // forces dark theme for this screen
|
||||||
val permissionState =
|
val permissionState =
|
||||||
if (LocalInspectionMode.current) {
|
if (LocalInspectionMode.current) {
|
||||||
|
@ -134,15 +134,15 @@ fun Scan(
|
||||||
val (scanState, setScanState) =
|
val (scanState, setScanState) =
|
||||||
if (LocalInspectionMode.current) {
|
if (LocalInspectionMode.current) {
|
||||||
remember {
|
remember {
|
||||||
mutableStateOf(ScanState.Scanning)
|
mutableStateOf(ScanScreenState.Scanning)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rememberSaveable {
|
rememberSaveable {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
if (permissionState.status.isGranted) {
|
if (permissionState.status.isGranted) {
|
||||||
ScanState.Scanning
|
ScanScreenState.Scanning
|
||||||
} else {
|
} else {
|
||||||
ScanState.Permission
|
ScanScreenState.Permission
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ fun Scan(
|
||||||
) { _ ->
|
) { _ ->
|
||||||
Box {
|
Box {
|
||||||
ScanMainContent(
|
ScanMainContent(
|
||||||
addressValidationResult = addressValidationResult,
|
validationResult = validationResult,
|
||||||
onScanned = onScanned,
|
onScanned = onScanned,
|
||||||
onScanError = onScanError,
|
onScanError = onScanError,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
|
@ -166,7 +166,7 @@ fun Scan(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
.background(
|
||||||
if (scanState != ScanState.Scanning) {
|
if (scanState != ScanScreenState.Scanning) {
|
||||||
ZcashTheme.colors.cameraDisabledBackgroundColor
|
ZcashTheme.colors.cameraDisabledBackgroundColor
|
||||||
} else {
|
} else {
|
||||||
Color.Black
|
Color.Black
|
||||||
|
@ -177,7 +177,7 @@ fun Scan(
|
||||||
|
|
||||||
ScanTopAppBar(
|
ScanTopAppBar(
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
showBack = scanState != ScanState.Scanning,
|
showBack = scanState != ScanScreenState.Scanning,
|
||||||
subTitleState = topAppBarSubTitleState,
|
subTitleState = topAppBarSubTitleState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -186,8 +186,8 @@ fun Scan(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanBottomItems(
|
fun ScanBottomItems(
|
||||||
addressValidationResult: AddressType?,
|
validationResult: ScanValidationState,
|
||||||
scanState: ScanState,
|
scanState: ScanScreenState,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -195,22 +195,21 @@ fun ScanBottomItems(
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
var failureText: String? = null
|
var failureText: String? = null
|
||||||
|
|
||||||
// Check validation result, if any
|
if (validationResult == ScanValidationState.INVALID) {
|
||||||
if (addressValidationResult is AddressType.Invalid) {
|
|
||||||
failureText = stringResource(id = R.string.scan_address_validation_failed)
|
failureText = stringResource(id = R.string.scan_address_validation_failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permission request result, if any
|
// Check permission request result, if any
|
||||||
failureText =
|
failureText =
|
||||||
when (scanState) {
|
when (scanState) {
|
||||||
ScanState.Permission ->
|
ScanScreenState.Permission ->
|
||||||
stringResource(
|
stringResource(
|
||||||
id = R.string.scan_state_permission,
|
id = R.string.scan_state_permission,
|
||||||
stringResource(id = R.string.app_name)
|
stringResource(id = R.string.app_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
ScanState.Failed -> stringResource(id = R.string.scan_state_failed)
|
ScanScreenState.Failed -> stringResource(id = R.string.scan_state_failed)
|
||||||
ScanState.Scanning -> failureText
|
ScanScreenState.Scanning -> failureText
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failureText != null) {
|
if (failureText != null) {
|
||||||
|
@ -236,7 +235,7 @@ fun ScanBottomItems(
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
when (scanState) {
|
when (scanState) {
|
||||||
ScanState.Scanning, ScanState.Failed -> {
|
ScanScreenState.Scanning, ScanScreenState.Failed -> {
|
||||||
ZashiButton(
|
ZashiButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = onBack,
|
onClick = onBack,
|
||||||
|
@ -244,7 +243,7 @@ fun ScanBottomItems(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanState.Permission -> {
|
ScanScreenState.Permission -> {
|
||||||
ZashiButton(
|
ZashiButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = onOpenSettings,
|
onClick = onOpenSettings,
|
||||||
|
@ -306,20 +305,20 @@ data class FramePosition(
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScanMainContent(
|
private fun ScanMainContent(
|
||||||
addressValidationResult: AddressType?,
|
validationResult: ScanValidationState,
|
||||||
onScanned: (String) -> Unit,
|
onScanned: (String) -> Unit,
|
||||||
onScanError: () -> Unit,
|
onScanError: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onScanStateChanged: (ScanState) -> Unit,
|
onScanStateChanged: (ScanScreenState) -> Unit,
|
||||||
permissionState: PermissionState,
|
permissionState: PermissionState,
|
||||||
scanState: ScanState,
|
scanState: ScanScreenState,
|
||||||
setScanState: (ScanState) -> Unit,
|
setScanState: (ScanScreenState) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
(!permissionState.status.isGranted) -> {
|
(!permissionState.status.isGranted) -> {
|
||||||
setScanState(ScanState.Permission)
|
setScanState(ScanScreenState.Permission)
|
||||||
if (permissionState.status.shouldShowRationale) {
|
if (permissionState.status.shouldShowRationale) {
|
||||||
// Keep dark screen with a link to the app settings - user denied the permission previously
|
// Keep dark screen with a link to the app settings - user denied the permission previously
|
||||||
} else {
|
} else {
|
||||||
|
@ -329,13 +328,13 @@ private fun ScanMainContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(scanState == ScanState.Failed) -> {
|
(scanState == ScanScreenState.Failed) -> {
|
||||||
// Keep current state
|
// Keep current state
|
||||||
}
|
}
|
||||||
|
|
||||||
(permissionState.status.isGranted) -> {
|
(permissionState.status.isGranted) -> {
|
||||||
if (scanState != ScanState.Scanning) {
|
if (scanState != ScanScreenState.Scanning) {
|
||||||
setScanState(ScanState.Scanning)
|
setScanState(ScanScreenState.Scanning)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -411,13 +410,13 @@ private fun ScanMainContent(
|
||||||
val (frame, frameWindow, bottomItems, topAnchor) = createRefs()
|
val (frame, frameWindow, bottomItems, topAnchor) = createRefs()
|
||||||
|
|
||||||
when (scanState) {
|
when (scanState) {
|
||||||
ScanState.Permission -> {
|
ScanScreenState.Permission -> {
|
||||||
// Keep initial ui state
|
// Keep initial ui state
|
||||||
onScanStateChanged(ScanState.Permission)
|
onScanStateChanged(ScanScreenState.Permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanState.Scanning -> {
|
ScanScreenState.Scanning -> {
|
||||||
onScanStateChanged(ScanState.Scanning)
|
onScanStateChanged(ScanScreenState.Scanning)
|
||||||
|
|
||||||
if (!LocalInspectionMode.current) {
|
if (!LocalInspectionMode.current) {
|
||||||
ScanCameraView(
|
ScanCameraView(
|
||||||
|
@ -487,8 +486,8 @@ private fun ScanMainContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanState.Failed -> {
|
ScanScreenState.Failed -> {
|
||||||
onScanStateChanged(ScanState.Failed)
|
onScanStateChanged(ScanScreenState.Failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -542,7 +541,7 @@ private fun ScanMainContent(
|
||||||
.constrainAs(bottomItems) { bottom.linkTo(parent.bottom) }
|
.constrainAs(bottomItems) { bottom.linkTo(parent.bottom) }
|
||||||
) {
|
) {
|
||||||
ScanBottomItems(
|
ScanBottomItems(
|
||||||
addressValidationResult = addressValidationResult,
|
validationResult = validationResult,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
scanState = scanState,
|
scanState = scanState,
|
||||||
|
@ -627,7 +626,7 @@ fun ScanCameraView(
|
||||||
isTorchOn: Boolean,
|
isTorchOn: Boolean,
|
||||||
onScanned: (result: String) -> Unit,
|
onScanned: (result: String) -> Unit,
|
||||||
permissionState: PermissionState,
|
permissionState: PermissionState,
|
||||||
setScanState: (ScanState) -> Unit,
|
setScanState: (ScanScreenState) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
@ -691,7 +690,7 @@ fun ScanCameraView(
|
||||||
).cameraControl
|
).cameraControl
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Twig.error { "Scan QR failed in bind phase with: ${it.message}" }
|
Twig.error { "Scan QR failed in bind phase with: ${it.message}" }
|
||||||
setScanState(ScanState.Failed)
|
setScanState(ScanScreenState.Failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
previewView
|
previewView
|
||||||
|
@ -755,7 +754,7 @@ private fun ScanPreview() =
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
onScanStateChanged = {},
|
onScanStateChanged = {},
|
||||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||||
addressValidationResult = AddressType.Invalid(),
|
validationResult = ScanValidationState.INVALID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
|
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase.Zip321ParseUriValidation
|
||||||
|
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs.ADDRESS_BOOK
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs.DEFAULT
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanResultState
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
internal class ScanViewModel(
|
||||||
|
private val args: ScanNavigationArgs,
|
||||||
|
private val getSynchronizer: GetSynchronizerUseCase,
|
||||||
|
private val zip321ParseUriValidationUseCase: Zip321ParseUriValidationUseCase,
|
||||||
|
) : ViewModel() {
|
||||||
|
val navigateBack = MutableSharedFlow<ScanResultState>()
|
||||||
|
|
||||||
|
val navigateCommand = MutableSharedFlow<String>()
|
||||||
|
|
||||||
|
var state = MutableStateFlow(ScanValidationState.NONE)
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
private var hasBeenScannedSuccessfully = false
|
||||||
|
|
||||||
|
fun onScanned(result: String) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutex.withLock {
|
||||||
|
if (!hasBeenScannedSuccessfully) {
|
||||||
|
val addressValidationResult = getSynchronizer().validateAddress(result)
|
||||||
|
|
||||||
|
val zip321ValidationResult = zip321ParseUriValidationUseCase(result)
|
||||||
|
|
||||||
|
state.update {
|
||||||
|
if (addressValidationResult is AddressType.Valid) {
|
||||||
|
ScanValidationState.INVALID
|
||||||
|
} else if (zip321ValidationResult is Zip321ParseUriValidation.Valid) {
|
||||||
|
ScanValidationState.INVALID
|
||||||
|
} else {
|
||||||
|
ScanValidationState.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zip321ValidationResult is Zip321ParseUriValidation.Valid) {
|
||||||
|
hasBeenScannedSuccessfully = true
|
||||||
|
navigateBack.emit(ScanResultState.Zip321Uri(zip321ValidationResult.zip321Uri))
|
||||||
|
} else if (addressValidationResult is AddressType.Valid) {
|
||||||
|
hasBeenScannedSuccessfully = true
|
||||||
|
|
||||||
|
val serializableAddress = SerializableAddress(result, addressValidationResult)
|
||||||
|
|
||||||
|
when (args) {
|
||||||
|
DEFAULT -> {
|
||||||
|
navigateBack.emit(
|
||||||
|
ScanResultState.Address(
|
||||||
|
Json.encodeToString(
|
||||||
|
SerializableAddress.serializer(),
|
||||||
|
serializableAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ADDRESS_BOOK -> {
|
||||||
|
navigateCommand.emit(AddContactArgs(serializableAddress.address))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onScannedError() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutex.withLock {
|
||||||
|
if (!hasBeenScannedSuccessfully) {
|
||||||
|
state.update { ScanValidationState.INVALID }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,12 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||||
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
|
import cash.z.ecc.android.sdk.model.Memo
|
||||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
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.ZecSend
|
||||||
import cash.z.ecc.android.sdk.model.proposeSend
|
import cash.z.ecc.android.sdk.model.proposeSend
|
||||||
import cash.z.ecc.android.sdk.model.toZecString
|
import cash.z.ecc.android.sdk.model.toZecString
|
||||||
|
@ -50,6 +54,7 @@ import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
import org.zecdev.zip321.ZIP321
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@ -61,6 +66,7 @@ internal fun WrapSend(
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
goBalances: () -> Unit,
|
goBalances: () -> Unit,
|
||||||
goSendConfirmation: (ZecSend) -> Unit,
|
goSendConfirmation: (ZecSend) -> Unit,
|
||||||
|
goPaymentRequest: (ZecSend, String) -> Unit,
|
||||||
goSettings: () -> Unit,
|
goSettings: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val activity = LocalActivity.current
|
val activity = LocalActivity.current
|
||||||
|
@ -100,6 +106,7 @@ internal fun WrapSend(
|
||||||
goBalances = goBalances,
|
goBalances = goBalances,
|
||||||
goSettings = goSettings,
|
goSettings = goSettings,
|
||||||
goSendConfirmation = goSendConfirmation,
|
goSendConfirmation = goSendConfirmation,
|
||||||
|
goPaymentRequest = goPaymentRequest,
|
||||||
hasCameraFeature = hasCameraFeature,
|
hasCameraFeature = hasCameraFeature,
|
||||||
monetarySeparators = monetarySeparators,
|
monetarySeparators = monetarySeparators,
|
||||||
topAppBarSubTitleState = walletState,
|
topAppBarSubTitleState = walletState,
|
||||||
|
@ -119,6 +126,7 @@ internal fun WrapSend(
|
||||||
goBalances: () -> Unit,
|
goBalances: () -> Unit,
|
||||||
goSettings: () -> Unit,
|
goSettings: () -> Unit,
|
||||||
goSendConfirmation: (ZecSend) -> Unit,
|
goSendConfirmation: (ZecSend) -> Unit,
|
||||||
|
goPaymentRequest: (ZecSend, String) -> Unit,
|
||||||
hasCameraFeature: Boolean,
|
hasCameraFeature: Boolean,
|
||||||
monetarySeparators: MonetarySeparators,
|
monetarySeparators: MonetarySeparators,
|
||||||
onHideBalances: () -> Unit,
|
onHideBalances: () -> Unit,
|
||||||
|
@ -156,6 +164,25 @@ internal fun WrapSend(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zip321 Uri scan result processing
|
||||||
|
if (sendArguments?.zip321Uri != null &&
|
||||||
|
synchronizer != null &&
|
||||||
|
spendingKey != null
|
||||||
|
) {
|
||||||
|
LaunchedEffect(goPaymentRequest) {
|
||||||
|
scope.launch {
|
||||||
|
processZip321Result(
|
||||||
|
zip321Uri = sendArguments.zip321Uri,
|
||||||
|
synchronizer = synchronizer,
|
||||||
|
account = spendingKey.account,
|
||||||
|
setSendStage = setSendStage,
|
||||||
|
setZecSend = setZecSend,
|
||||||
|
goPaymentRequest = goPaymentRequest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val existingContact: MutableState<AddressBookContact?> = remember { mutableStateOf(null) }
|
val existingContact: MutableState<AddressBookContact?> = remember { mutableStateOf(null) }
|
||||||
var isHintVisible by remember { mutableStateOf(false) }
|
var isHintVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
@ -356,3 +383,68 @@ internal fun WrapSend(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun processZip321Result(
|
||||||
|
zip321Uri: String,
|
||||||
|
synchronizer: Synchronizer,
|
||||||
|
account: Account,
|
||||||
|
setSendStage: (SendStage) -> Unit,
|
||||||
|
setZecSend: (ZecSend?) -> Unit,
|
||||||
|
goPaymentRequest: (ZecSend, String) -> Unit,
|
||||||
|
) {
|
||||||
|
val request =
|
||||||
|
runCatching {
|
||||||
|
// At this point there should by only a valid Zcash address coming
|
||||||
|
ZIP321.request(zip321Uri, null)
|
||||||
|
}.onFailure {
|
||||||
|
Twig.error(it) { "Failed to validate address" }
|
||||||
|
}.getOrElse {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
val payment =
|
||||||
|
when (request) {
|
||||||
|
// We support only one payment currently
|
||||||
|
is ZIP321.ParserResult.Request -> {
|
||||||
|
request.paymentRequest.payments[0]
|
||||||
|
}
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
val address =
|
||||||
|
synchronizer
|
||||||
|
.validateAddress(payment.recipientAddress.value)
|
||||||
|
.toWalletAddress(payment.recipientAddress.value)
|
||||||
|
|
||||||
|
val amount = payment.nonNegativeAmount.value.convertZecToZatoshi()
|
||||||
|
|
||||||
|
val memo = Memo(payment.memo?.let { String(it.data, Charsets.UTF_8) } ?: "")
|
||||||
|
|
||||||
|
val zecSend =
|
||||||
|
ZecSend(
|
||||||
|
destination = address,
|
||||||
|
amount = amount,
|
||||||
|
memo = memo,
|
||||||
|
proposal = null
|
||||||
|
)
|
||||||
|
setZecSend(zecSend)
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
synchronizer.proposeFulfillingPaymentUri(account, zip321Uri)
|
||||||
|
}.onSuccess { proposal ->
|
||||||
|
Twig.debug { "Transaction proposal from Zip321 Uri: ${proposal.toPrettyString()}" }
|
||||||
|
val enrichedZecSend = zecSend.copy(proposal = proposal)
|
||||||
|
setZecSend(enrichedZecSend)
|
||||||
|
goPaymentRequest(enrichedZecSend, zip321Uri)
|
||||||
|
}.onFailure {
|
||||||
|
Twig.error(it) { "Transaction proposal from Zip321 Uri failed" }
|
||||||
|
setSendStage(SendStage.SendFailure(it.message ?: ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun AddressType.toWalletAddress(value: String) =
|
||||||
|
when (this) {
|
||||||
|
AddressType.Unified -> WalletAddress.Unified.new(value)
|
||||||
|
AddressType.Shielded -> WalletAddress.Sapling.new(value)
|
||||||
|
AddressType.Transparent -> WalletAddress.Transparent.new(value)
|
||||||
|
else -> error("Invalid address type")
|
||||||
|
}
|
||||||
|
|
|
@ -2,5 +2,6 @@ package co.electriccoin.zcash.ui.screen.send.model
|
||||||
|
|
||||||
data class SendArguments(
|
data class SendArguments(
|
||||||
val recipientAddress: RecipientAddressState? = null,
|
val recipientAddress: RecipientAddressState? = null,
|
||||||
|
val zip321Uri: String? = null,
|
||||||
val clearForm: Boolean = false,
|
val clearForm: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:pathData="M5,7.5L10,12.5L15,7.5"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#4D4941"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:pathData="M15,12.5L10,7.5L5,12.5"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#4D4941"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:pathData="M10,12.917H6.25C5.087,12.917 4.505,12.917 4.032,13.06C2.967,13.383 2.133,14.217 1.81,15.282C1.667,15.755 1.667,16.337 1.667,17.5M15.833,17.5V12.5M13.333,15H18.333M12.083,6.25C12.083,8.321 10.404,10 8.333,10C6.262,10 4.583,8.321 4.583,6.25C4.583,4.179 6.262,2.5 8.333,2.5C10.404,2.5 12.083,4.179 12.083,6.25Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#4D4941"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="payment_request_title">Payment Request</string>
|
||||||
|
<string name="payment_request_close_content_description">Close</string>
|
||||||
|
|
||||||
|
<string name="payment_request_requested_by">Requested By</string>
|
||||||
|
<string name="payment_request_btn_show_address">Show</string>
|
||||||
|
<string name="payment_request_btn_hide_address">Hide</string>
|
||||||
|
<string name="payment_request_btn_save_contact">Save</string>
|
||||||
|
|
||||||
|
<string name="payment_request_memo">For:</string>
|
||||||
|
|
||||||
|
<string name="payment_request_fee">Fee</string>
|
||||||
|
<string name="payment_request_total">Total</string>
|
||||||
|
|
||||||
|
<string name="payment_request_send_btn">Send</string>
|
||||||
|
|
||||||
|
<string name="payment_request_dialog_error_title">Transaction Failed</string>
|
||||||
|
<string name="payment_request_dialog_error_text">An error occurred and the attempt to send funds failed. Try it again, please.</string>
|
||||||
|
<string name="payment_request_dialog_error_ok_btn">OK</string>
|
||||||
|
<string name="payment_request_dialog_error_report_btn">Report</string>
|
||||||
|
|
||||||
|
<string name="payment_request_dialog_error_grpc_title">Connection Error</string>
|
||||||
|
<string name="payment_request_dialog_error_grpc_text">Zashi encountered some connection issues when submitting
|
||||||
|
the transaction to the network. It will retry during the next few minutes.</string>
|
||||||
|
<string name="payment_request_dialog_error_grpc_btn">OK</string>
|
||||||
|
|
||||||
|
<string name="payment_request_send_failed_report_unable_open_email">Unable to launch email app.</string>
|
||||||
|
</resources>
|
Loading…
Reference in New Issue