From 047e6727f692bdc0ca7de931bfecf9ea604cc22a Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Fri, 23 Feb 2024 14:53:27 +0700 Subject: [PATCH] feat: replace webview auth in CoinbaseServicesFragment --- .../coinbase/CoinbaseConstants.kt | 14 ++++ .../coinbase/service/CoinBaseAuthApi.kt | 5 +- .../coinbase/ui/CoinbaseServicesFragment.kt | 83 ++++++++++++------- .../wallet/ui/WalletUriHandlerActivity.kt | 3 +- .../buy_sell/IntegrationOverviewFragment.kt | 25 ++++-- .../buy_sell/IntegrationOverviewViewModel.kt | 15 ---- 6 files changed, 89 insertions(+), 56 deletions(-) diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/CoinbaseConstants.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/CoinbaseConstants.kt index 3d08d65f30..ccd0315534 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/CoinbaseConstants.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/CoinbaseConstants.kt @@ -17,6 +17,7 @@ package org.dash.wallet.integrations.coinbase import android.content.Context +import org.dash.wallet.integrations.coinbase.service.CoinBaseClientConstants import java.io.File object CoinbaseConstants { @@ -37,6 +38,19 @@ object CoinbaseConstants { const val BASE_IDS_REQUEST_URL = "v2/assets/prices?filter=holdable&resolution=latest" const val BUY_FEE = 0.006 const val REDIRECT_URL = "dashwallet://brokers/coinbase/connect" + const val AUTH_RESULT_ACTION = "Coinbase.AUTH_RESULT" + val LINK_URL = "https://www.coinbase.com/oauth/authorize?client_id=${CoinBaseClientConstants.CLIENT_ID}" + + "&redirect_uri=${REDIRECT_URL}&response_type" + + "=code&scope=wallet:accounts:read,wallet:user:read,wallet:payment-methods:read," + + "wallet:buys:read,wallet:buys:create,wallet:transactions:transfer," + + "wallet:sells:create,wallet:sells:read,wallet:deposits:create," + + "wallet:transactions:request,wallet:transactions:read,wallet:trades:create," + + "wallet:supported-assets:read,wallet:transactions:send," + + "wallet:addresses:read,wallet:addresses:create" + + "&meta[send_limit_amount]=10" + + "&meta[send_limit_currency]=USD" + + "&meta[send_limit_period]=month" + + "&account=all" fun getCacheDir(context: Context): File { return File(context.cacheDir, "coinbase") diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/service/CoinBaseAuthApi.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/service/CoinBaseAuthApi.kt index 715ef977df..16727967a8 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/service/CoinBaseAuthApi.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/service/CoinBaseAuthApi.kt @@ -28,11 +28,12 @@ interface CoinBaseAuthApi { suspend fun getToken( @Field("client_id") clientId: String = CoinBaseClientConstants.CLIENT_ID, @Field("redirect_uri") redirectUri: String = CoinbaseConstants.REDIRECT_URL, - @Field("grant_type") grant_type: String = "authorization_code", - @Field("client_secret") client_secret: String = CoinBaseClientConstants.CLIENT_SECRET, + @Field("grant_type") grantType: String = "authorization_code", + @Field("client_secret") clientSecret: String = CoinBaseClientConstants.CLIENT_SECRET, @Field("code") code: String ): TokenResponse? + @FormUrlEncoded @POST("oauth/revoke") suspend fun revokeToken(@Field("token") token: String) } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseServicesFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseServicesFragment.kt index 6d76ab36ab..0d9c4b99ad 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseServicesFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseServicesFragment.kt @@ -18,7 +18,10 @@ package org.dash.wallet.integrations.coinbase.ui import android.animation.ObjectAnimator import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.net.Uri import android.os.Bundle import android.view.View @@ -27,8 +30,11 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.withResumed +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import org.bitcoinj.core.Coin import org.dash.wallet.common.databinding.FragmentIntegrationPortalBinding import org.dash.wallet.common.services.analytics.AnalyticsConstants @@ -36,8 +42,10 @@ import org.dash.wallet.common.ui.blinkAnimator import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.observe +import org.dash.wallet.common.util.openCustomTab import org.dash.wallet.common.util.safeNavigate import org.dash.wallet.common.util.toFormattedString +import org.dash.wallet.integrations.coinbase.CoinbaseConstants import org.dash.wallet.integrations.coinbase.R import org.dash.wallet.integrations.coinbase.model.CoinbaseErrorType import org.dash.wallet.integrations.coinbase.viewmodels.CoinbaseViewModel @@ -51,17 +59,20 @@ class CoinbaseServicesFragment : Fragment(R.layout.fragment_integration_portal) private val sharedViewModel by coinbaseViewModels() private var balanceAnimator: ObjectAnimator? = null - private val coinbaseAuthLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - val data = result.data + private val coinbaseAuthResultReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val uri = intent.extras?.get("uri") as Uri? + val code = uri?.getQueryParameter("code") - if (result.resultCode == Activity.RESULT_OK) { - data?.extras?.getString(CoinBaseWebClientActivity.RESULT_TEXT)?.let { code -> - lifecycleScope.launchWhenResumed { - handleCoinbaseAuthResult(code) + if (code != null) { + lifecycleScope.launch { + withResumed { + handleCoinbaseAuthResult(code) + } } } + + startActivity(intent) } } @@ -125,9 +136,7 @@ class CoinbaseServicesFragment : Fragment(R.layout.fragment_integration_portal) it.isCancelable = false }.show(requireActivity()) { login -> if (login == true) { - coinbaseAuthLauncher.launch( - Intent(requireContext(), CoinBaseWebClientActivity::class.java) - ) + requireActivity().openCustomTab(CoinbaseConstants.LINK_URL) } else { findNavController().popBackStack() } @@ -170,6 +179,11 @@ class CoinbaseServicesFragment : Fragment(R.layout.fragment_integration_portal) viewModel.refreshBalance() sharedViewModel.getBaseIdForFiatModel() + + LocalBroadcastManager.getInstance(requireContext()).registerReceiver( + coinbaseAuthResultReceiver, + IntentFilter(CoinbaseConstants.AUTH_RESULT_ACTION) + ) } private fun setNetworkState(hasInternet: Boolean) { @@ -186,30 +200,39 @@ class CoinbaseServicesFragment : Fragment(R.layout.fragment_integration_portal) startActivity(defaultBrowser) } - private suspend fun handleCoinbaseAuthResult(code: String) { - val success = AdaptiveDialog.withProgress(getString(R.string.loading), requireActivity()) { - sharedViewModel.loginToCoinbase(code) - } + private fun handleCoinbaseAuthResult(code: String) { + lifecycleScope.launch { + val success = AdaptiveDialog.withProgress(getString(R.string.loading), requireActivity()) { + sharedViewModel.loginToCoinbase(code) + } - if (success) { - return - } + if (success) { + return@launch + } - val retry = AdaptiveDialog.create( - R.drawable.ic_error, - getString(R.string.login_error_title, getString(R.string.coinbase)), - getString(R.string.login_error_message, getString(R.string.coinbase)), - getString(android.R.string.cancel), - getString(R.string.retry) - ).showAsync(requireActivity()) - - if (retry == true) { - handleCoinbaseAuthResult(code) - } else { - findNavController().popBackStack() + val retry = AdaptiveDialog.create( + R.drawable.ic_error, + getString(R.string.login_error_title, getString(R.string.coinbase)), + getString(R.string.login_error_message, getString(R.string.coinbase)), + getString(android.R.string.cancel), + getString(R.string.retry) + ).showAsync(requireActivity()) + + if (retry == true) { + handleCoinbaseAuthResult(code) + } else { + findNavController().popBackStack() + } } } + override fun onDestroyView() { + super.onDestroyView() + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver( + coinbaseAuthResultReceiver + ) + } + override fun onDestroy() { super.onDestroy() this.balanceAnimator = null diff --git a/wallet/src/de/schildbach/wallet/ui/WalletUriHandlerActivity.kt b/wallet/src/de/schildbach/wallet/ui/WalletUriHandlerActivity.kt index 71cedcd03a..105eaef167 100644 --- a/wallet/src/de/schildbach/wallet/ui/WalletUriHandlerActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/WalletUriHandlerActivity.kt @@ -34,6 +34,7 @@ import de.schildbach.wallet_test.R import org.bitcoinj.wallet.Wallet import org.dash.wallet.common.ui.BaseAlertDialogBuilder import org.dash.wallet.common.ui.formatString +import org.dash.wallet.integrations.coinbase.CoinbaseConstants import org.dash.wallet.integrations.uphold.ui.UpholdPortalFragment /** @@ -82,7 +83,7 @@ class WalletUriHandlerActivity : AppCompatActivity() { activityIntent.setAction(UpholdPortalFragment.AUTH_RESULT_ACTION) LocalBroadcastManager.getInstance(this).sendBroadcast(activityIntent) } else if (intentUri.path?.contains("coinbase") == true) { - activityIntent.setAction(IntegrationOverviewFragment.AUTH_RESULT_ACTION) + activityIntent.setAction(CoinbaseConstants.AUTH_RESULT_ACTION) LocalBroadcastManager.getInstance(this).sendBroadcast(activityIntent) } finish() diff --git a/wallet/src/de/schildbach/wallet/ui/buy_sell/IntegrationOverviewFragment.kt b/wallet/src/de/schildbach/wallet/ui/buy_sell/IntegrationOverviewFragment.kt index 8b49557638..9316481563 100644 --- a/wallet/src/de/schildbach/wallet/ui/buy_sell/IntegrationOverviewFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/buy_sell/IntegrationOverviewFragment.kt @@ -28,6 +28,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.withResumed import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint @@ -38,13 +39,10 @@ import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.openCustomTab import org.dash.wallet.common.util.safeNavigate +import org.dash.wallet.integrations.coinbase.CoinbaseConstants @AndroidEntryPoint class IntegrationOverviewFragment : Fragment(R.layout.fragment_integration_overview) { - companion object { - const val AUTH_RESULT_ACTION = "IntegrationOverviewFragment.AUTH_RESULT" - } - private val binding by viewBinding(FragmentIntegrationOverviewBinding::bind) private val viewModel by viewModels() @@ -54,7 +52,11 @@ class IntegrationOverviewFragment : Fragment(R.layout.fragment_integration_overv val code = uri?.getQueryParameter("code") if (code != null) { - handleCoinbaseAuthResult(code) + lifecycleScope.launch { + withResumed { + handleCoinbaseAuthResult(code) + } + } } startActivity(intent) @@ -78,7 +80,7 @@ class IntegrationOverviewFragment : Fragment(R.layout.fragment_integration_overv LocalBroadcastManager.getInstance(requireContext()).registerReceiver( coinbaseAuthResultReceiver, - IntentFilter(AUTH_RESULT_ACTION) + IntentFilter(CoinbaseConstants.AUTH_RESULT_ACTION) ) } @@ -100,8 +102,7 @@ class IntegrationOverviewFragment : Fragment(R.layout.fragment_integration_overv } private fun linkCoinbaseAccount() { - val url = viewModel.getCoinbaseLinkAccountUrl() - requireActivity().openCustomTab(url) + requireActivity().openCustomTab(CoinbaseConstants.LINK_URL) } private fun handleCoinbaseAuthResult(code: String) { @@ -127,4 +128,12 @@ class IntegrationOverviewFragment : Fragment(R.layout.fragment_integration_overv } } } + + override fun onDestroyView() { + super.onDestroyView() + + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver( + coinbaseAuthResultReceiver + ) + } } diff --git a/wallet/src/de/schildbach/wallet/ui/buy_sell/IntegrationOverviewViewModel.kt b/wallet/src/de/schildbach/wallet/ui/buy_sell/IntegrationOverviewViewModel.kt index c4b22fa9fc..034b695e75 100644 --- a/wallet/src/de/schildbach/wallet/ui/buy_sell/IntegrationOverviewViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/buy_sell/IntegrationOverviewViewModel.kt @@ -60,19 +60,4 @@ class IntegrationOverviewViewModel @Inject constructor( fun logEvent(eventName: String) { analyticsService.logEvent(eventName, mapOf()) } - - fun getCoinbaseLinkAccountUrl(): String { - return "https://www.coinbase.com/oauth/authorize?client_id=${CoinBaseClientConstants.CLIENT_ID}" + - "&redirect_uri=${CoinbaseConstants.REDIRECT_URL}&response_type" + - "=code&scope=wallet:accounts:read,wallet:user:read,wallet:payment-methods:read," + - "wallet:buys:read,wallet:buys:create,wallet:transactions:transfer," + - "wallet:sells:create,wallet:sells:read,wallet:deposits:create," + - "wallet:transactions:request,wallet:transactions:read,wallet:trades:create," + - "wallet:supported-assets:read,wallet:transactions:send," + - "wallet:addresses:read,wallet:addresses:create" + - "&meta[send_limit_amount]=10" + - "&meta[send_limit_currency]=USD" + - "&meta[send_limit_period]=month" + - "&account=all" - } }