diff --git a/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/DeepLinkingActivity.kt b/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/DeepLinkingActivity.kt index 41293cbe5..9ef83a5dd 100644 --- a/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/DeepLinkingActivity.kt +++ b/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/DeepLinkingActivity.kt @@ -24,9 +24,8 @@ class DeepLinkingActivity : BaseActivity() { } logd(TAG, "uri:$uri") - MainActivity.launch(this) - dispatchDeepLinking(uri) + dispatchDeepLinking(this, uri) finish() } diff --git a/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/PendingActionHelper.kt b/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/PendingActionHelper.kt new file mode 100644 index 000000000..74730aeb9 --- /dev/null +++ b/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/PendingActionHelper.kt @@ -0,0 +1,41 @@ +package com.flowfoundation.wallet.page.component.deeplinking + +import android.content.Context +import android.content.SharedPreferences +import android.net.Uri + + +object PendingActionHelper { + private const val PREFS_NAME = "pending_action_prefs" + private const val KEY_PENDING_DEEPLINK = "pending_deeplink" + private const val KEY_HAS_PENDING_DEEPLINK = "has_pending_deeplink" + + private fun getPrefs(context: Context): SharedPreferences { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + } + + fun savePendingDeepLink(context: Context, deepLink: Uri) { + getPrefs(context).edit().apply { + putString(KEY_PENDING_DEEPLINK, deepLink.toString()) + putBoolean(KEY_HAS_PENDING_DEEPLINK, true) + commit() + } + } + + fun hasPendingDeepLink(context: Context): Boolean { + return getPrefs(context).getBoolean(KEY_HAS_PENDING_DEEPLINK, false) + } + + fun getPendingDeepLink(context: Context): Uri? { + val deepLinkStr = getPrefs(context).getString(KEY_PENDING_DEEPLINK, null) ?: return null + return Uri.parse(deepLinkStr) + } + + fun clearPendingDeepLink(context: Context) { + getPrefs(context).edit().apply { + remove(KEY_PENDING_DEEPLINK) + putBoolean(KEY_HAS_PENDING_DEEPLINK, false) + commit() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/Utils.kt b/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/Utils.kt index 3f5b88f02..a5c5b8b4d 100644 --- a/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/Utils.kt +++ b/app/src/main/java/com/flowfoundation/wallet/page/component/deeplinking/Utils.kt @@ -1,18 +1,81 @@ package com.flowfoundation.wallet.page.component.deeplinking +import android.content.Context import android.net.Uri +import com.flowfoundation.wallet.base.activity.BaseActivity +import com.flowfoundation.wallet.manager.app.chainNetWorkString +import com.flowfoundation.wallet.manager.app.networkId +import com.flowfoundation.wallet.manager.coin.FlowCoinListManager import com.flowfoundation.wallet.manager.walletconnect.WalletConnect +import com.flowfoundation.wallet.network.model.AddressBookContact +import com.flowfoundation.wallet.page.browser.openBrowser +import com.flowfoundation.wallet.page.send.transaction.subpage.amount.SendAmountActivity +import com.flowfoundation.wallet.page.wallet.dialog.SwapDialog import com.flowfoundation.wallet.utils.ioScope import com.flowfoundation.wallet.utils.logd +import com.flowfoundation.wallet.utils.uiScope +import com.flowfoundation.wallet.wallet.toAddress +import com.flowfoundation.wallet.widgets.DialogType +import com.flowfoundation.wallet.widgets.SwitchNetworkDialog import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import org.web3j.utils.Convert +import org.web3j.utils.Numeric +import java.math.BigDecimal import java.net.URLDecoder import java.nio.charset.StandardCharsets private const val TAG = "DeepLinkingDispatch" -fun dispatchDeepLinking(uri: Uri) { - ioScope { dispatchWalletConnect(uri) } +enum class DeepLinkPath(val path: String) { + DAPP("/dapp"), + SEND("/send"), + BUY("/buy"); + + companion object { + fun fromPath(path: String?): DeepLinkPath? { + return entries.firstOrNull { it.path == path } + } + } +} + + +fun dispatchDeepLinking(context: Context, uri: Uri) { + ioScope { + val wcUri = getWalletConnectUri(uri) + if (wcUri?.startsWith("wc:") == true) { + dispatchWalletConnect(uri) + return@ioScope + } + PendingActionHelper.savePendingDeepLink(context, uri) + } +} + +fun executePendingDeepLink(uri: Uri) { + if (uri.host == "link.wallet.flow.com") { + when (DeepLinkPath.fromPath(uri.path)) { + DeepLinkPath.DAPP -> { + val dappUrl = uri.getQueryParameter("url") + if (dappUrl != null) { + dispatchDapp(dappUrl) + } + } + DeepLinkPath.SEND -> { + val recipient = uri.getQueryParameter("recipient") + val network = uri.getQueryParameter("network") + val value = uri.getQueryParameter("value") + if (recipient != null) { + dispatchSend(uri, recipient, network, parseValue(value)) + } + } + DeepLinkPath.BUY -> { + dispatchBuy() + } + else -> { + logd(TAG, "executeDeepLinking: unknown path=${uri.path}") + } + } + } } // https://lilico.app/?uri=wc%3A83ba9cb3adf9da4b573ae0c499d49be91995aa3e38b5d9a41649adfaf986040c%402%3Frelay-protocol%3Diridium%26symKey%3D618e22482db56c3dda38b52f7bfca9515cc307f413694c1d6d91931bbe00ae90 @@ -55,3 +118,59 @@ fun getWalletConnectUri(uri: Uri): String? { } }.getOrNull() } + +private fun parseValue(value: String?): BigDecimal? { + if (value == null) return null + + return try { + if (value.startsWith("0x", ignoreCase = true)) { + val amountValue = Numeric.decodeQuantity(value) + Convert.fromWei(amountValue.toString(), Convert.Unit.ETHER) + } else { + BigDecimal(value) + } + } catch (e: Exception) { + logd(TAG, "Failed to parse value: $value, ${e.message}") + null + } +} + +private fun dispatchDapp(dappUrl: String) { + BaseActivity.getCurrentActivity()?.let { + uiScope { + openBrowser(it, dappUrl) + return@uiScope + } + } +} + +private fun dispatchSend(uri: Uri, recipient: String, network: String?, value: BigDecimal?) { + logd(TAG, "dispatchSend: recipient=$recipient, network=$network, value=$value") + BaseActivity.getCurrentActivity()?.let { + if (network != null && network != chainNetWorkString()) { + PendingActionHelper.savePendingDeepLink(it, uri) + uiScope { + SwitchNetworkDialog( + context = it, + dialogType = DialogType.DEEPLINK, + targetNetwork = networkId(network) + ).show() + } + } else { + SendAmountActivity.launch( + it, + AddressBookContact(address = recipient.toAddress()), + FlowCoinListManager.getFlowCoinContractId(), + value?.toString() + ) + } + } +} + +private fun dispatchBuy() { + BaseActivity.getCurrentActivity()?.let { + uiScope { + SwapDialog.show(it.supportFragmentManager) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flowfoundation/wallet/page/main/MainActivity.kt b/app/src/main/java/com/flowfoundation/wallet/page/main/MainActivity.kt index 58cfce76d..179e44e5c 100644 --- a/app/src/main/java/com/flowfoundation/wallet/page/main/MainActivity.kt +++ b/app/src/main/java/com/flowfoundation/wallet/page/main/MainActivity.kt @@ -14,6 +14,8 @@ import com.flowfoundation.wallet.base.activity.BaseActivity import com.zackratos.ultimatebarx.ultimatebarx.UltimateBarX import com.flowfoundation.wallet.databinding.ActivityMainBinding import com.flowfoundation.wallet.firebase.firebaseInformationCheck +import com.flowfoundation.wallet.page.component.deeplinking.PendingActionHelper +import com.flowfoundation.wallet.page.component.deeplinking.executePendingDeepLink import com.flowfoundation.wallet.page.dialog.common.RootDetectedDialog import com.flowfoundation.wallet.page.main.model.MainContentModel import com.flowfoundation.wallet.page.main.model.MainDrawerLayoutModel @@ -30,7 +32,6 @@ import com.flowfoundation.wallet.utils.isRegistered import com.flowfoundation.wallet.utils.uiScope import com.instabug.bug.BugReporting import com.instabug.library.Instabug -import com.instabug.library.OnSdkDismissCallback class MainActivity : BaseActivity() { @@ -111,6 +112,17 @@ class MainActivity : BaseActivity() { override fun onResume() { RootDetectedDialog.show(supportFragmentManager) super.onResume() + checkPendingAction() + } + + private fun checkPendingAction() { + if (PendingActionHelper.hasPendingDeepLink(this)) { + val pendingDeeplink = PendingActionHelper.getPendingDeepLink(this) + PendingActionHelper.clearPendingDeepLink(this) + if (pendingDeeplink != null) { + executePendingDeepLink(pendingDeeplink) + } + } } override fun onDestroy() { diff --git a/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/SendAmountActivity.kt b/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/SendAmountActivity.kt index 66b41ae31..b137e875a 100644 --- a/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/SendAmountActivity.kt +++ b/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/SendAmountActivity.kt @@ -23,6 +23,7 @@ class SendAmountActivity : BaseActivity(), OnTransactionStateChange { private val contact by lazy { intent.getParcelableExtra(EXTRA_CONTACT)!! } private val coinContractId by lazy { intent.getStringExtra(EXTRA_COIN_CONTRACT_ID) } + private val initialAmount by lazy { intent.getStringExtra(EXTRA_AMOUNT) } private lateinit var binding: ActivitySendAmountBinding private lateinit var presenter: SendAmountPresenter @@ -38,6 +39,7 @@ class SendAmountActivity : BaseActivity(), OnTransactionStateChange { presenter = SendAmountPresenter(this, binding, contact) viewModel = ViewModelProvider(this)[SendAmountViewModel::class.java].apply { setContact(contact) + setInitialAmount(initialAmount) FlowCoinListManager.getCoinById(coinContractId.orEmpty())?.let { changeCoin(it) } balanceLiveData.observe(this@SendAmountActivity) { presenter.bind(SendAmountModel(balance = it)) } onCoinSwap.observe(this@SendAmountActivity) { presenter.bind(SendAmountModel(onCoinSwap = true)) } @@ -61,11 +63,13 @@ class SendAmountActivity : BaseActivity(), OnTransactionStateChange { companion object { private const val EXTRA_CONTACT = "extra_contact" private const val EXTRA_COIN_CONTRACT_ID = "coin_contract_id" + private const val EXTRA_AMOUNT = "extra_amount" - fun launch(context: Context, contact: AddressBookContact, coinContractId: String?) { + fun launch(context: Context, contact: AddressBookContact, coinContractId: String?, amount: String? = null) { context.startActivity(Intent(context, SendAmountActivity::class.java).apply { putExtra(EXTRA_CONTACT, contact) putExtra(EXTRA_COIN_CONTRACT_ID, coinContractId) + putExtra(EXTRA_AMOUNT, amount) }) } } diff --git a/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/SendAmountViewModel.kt b/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/SendAmountViewModel.kt index 9b03b4be2..f00bc4172 100644 --- a/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/SendAmountViewModel.kt +++ b/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/SendAmountViewModel.kt @@ -19,6 +19,7 @@ class SendAmountViewModel : ViewModel(), OnBalanceUpdate, OnCoinRateUpdate { private lateinit var contact: AddressBookContact val balanceLiveData = MutableLiveData() + private var initialAmount: String? = null val onCoinSwap = MutableLiveData() @@ -39,6 +40,12 @@ class SendAmountViewModel : ViewModel(), OnBalanceUpdate, OnCoinRateUpdate { fun contact() = contact + fun setInitialAmount(amount: String?) { + this.initialAmount = amount + } + + fun getInitialAmount() = initialAmount + fun load() { viewModelIOScope(this) { val coin = FlowCoinListManager.getCoinById(currentCoin) ?: return@viewModelIOScope diff --git a/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/presenter/SendAmountPresenter.kt b/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/presenter/SendAmountPresenter.kt index 929471946..a74ba7877 100644 --- a/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/presenter/SendAmountPresenter.kt +++ b/app/src/main/java/com/flowfoundation/wallet/page/send/transaction/subpage/amount/presenter/SendAmountPresenter.kt @@ -45,6 +45,7 @@ class SendAmountPresenter( private val viewModel by lazy { ViewModelProvider(activity)[SendAmountViewModel::class.java] } private var minFlowBalance = BigDecimal.ZERO + private var initialAmountSet = false private fun balance() = viewModel.balanceLiveData.value @@ -78,10 +79,27 @@ class SendAmountPresenter( } override fun bind(model: SendAmountModel) { - model.balance?.let { updateBalance(it) } + model.balance?.let { + updateBalance(it) + if (!initialAmountSet) { + setInitialAmount() + initialAmountSet = true + } + } model.onCoinSwap?.let { updateCoinState() } } + private fun setInitialAmount() { + viewModel.getInitialAmount()?.let { amount -> + if (amount.isNotBlank()) { + with(binding) { + transferAmountInput.setText(amount) + transferAmountInput.setSelection(transferAmountInput.text.length) + } + } + } + } + private fun getMinFlowBalance() { ioScope { minFlowBalance = try { diff --git a/app/src/main/java/com/flowfoundation/wallet/widgets/SwitchNetworkDialog.kt b/app/src/main/java/com/flowfoundation/wallet/widgets/SwitchNetworkDialog.kt index 3f6bed954..ab5e88c68 100644 --- a/app/src/main/java/com/flowfoundation/wallet/widgets/SwitchNetworkDialog.kt +++ b/app/src/main/java/com/flowfoundation/wallet/widgets/SwitchNetworkDialog.kt @@ -9,18 +9,22 @@ import android.view.View import android.widget.FrameLayout import android.widget.TextView import com.flowfoundation.wallet.R +import com.flowfoundation.wallet.manager.app.chainNetWorkString import com.flowfoundation.wallet.manager.app.doNetworkChangeTask import com.flowfoundation.wallet.manager.app.refreshChainNetworkSync import com.flowfoundation.wallet.manager.flow.FlowCadenceApi import com.flowfoundation.wallet.manager.flowjvm.FlowApi import com.flowfoundation.wallet.manager.wallet.WalletManager import com.flowfoundation.wallet.network.clearUserCache +import com.flowfoundation.wallet.page.component.deeplinking.PendingActionHelper import com.flowfoundation.wallet.page.main.MainActivity import com.flowfoundation.wallet.utils.NETWORK_MAINNET +import com.flowfoundation.wallet.utils.extensions.capitalizeV2 import com.flowfoundation.wallet.utils.extensions.res2String import com.flowfoundation.wallet.utils.ioScope import com.flowfoundation.wallet.utils.uiScope import com.flowfoundation.wallet.utils.updateChainNetworkPreference +import com.google.android.material.button.MaterialButton import kotlinx.coroutines.delay enum class DialogType( @@ -29,19 +33,33 @@ enum class DialogType( BACKUP(R.string.backup_on_mainnet), RESTORE(R.string.restore_on_mainnet), SWITCH(R.string.switch_on_mainnet), + DEEPLINK(R.string.network_error_dialog_desc), CREATE(R.string.create_on_mainnet_or_testnet) } class SwitchNetworkDialog( private val context: Context, - private val dialogType: DialogType + private val dialogType: DialogType, + private val targetNetwork: Int? = NETWORK_MAINNET ) { fun show() { var dialog: Dialog? = null + var switchButtonClicked = false with(AlertDialog.Builder(context, R.style.Theme_AlertDialogTheme)) { - setView(SwitchNetworkDialogView(context, dialogType) { dialog?.cancel() }) + setView(SwitchNetworkDialogView(context, dialogType, targetNetwork ?: NETWORK_MAINNET, + onSwitchClick = { + switchButtonClicked = true + }, + onCancel = { dialog?.cancel() } + )) with(create()) { dialog = this + setOnDismissListener { + if (switchButtonClicked) { + return@setOnDismissListener + } + PendingActionHelper.clearPendingDeepLink(context) + } show() } } @@ -52,20 +70,33 @@ class SwitchNetworkDialog( private class SwitchNetworkDialogView( context: Context, dialogType: DialogType, + targetNetwork: Int, + private val onSwitchClick: () -> Unit, private val onCancel: () -> Unit, ) : FrameLayout(context) { - private val switchButton by lazy { findViewById(R.id.switch_button) } + private val switchButton by lazy { findViewById(R.id.switch_button) } private val cancelButton by lazy { findViewById(R.id.cancel_button) } private val descView by lazy { findViewById(R.id.desc_view) } init { LayoutInflater.from(context).inflate(R.layout.dialog_switch_network, this) - descView.text = dialogType.descResId.res2String() + descView.text = if (dialogType == DialogType.DEEPLINK) { + context.getString(dialogType.descResId, chainNetWorkString(), chainNetWorkString(targetNetwork)) + } else { + dialogType.descResId.res2String() + } + + switchButton.text = if (dialogType == DialogType.DEEPLINK) { + context.getString(R.string.switch_to, chainNetWorkString(targetNetwork).capitalizeV2()) + } else { + context.getString(R.string.switch_to_mainnet) + } switchButton.setOnClickListener { - updateChainNetworkPreference(NETWORK_MAINNET) { + onSwitchClick() + updateChainNetworkPreference(targetNetwork) { ioScope { delay(200) refreshChainNetworkSync() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6db2b619..3e344a9f4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -743,4 +743,5 @@ Ignore warning, show anyway Ignore warning, connect anyway This app has been flagged as dangerous. + Switch to %1$s diff --git a/gradle.properties b/gradle.properties index e3032d2d1..9d512c27f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,5 +23,5 @@ android.injected.testOnly=false android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false -vCode=266 -vName=r2.8.2 +vCode=270 +vName=r2.8.3