From 0a90b90bc790a17d4a8fbf567b342c81da5dac43 Mon Sep 17 00:00:00 2001 From: Michael T Chuang Date: Mon, 4 Nov 2024 20:59:27 -0800 Subject: [PATCH] PERA-1060 :: Staking WIP snapshot --- app/build.gradle | 2 + .../java/com/algorand/android/MainActivity.kt | 4 + .../com/algorand/android/MainViewModel.kt | 21 +++ .../customviews/CoreActionsTabBarView.kt | 6 +- .../domain/usecase/DeviceIdUseCase.kt | 4 +- .../home/domain/PeraMobileWebInterface.kt | 2 + .../android/discover/utils/DiscoverJsUtils.kt | 3 + .../modules/accounts/ui/AccountsFragment.kt | 5 + .../accounts/ui/adapter/AccountAdapter.kt | 5 + .../AccountsQuickActionsViewHolder.kt | 3 +- .../GetAuthorizedAddressesWebMessage.kt | 17 ++ ...etAuthorizedAddressesWebMessagesUseCase.kt | 47 +++++ .../perawebview/GetDeviceIdWebMessage.kt | 17 ++ .../GetDeviceIdWebMessageUseCase.kt | 27 +++ .../perawebview/ParseOpenSystemBrowserUrl.kt | 17 ++ .../ParseOpenSystemBrowserUrlUseCase.kt | 26 +++ .../perawebview/PeraWebMessageAction.kt | 18 ++ .../perawebview/PeraWebMessageBuilder.kt | 17 ++ .../perawebview/PeraWebMessageBuilderImpl.kt | 35 ++++ .../modules/perawebview/WebViewThemeHelper.kt | 52 +++++ .../perawebview/model/PeraWebMessage.kt | 22 +++ .../modules/staking/StakingFragment.kt | 177 ++++++++++++++++++ .../modules/staking/StakingViewModel.kt | 79 ++++++++ .../modules/staking/di/StakingModule.kt | 50 +++++ .../modules/staking/model/StakingPreview.kt | 21 +++ .../layout/custom_core_actions_tab_bar.xml | 45 +++-- app/src/main/res/layout/fragment_staking.xml | 36 ++++ .../layout/item_accounts_quick_actions.xml | 6 +- .../main/res/navigation/home_navigation.xml | 13 ++ app/src/main/res/values/strings.xml | 3 + 30 files changed, 754 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/GetAuthorizedAddressesWebMessage.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/GetAuthorizedAddressesWebMessagesUseCase.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/GetDeviceIdWebMessage.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/GetDeviceIdWebMessageUseCase.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/ParseOpenSystemBrowserUrl.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/ParseOpenSystemBrowserUrlUseCase.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageAction.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageBuilder.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageBuilderImpl.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/WebViewThemeHelper.kt create mode 100644 app/src/main/java/com/algorand/android/modules/perawebview/model/PeraWebMessage.kt create mode 100644 app/src/main/java/com/algorand/android/modules/staking/StakingFragment.kt create mode 100644 app/src/main/java/com/algorand/android/modules/staking/StakingViewModel.kt create mode 100644 app/src/main/java/com/algorand/android/modules/staking/di/StakingModule.kt create mode 100644 app/src/main/java/com/algorand/android/modules/staking/model/StakingPreview.kt create mode 100644 app/src/main/res/layout/fragment_staking.xml diff --git a/app/build.gradle b/app/build.gradle index 225c1d09b..c1089d6b3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -128,6 +128,7 @@ android { buildConfigField "String", "DISCOVER_URL", '"https://discover-mobile-staging.perawallet.app/"' buildConfigField "String", "DISCOVER_BROWSE_DAPP_URL", '"https://discover-mobile-staging.perawallet.app/main/browser"' buildConfigField "String", "MELD_URL", '"https://mainnet.staging.api.perawallet.app/v1/onramp-services/meld/redirect-to-fluidmoney/?walletAddress="' + buildConfigField "String", "STAKING_URL", '"https://staking-mobile-staging.perawallet.app"' buildConfigField "String", "NODE_MAINNET_URL", apiUrlProps["NODE_MAINNET_URL"] buildConfigField "String", "NODE_TESTNET_URL", apiUrlProps["NODE_TESTNET_URL"] buildConfigField "String", "INDEXER_MAINNET_URL", apiUrlProps["INDEXER_MAINNET_URL"] @@ -158,6 +159,7 @@ android { buildConfigField "String", "DISCOVER_URL", '"https://discover-mobile.perawallet.app/"' buildConfigField "String", "DISCOVER_BROWSE_DAPP_URL", '"https://discover-mobile.perawallet.app/main/browser"' buildConfigField "String", "MELD_URL", '"https://mainnet.api.perawallet.app/v1/onramp-services/meld/redirect-to-fluidmoney/?walletAddress="' + buildConfigField "String", "STAKING_URL", '"https://staking-mobile.perawallet.app"' buildConfigField "String", "NODE_MAINNET_URL", apiUrlProps["NODE_MAINNET_URL"] buildConfigField "String", "NODE_TESTNET_URL", apiUrlProps["NODE_TESTNET_URL"] buildConfigField "String", "INDEXER_MAINNET_URL", apiUrlProps["INDEXER_MAINNET_URL"] diff --git a/app/src/main/java/com/algorand/android/MainActivity.kt b/app/src/main/java/com/algorand/android/MainActivity.kt index 61d891a69..41bb0748f 100644 --- a/app/src/main/java/com/algorand/android/MainActivity.kt +++ b/app/src/main/java/com/algorand/android/MainActivity.kt @@ -678,6 +678,10 @@ class MainActivity : override fun onBrowseDappsClick() { handleBrowseDappsClick() } + + override fun onStakingClick() { + nav(HomeNavigationDirections.actionGlobalStakingFragment()) + } }) } diff --git a/app/src/main/java/com/algorand/android/MainViewModel.kt b/app/src/main/java/com/algorand/android/MainViewModel.kt index e6f1d275e..98c9fa543 100644 --- a/app/src/main/java/com/algorand/android/MainViewModel.kt +++ b/app/src/main/java/com/algorand/android/MainViewModel.kt @@ -261,6 +261,27 @@ class MainViewModel @Inject constructor( } } + fun onStakingButtonClick() { + viewModelScope.launch { + mainActivityEventTracker.logQuickActionSwapButtonClickEvent() + var swapNavDirection: NavDirections? = null + swapNavigationDestinationHelper.getSwapNavigationDestination( + onNavToIntroduction = { + swapNavDirection = HomeNavigationDirections.actionGlobalSwapIntroductionNavigation() + }, + onNavToAccountSelection = { + swapNavDirection = HomeNavigationDirections.actionGlobalSwapAccountSelectionNavigation() + }, + onNavToSwap = { accountAddress -> + swapNavDirection = HomeNavigationDirections.actionGlobalSwapNavigation(accountAddress) + } + ) + swapNavDirection?.let { direction -> + _swapNavigationResultFlow.emit(Event(direction)) + } + } + } + private fun initializeTutorial() { viewModelScope.launch { tutorialUseCase.initializeTutorial() diff --git a/app/src/main/java/com/algorand/android/customviews/CoreActionsTabBarView.kt b/app/src/main/java/com/algorand/android/customviews/CoreActionsTabBarView.kt index 8559c0124..671ef5071 100644 --- a/app/src/main/java/com/algorand/android/customviews/CoreActionsTabBarView.kt +++ b/app/src/main/java/com/algorand/android/customviews/CoreActionsTabBarView.kt @@ -34,13 +34,14 @@ class CoreActionsTabBarView @JvmOverloads constructor( init { with(binding) { coreActionsButton.setOnClickListener { onCoreActionsButtonClick() } - sendButton.setOnClickListener { listener?.onSendClick() } + // sendButton.setOnClickListener { listener?.onSendClick() } receiveButton.setOnClickListener { listener?.onReceiveClick() } buySellButton.setOnClickListener { listener?.onBuySellClick() } - scanQrButton.setOnClickListener { listener?.onScanQRClick() } + // scanQrButton.setOnClickListener { listener?.onScanQRClick() } swapButton.setOnClickListener { listener?.onSwapClick() } browseDAppsButton.setOnClickListener { listener?.onBrowseDappsClick() } backgroundColorView.setOnClickListener { startHidingAnimation() } + stakingButton.setOnClickListener { listener?.onStakingClick() } } } @@ -108,5 +109,6 @@ class CoreActionsTabBarView @JvmOverloads constructor( fun onCoreActionsClick(isCoreActionsOpen: Boolean) fun onSwapClick() fun onBrowseDappsClick() + fun onStakingClick() } } diff --git a/app/src/main/java/com/algorand/android/deviceregistration/domain/usecase/DeviceIdUseCase.kt b/app/src/main/java/com/algorand/android/deviceregistration/domain/usecase/DeviceIdUseCase.kt index 2ab9ff82d..cbdd49710 100644 --- a/app/src/main/java/com/algorand/android/deviceregistration/domain/usecase/DeviceIdUseCase.kt +++ b/app/src/main/java/com/algorand/android/deviceregistration/domain/usecase/DeviceIdUseCase.kt @@ -26,7 +26,7 @@ class DeviceIdUseCase @Inject constructor( private val getActiveNodeUseCase: GetActiveNodeUseCase ) { - suspend fun getSelectedNodeDeviceId(): String? { + fun getSelectedNodeDeviceId(): String? { return when (getSelectedNetworkSlug()) { MAINNET_NETWORK_SLUG -> userDeviceIdRepository.getMainnetDeviceId() TESTNET_NETWORK_SLUG -> userDeviceIdRepository.getTestnetDeviceId() @@ -42,7 +42,7 @@ class DeviceIdUseCase @Inject constructor( } } - suspend fun setSelectedNodeDeviceId(deviceId: String?) { + fun setSelectedNodeDeviceId(deviceId: String?) { when (getSelectedNetworkSlug()) { MAINNET_NETWORK_SLUG -> userDeviceIdRepository.setMainnetDeviceId(deviceId) TESTNET_NETWORK_SLUG -> userDeviceIdRepository.setTestnetDeviceId(deviceId) diff --git a/app/src/main/java/com/algorand/android/discover/home/domain/PeraMobileWebInterface.kt b/app/src/main/java/com/algorand/android/discover/home/domain/PeraMobileWebInterface.kt index 12fbddb9a..7ae854726 100644 --- a/app/src/main/java/com/algorand/android/discover/home/domain/PeraMobileWebInterface.kt +++ b/app/src/main/java/com/algorand/android/discover/home/domain/PeraMobileWebInterface.kt @@ -53,6 +53,8 @@ class PeraMobileWebInterface private constructor(val listener: WebInterfaceListe fun handleTokenDetailActionButtonClick(jsonEncodedPayload: String) {} fun getDeviceId() {} fun openSystemBrowser(jsonEncodedPayload: String) {} + fun getAuthorizedAddresses() {} + fun closePeraCards() {} } companion object { diff --git a/app/src/main/java/com/algorand/android/discover/utils/DiscoverJsUtils.kt b/app/src/main/java/com/algorand/android/discover/utils/DiscoverJsUtils.kt index efd1e3fc4..f216cfe60 100644 --- a/app/src/main/java/com/algorand/android/discover/utils/DiscoverJsUtils.kt +++ b/app/src/main/java/com/algorand/android/discover/utils/DiscoverJsUtils.kt @@ -18,6 +18,9 @@ import com.google.gson.Gson @Suppress("MaxLineLength") const val JAVASCRIPT_PERACONNECT = "function setupPeraConnectObserver(){const e=new MutationObserver(()=>{const t=document.getElementById(\"pera-wallet-connect-modal-wrapper\"),e=document.getElementById(\"pera-wallet-redirect-modal-wrapper\");if(e&&e.remove(),t){const o=t.getElementsByTagName(\"pera-wallet-connect-modal\");let e=\"\";if(o&&o[0]&&o[0].shadowRoot){const r=o[0].shadowRoot.querySelector(\"pera-wallet-modal-touch-screen-mode\").shadowRoot.querySelector(\"#pera-wallet-connect-modal-touch-screen-mode-launch-pera-wallet-button\");r&&(e=r.getAttribute(\"href\"))}else{const n=t.getElementsByClassName(\"pera-wallet-connect-modal-touch-screen-mode__launch-pera-wallet-button\");n&&(e=n[0].getAttribute(\"href\"))}e&&(e=e.replace(/&browser=\\w+/,\"\"),window.open(e)),t.remove()}});e.disconnect(),e.observe(document.body,{childList:!0,subtree:!0})}setupPeraConnectObserver();" +@Suppress("MaxLineLength") +const val JAVASCRIPT_NAVIGATION = "!function(t){function e(t){setTimeout((function(){window.webkit.messageHandlers.navigation.postMessage(t)}),0)}function n(n){return function(){return e(\"other\"),n.apply(t,arguments)}}t.pushState=n(t.pushState),t.replaceState=n(t.replaceState),window.addEventListener(\"popstate\",(function(){e(\"backforward\")}))}(window.history);" + private const val BROWSER_FAVORITE_BUTTON_CLICK_ACTION = "handleBrowserFavoriteButtonClick" private const val GET_DEVICE_ID_ACTION = "getDeviceId" diff --git a/app/src/main/java/com/algorand/android/modules/accounts/ui/AccountsFragment.kt b/app/src/main/java/com/algorand/android/modules/accounts/ui/AccountsFragment.kt index f2be9ef67..f9634fa56 100644 --- a/app/src/main/java/com/algorand/android/modules/accounts/ui/AccountsFragment.kt +++ b/app/src/main/java/com/algorand/android/modules/accounts/ui/AccountsFragment.kt @@ -134,6 +134,11 @@ class AccountsFragment : DaggerBaseFragment(R.layout.fragment_accounts), override fun onAddAccountClick() { this@AccountsFragment.onAddAccountClick() } + + override fun onStakingClick() { + // TODO refactor with a better name for logging + nav(AccountsFragmentDirections.actionAccountsFragmentToStakingFragment()) + } } private val accountAdapter: AccountAdapter = AccountAdapter(accountAdapterListener = accountAdapterListener) diff --git a/app/src/main/java/com/algorand/android/modules/accounts/ui/adapter/AccountAdapter.kt b/app/src/main/java/com/algorand/android/modules/accounts/ui/adapter/AccountAdapter.kt index e2d1e67b6..4fe2a8135 100644 --- a/app/src/main/java/com/algorand/android/modules/accounts/ui/adapter/AccountAdapter.kt +++ b/app/src/main/java/com/algorand/android/modules/accounts/ui/adapter/AccountAdapter.kt @@ -109,6 +109,10 @@ class AccountAdapter( override fun onScanQrClick() { accountAdapterListener.onScanQrClick() } + + override fun onStakingClick() { + accountAdapterListener.onStakingClick() + } } override fun getItemViewType(position: Int): Int { @@ -141,6 +145,7 @@ class AccountAdapter( fun onBackupBannerActionButtonClick() fun onBuySellClick() fun onSendClick() + fun onStakingClick() fun onSwapClick() fun onScanQrClick() fun onSortClick() diff --git a/app/src/main/java/com/algorand/android/modules/accounts/ui/viewholder/AccountsQuickActionsViewHolder.kt b/app/src/main/java/com/algorand/android/modules/accounts/ui/viewholder/AccountsQuickActionsViewHolder.kt index 00ea4da7e..adbb66ca8 100644 --- a/app/src/main/java/com/algorand/android/modules/accounts/ui/viewholder/AccountsQuickActionsViewHolder.kt +++ b/app/src/main/java/com/algorand/android/modules/accounts/ui/viewholder/AccountsQuickActionsViewHolder.kt @@ -26,7 +26,7 @@ class AccountsQuickActionsViewHolder( override fun bind(item: BaseAccountListItem) { if (item !is BaseAccountListItem.QuickActionsItem) return with(binding) { - buySellButton.setOnClickListener { listener.onBuySellClick() } + stakingButton.setOnClickListener { listener.onStakingClick() } sendButton.setOnClickListener { listener.onSendClick() } swapButton.apply { isSelected = item.isSwapButtonSelected @@ -41,6 +41,7 @@ class AccountsQuickActionsViewHolder( fun onSendClick() fun onSwapClick() fun onScanQrClick() + fun onStakingClick() } companion object { diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/GetAuthorizedAddressesWebMessage.kt b/app/src/main/java/com/algorand/android/modules/perawebview/GetAuthorizedAddressesWebMessage.kt new file mode 100644 index 000000000..8f67296fd --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/GetAuthorizedAddressesWebMessage.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +fun interface GetAuthorizedAddressesWebMessage { + suspend operator fun invoke(): String +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/GetAuthorizedAddressesWebMessagesUseCase.kt b/app/src/main/java/com/algorand/android/modules/perawebview/GetAuthorizedAddressesWebMessagesUseCase.kt new file mode 100644 index 000000000..76bb1293e --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/GetAuthorizedAddressesWebMessagesUseCase.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +import com.algorand.android.modules.peraserializer.PeraSerializer +import com.algorand.android.usecase.AccountAlgoAmountUseCase +import com.algorand.android.usecase.GetLocalAccountsUseCase +import com.google.crypto.tink.subtle.Base64 +import javax.inject.Inject + +class GetAuthorizedAddressesWebMessagesUseCase @Inject constructor( + private val localAccountsUseCase: GetLocalAccountsUseCase, + private val peraSerializer: PeraSerializer, + private val peraWebMessageBuilder: PeraWebMessageBuilder, + private val accountAlgoAmountUseCase: AccountAlgoAmountUseCase +) : GetAuthorizedAddressesWebMessage { + + override suspend fun invoke(): String { + val addressNameMap = getAddressNameMap() + val messagePayload = getMessagePayload(addressNameMap) + return peraWebMessageBuilder.buildMessage(PeraWebMessageAction.GET_AUTHORIZED_ADDRESSES, messagePayload) + } + + private fun getAddressNameMap(): List> { + val localAccounts = localAccountsUseCase.getLocalAccountsThatCanSignTransaction() + val sortedAddressAlgoBalanceMap = localAccounts.map { + it to accountAlgoAmountUseCase.getAccountAlgoAmount(it.address).amount + }.sortedByDescending { (_, algoBalance) -> algoBalance } + return sortedAddressAlgoBalanceMap.map { (account, _) -> + mapOf(account.address to account.name) + } + } + + private fun getMessagePayload(addressNameMap: List>): String { + return Base64.encode(peraSerializer.toJson(addressNameMap).toByteArray()) + } +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/GetDeviceIdWebMessage.kt b/app/src/main/java/com/algorand/android/modules/perawebview/GetDeviceIdWebMessage.kt new file mode 100644 index 000000000..3db08ed02 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/GetDeviceIdWebMessage.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +fun interface GetDeviceIdWebMessage { + operator fun invoke(): String? +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/GetDeviceIdWebMessageUseCase.kt b/app/src/main/java/com/algorand/android/modules/perawebview/GetDeviceIdWebMessageUseCase.kt new file mode 100644 index 000000000..b6bb94a3a --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/GetDeviceIdWebMessageUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +import com.algorand.android.deviceregistration.domain.usecase.DeviceIdUseCase +import javax.inject.Inject + +class GetDeviceIdWebMessageUseCase @Inject constructor( + private val deviceIdUseCase: DeviceIdUseCase, + private val peraWebMessageBuilder: PeraWebMessageBuilder +) : GetDeviceIdWebMessage { + + override fun invoke(): String? { + val deviceId = deviceIdUseCase.getSelectedNodeDeviceId() ?: return null + return peraWebMessageBuilder.buildMessage(PeraWebMessageAction.GET_DEVICE_ID, deviceId) + } +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/ParseOpenSystemBrowserUrl.kt b/app/src/main/java/com/algorand/android/modules/perawebview/ParseOpenSystemBrowserUrl.kt new file mode 100644 index 000000000..6f5f29b78 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/ParseOpenSystemBrowserUrl.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +fun interface ParseOpenSystemBrowserUrl { + operator fun invoke(jsonPayload: String): String? +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/ParseOpenSystemBrowserUrlUseCase.kt b/app/src/main/java/com/algorand/android/modules/perawebview/ParseOpenSystemBrowserUrlUseCase.kt new file mode 100644 index 000000000..ca1484a4e --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/ParseOpenSystemBrowserUrlUseCase.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +import com.algorand.android.discover.common.ui.model.OpenSystemBrowserRequest +import com.algorand.android.modules.peraserializer.PeraSerializer +import javax.inject.Inject + +class ParseOpenSystemBrowserUrlUseCase @Inject constructor( + private val peraSerializer: PeraSerializer +) : ParseOpenSystemBrowserUrl { + + override fun invoke(jsonPayload: String): String? { + return peraSerializer.fromJson(jsonPayload, OpenSystemBrowserRequest::class.java)?.url + } +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageAction.kt b/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageAction.kt new file mode 100644 index 000000000..fc7685db8 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageAction.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +enum class PeraWebMessageAction(val key: String) { + GET_AUTHORIZED_ADDRESSES("getAuthorizedAddresses"), + GET_DEVICE_ID("getDeviceId") +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageBuilder.kt b/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageBuilder.kt new file mode 100644 index 000000000..122a16ae3 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageBuilder.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +interface PeraWebMessageBuilder { + fun buildMessage(action: PeraWebMessageAction, payload: String): String +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageBuilderImpl.kt b/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageBuilderImpl.kt new file mode 100644 index 000000000..24dab8f44 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/PeraWebMessageBuilderImpl.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +import com.algorand.android.modules.perawebview.model.PeraWebMessage +import com.algorand.android.modules.peraserializer.PeraSerializer +import javax.inject.Inject + +class PeraWebMessageBuilderImpl @Inject constructor( + private val peraSerializer: PeraSerializer +) : PeraWebMessageBuilder { + + override fun buildMessage(action: PeraWebMessageAction, payload: String): String { + val messagePayload = getMessagePayload(action, payload) + val messageJson = peraSerializer.toJson(messagePayload) + return "handleMessage('$messageJson')" + } + + private fun getMessagePayload(action: PeraWebMessageAction, payload: String): PeraWebMessage { + return PeraWebMessage( + action = action.key, + payload = payload + ) + } +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/WebViewThemeHelper.kt b/app/src/main/java/com/algorand/android/modules/perawebview/WebViewThemeHelper.kt new file mode 100644 index 000000000..3bbf04fab --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/WebViewThemeHelper.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview + +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Configuration +import android.os.Build +import android.webkit.WebSettings +import android.webkit.WebView +import com.algorand.android.discover.common.ui.model.WebViewTheme +import com.algorand.android.utils.preference.ThemePreference +import com.algorand.android.utils.preference.getSavedThemePreference +import javax.inject.Inject + +class WebViewThemeHelper @Inject constructor(private val sharedPreferences: SharedPreferences) { + + fun initWebViewTheme(webView: WebView) { + with(webView.settings) { + val themePreference = sharedPreferences.getSavedThemePreference() + val isDarkMode = getWebViewThemeFromThemePreference(webView.context, themePreference) == WebViewTheme.DARK + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + isAlgorithmicDarkeningAllowed = isDarkMode + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + forceDark = if (isDarkMode) WebSettings.FORCE_DARK_ON else WebSettings.FORCE_DARK_OFF + return + } + } + } + + private fun getWebViewThemeFromThemePreference(context: Context, themePreference: ThemePreference): WebViewTheme { + val themeFromSystem = when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES -> WebViewTheme.DARK + Configuration.UI_MODE_NIGHT_NO -> WebViewTheme.LIGHT + else -> null + } + return WebViewTheme.getByThemePreference(themePreference, themeFromSystem) + } +} diff --git a/app/src/main/java/com/algorand/android/modules/perawebview/model/PeraWebMessage.kt b/app/src/main/java/com/algorand/android/modules/perawebview/model/PeraWebMessage.kt new file mode 100644 index 000000000..5bc5d4e38 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/perawebview/model/PeraWebMessage.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.perawebview.model + +import com.google.gson.annotations.SerializedName + +data class PeraWebMessage( + @SerializedName("action") + val action: String, + @SerializedName("payload") + val payload: String +) diff --git a/app/src/main/java/com/algorand/android/modules/staking/StakingFragment.kt b/app/src/main/java/com/algorand/android/modules/staking/StakingFragment.kt new file mode 100644 index 000000000..3e4d61f6c --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/staking/StakingFragment.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.staking + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.algorand.android.BuildConfig.STAKING_URL +import com.algorand.android.R +import com.algorand.android.databinding.FragmentStakingBinding +import com.algorand.android.discover.common.ui.model.PeraWebChromeClient +import com.algorand.android.discover.common.ui.model.PeraWebViewClient +import com.algorand.android.discover.common.ui.model.WebViewError +import com.algorand.android.discover.common.ui.model.WebViewError.HTTP_ERROR +import com.algorand.android.discover.common.ui.model.WebViewError.NO_CONNECTION +import com.algorand.android.discover.home.domain.PeraMobileWebInterface +import com.algorand.android.discover.home.domain.PeraMobileWebInterface.Companion.WEB_INTERFACE_NAME +import com.algorand.android.discover.utils.JAVASCRIPT_NAVIGATION +import com.algorand.android.discover.utils.JAVASCRIPT_PERACONNECT +import com.algorand.android.models.FragmentConfiguration +import com.algorand.android.models.ScreenState +import com.algorand.android.modules.perawebview.WebViewThemeHelper +import com.algorand.android.modules.perawebview.ui.BasePeraWebViewFragment +import com.algorand.android.modules.perawebview.ui.BasePeraWebViewViewModel +import com.algorand.android.utils.Event +import com.algorand.android.utils.browser.openExternalBrowserApp +import com.algorand.android.utils.extensions.collectLatestOnLifecycle +import com.algorand.android.utils.extensions.hide +import com.algorand.android.utils.extensions.show +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapNotNull + +@AndroidEntryPoint +class StakingFragment : BasePeraWebViewFragment(R.layout.fragment_staking), + PeraMobileWebInterface.WebInterfaceListener { + + private val stakingViewModel: StakingViewModel by viewModels() + + override lateinit var binding: FragmentStakingBinding + + override val fragmentConfiguration: FragmentConfiguration = FragmentConfiguration() + + @Inject + lateinit var webViewThemeHelper: WebViewThemeHelper + + override fun bindWebView(view: View?) { + view?.let { binding = FragmentStakingBinding.bind(it) } + } + + override val basePeraWebViewViewModel: BasePeraWebViewViewModel + get() = stakingViewModel + + private val sendMessageEventCollector: suspend (Event) -> Unit = { + it.consume()?.let { message -> + sendWebMessage(message) + } + } + + private val errorEventCollector: suspend (Event) -> Unit = { + it.consume()?.let { error -> + handleWebViewError(error) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = super.onCreateView(inflater, container, savedInstanceState) + initWebViewTheme() + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUi() + initObservers() + loadStakingUrl() + } + + private fun initUi() { + initWebView() + with(binding) { + screenStateView.setOnNeutralButtonClickListener { + screenStateView.hide() + webView.show() + webView.loadUrl(STAKING_URL) + } + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun initWebView() { + with(binding) { + val peraWebInterface = PeraMobileWebInterface.create(this@StakingFragment) + webView.addJavascriptInterface(peraWebInterface, WEB_INTERFACE_NAME) + webView.webViewClient = PeraWebViewClient(peraWebViewClientListener) + webView.webChromeClient = PeraWebChromeClient(peraWebViewClientListener) + webView.evaluateJavascript(JAVASCRIPT_NAVIGATION, null) + webView.evaluateJavascript(JAVASCRIPT_PERACONNECT, null) + } + } + + private fun loadStakingUrl() { + with(binding.webView) { + if (url == null) loadUrl(STAKING_URL) + } + } + + private fun initObservers() { + collectLatestOnLifecycle( + stakingViewModel.cardsPreviewFlow.mapNotNull { it?.sendMessageEvent }.distinctUntilChanged(), + sendMessageEventCollector + ) + collectLatestOnLifecycle( + stakingViewModel.cardsPreviewFlow.mapNotNull { it?.errorEvent }.distinctUntilChanged(), + errorEventCollector + ) + } + + override fun getAuthorizedAddresses() { + stakingViewModel.getAuthorizedAddresses() + } + + override fun getDeviceId() { + stakingViewModel.getDeviceId() + } + + override fun closePeraCards() { + binding.root.post { + findNavController().navigateUp() + } + } + + override fun openSystemBrowser(jsonEncodedPayload: String) { + stakingViewModel.getOpenSystemBrowserUrl(jsonEncodedPayload)?.let { url -> + context?.openExternalBrowserApp(url) + } + } + + private fun handleWebViewError(error: WebViewError) { + val errorState = when (error) { + NO_CONNECTION -> ScreenState.ConnectionError() + HTTP_ERROR -> ScreenState.DefaultError() + } + with(binding) { + screenStateView.setupUi(errorState) + screenStateView.show() + webView.hide() + } + } + + private fun sendWebMessage(message: String) { + binding.webView.post { + binding.webView.evaluateJavascript(message, null) + } + } + + private fun initWebViewTheme() { + getWebView(binding.root)?.let { currentWebView -> + webViewThemeHelper.initWebViewTheme(currentWebView) + } + } +} diff --git a/app/src/main/java/com/algorand/android/modules/staking/StakingViewModel.kt b/app/src/main/java/com/algorand/android/modules/staking/StakingViewModel.kt new file mode 100644 index 000000000..dca413644 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/staking/StakingViewModel.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.staking + +import androidx.lifecycle.viewModelScope +import com.algorand.android.discover.common.ui.model.WebViewError +import com.algorand.android.modules.staking.model.StakingPreview +import com.algorand.android.modules.perawebview.GetAuthorizedAddressesWebMessage +import com.algorand.android.modules.perawebview.GetDeviceIdWebMessage +import com.algorand.android.modules.perawebview.ParseOpenSystemBrowserUrl +import com.algorand.android.modules.perawebview.ui.BasePeraWebViewViewModel +import com.algorand.android.utils.Event +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class StakingViewModel @Inject constructor( + private val getAuthorizedAddressesWebMessage: GetAuthorizedAddressesWebMessage, + private val getDeviceIdWebMessage: GetDeviceIdWebMessage, + private val parseOpenSystemBrowserUrl: ParseOpenSystemBrowserUrl +) : BasePeraWebViewViewModel() { + + private val _cardsPreviewFlow = MutableStateFlow(StakingPreview()) + val cardsPreviewFlow: StateFlow + get() = _cardsPreviewFlow.asStateFlow() + + fun getAuthorizedAddresses() { + viewModelScope.launch { + val authAddressesMessage = getAuthorizedAddressesWebMessage() + _cardsPreviewFlow.update { + it.copy(sendMessageEvent = Event(authAddressesMessage)) + } + } + } + + fun getDeviceId() { + viewModelScope.launch { + val deviceIdMessage = getDeviceIdWebMessage() ?: return@launch + _cardsPreviewFlow.update { + it.copy(sendMessageEvent = Event(deviceIdMessage)) + } + } + } + + override fun onError() { + viewModelScope.launch { + _cardsPreviewFlow.update { + it.copy(errorEvent = Event(WebViewError.NO_CONNECTION)) + } + } + } + + override fun onHttpError() { + viewModelScope.launch { + _cardsPreviewFlow.update { + it.copy(errorEvent = Event(WebViewError.HTTP_ERROR)) + } + } + } + + fun getOpenSystemBrowserUrl(jsonPayload: String): String? { + return parseOpenSystemBrowserUrl(jsonPayload) + } +} diff --git a/app/src/main/java/com/algorand/android/modules/staking/di/StakingModule.kt b/app/src/main/java/com/algorand/android/modules/staking/di/StakingModule.kt new file mode 100644 index 000000000..de2b2fa18 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/staking/di/StakingModule.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.card.di + +import com.algorand.android.modules.perawebview.GetAuthorizedAddressesWebMessage +import com.algorand.android.modules.perawebview.GetAuthorizedAddressesWebMessagesUseCase +import com.algorand.android.modules.perawebview.GetDeviceIdWebMessage +import com.algorand.android.modules.perawebview.GetDeviceIdWebMessageUseCase +import com.algorand.android.modules.perawebview.ParseOpenSystemBrowserUrl +import com.algorand.android.modules.perawebview.ParseOpenSystemBrowserUrlUseCase +import com.algorand.android.modules.perawebview.PeraWebMessageBuilder +import com.algorand.android.modules.perawebview.PeraWebMessageBuilderImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object StakingModule { + + @Provides + @Singleton + fun provideGetAuthorizedAddressesWebMessage( + useCase: GetAuthorizedAddressesWebMessagesUseCase + ): GetAuthorizedAddressesWebMessage = useCase + + @Provides + @Singleton + fun provideGetDeviceIdWebMessage(useCase: GetDeviceIdWebMessageUseCase): GetDeviceIdWebMessage = useCase + + @Provides + @Singleton + fun provideParseOpenSystemBrowserUrl(useCase: ParseOpenSystemBrowserUrlUseCase): ParseOpenSystemBrowserUrl = useCase + + @Provides + @Singleton + fun providePeraWebMessageBuilder(impl: PeraWebMessageBuilderImpl): PeraWebMessageBuilder = impl +} diff --git a/app/src/main/java/com/algorand/android/modules/staking/model/StakingPreview.kt b/app/src/main/java/com/algorand/android/modules/staking/model/StakingPreview.kt new file mode 100644 index 000000000..cf10fe4f9 --- /dev/null +++ b/app/src/main/java/com/algorand/android/modules/staking/model/StakingPreview.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.android.modules.staking.model + +import com.algorand.android.discover.common.ui.model.WebViewError +import com.algorand.android.utils.Event + +data class StakingPreview( + val sendMessageEvent: Event? = null, + val errorEvent: Event? = null +) diff --git a/app/src/main/res/layout/custom_core_actions_tab_bar.xml b/app/src/main/res/layout/custom_core_actions_tab_bar.xml index 3a6152efb..06e05b03b 100644 --- a/app/src/main/res/layout/custom_core_actions_tab_bar.xml +++ b/app/src/main/res/layout/custom_core_actions_tab_bar.xml @@ -44,13 +44,22 @@ tools:layout_height="wrap_content"> + + @@ -63,14 +72,14 @@ app:icon="@drawable/ic_buy_sell_helper_button" app:title="@string/buy_sell" /> - + + + + + + + + - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_accounts_quick_actions.xml b/app/src/main/res/layout/item_accounts_quick_actions.xml index 4f831ecfd..25f8ad033 100644 --- a/app/src/main/res/layout/item_accounts_quick_actions.xml +++ b/app/src/main/res/layout/item_accounts_quick_actions.xml @@ -22,7 +22,7 @@ + + + + + Sorry, there was an error fetching some of your accounts. The portfolio value is calculated based only on successfully fetched ones. We couldn\'t find any assets for remove. + Staking + Explore the ecosystem for staking rewards + The Algo is the official cryptocurrency of the Algorand blockchain. The Algorand blockchain has been built to help create an open, borderless economy where everybody can participate. This new type of digital economy can only run on a truly digital currency that works instantly, for everybody. This is the Algo. %1$s successfully removed to your account. Swap