diff --git a/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt b/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt index 964ad5cbdd..f2ebf0b425 100644 --- a/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt +++ b/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt @@ -53,5 +53,5 @@ interface SendPaymentService { val totalAmount: String ) - suspend fun payWithDashUrl(dashUri: String): Transaction + suspend fun payWithDashUrl(dashUri: String, serviceName: String?): Transaction } diff --git a/common/src/main/java/org/dash/wallet/common/util/Constants.kt b/common/src/main/java/org/dash/wallet/common/util/Constants.kt index 6d00115855..d1066700a4 100644 --- a/common/src/main/java/org/dash/wallet/common/util/Constants.kt +++ b/common/src/main/java/org/dash/wallet/common/util/Constants.kt @@ -61,9 +61,9 @@ object Constants { val HTTP_CLIENT: OkHttpClient = OkHttpClient.Builder() .followRedirects(false) .followSslRedirects(true) - .connectTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) + .connectTimeout(20, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) .addInterceptor( HttpLoggingInterceptor { log.debug(it) }.setLevel(HttpLoggingInterceptor.Level.BASIC) ) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/ctxspend/model/GiftCardResponse.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/ctxspend/model/GiftCardResponse.kt index c68124b3c3..e57d0777ae 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/ctxspend/model/GiftCardResponse.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/ctxspend/model/GiftCardResponse.kt @@ -24,7 +24,9 @@ data class GiftCardResponse( @SerializedName("barcodeUrl") val barcodeUrl: String? = "", @SerializedName("number") val cardNumber: String? = "", @SerializedName("pin") val cardPin: String? = "", - + val cardFiatAmount: String? = "", + val cardFiatCurrency: String? = "", + val merchantName: String? = "", @SerializedName("paymentCryptoAmount") val cryptoAmount: String? = "", @SerializedName("paymentCryptoCurrency") val cryptoCurrency: String? = "", val paymentCryptoNetwork: String = "", diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/RemoteDataSource.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/RemoteDataSource.kt index f31a2df776..d7bbe26498 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/RemoteDataSource.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/RemoteDataSource.kt @@ -64,6 +64,7 @@ class RemoteDataSource @Inject constructor(private val config: CTXSpendConfig) { authenticator?.let { client.authenticator(it) } // if (BuildConfig.DEBUG) { TODO val logging = HttpLoggingInterceptor { message -> log.info(message) } + logging.redactHeader("Authorization") logging.level = HttpLoggingInterceptor.Level.BODY client.addInterceptor(logging) // } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/service/stubs/FakeDashSpendService.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/service/stubs/FakeDashSpendService.kt index e4fc9c7d23..6421f8ad72 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/service/stubs/FakeDashSpendService.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/service/stubs/FakeDashSpendService.kt @@ -65,7 +65,7 @@ class FakeDashSpendService @Inject constructor( return realService.estimateNetworkFee(address, amount, emptyWallet) } - override suspend fun payWithDashUrl(dashUri: String): Transaction { + override suspend fun payWithDashUrl(dashUri: String, serviceName: String?): Transaction { return if (dashUri.startsWith(DASH_SPEND_SCHEMA)) { val uri = Uri.parse(dashUri) val amount = Coin.valueOf(uri.getQueryParameter("amount")?.toLong() ?: 0) @@ -74,7 +74,7 @@ class FakeDashSpendService @Inject constructor( amount ) } else { - realService.payWithDashUrl(dashUri) + realService.payWithDashUrl(dashUri, serviceName) } } } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt index 36a60b84f9..4fc66367b9 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt @@ -18,7 +18,6 @@ package org.dash.wallet.features.exploredash.repository import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import kotlinx.coroutines.flow.Flow import org.dash.wallet.common.data.ResponseResource import org.dash.wallet.common.data.safeApiCall @@ -40,20 +39,22 @@ class CTXSpendException( message: String, val errorCode: Int? = null, val errorBody: String? = null, - cause: Exception? = null + cause: Throwable? = null ) : Exception(message, cause) { var resourceString: ResourceString? = null + var giftCardResponse: GiftCardResponse? = null private val errorMap: Map - constructor(message: ResourceString) : this("") { + constructor(message: ResourceString, giftCardResponse: GiftCardResponse? = null) : this("") { this.resourceString = message + this.giftCardResponse = giftCardResponse } init { - val type = object : TypeToken>() {}.type errorMap = try { if (errorBody != null) { - Gson().fromJson(errorBody, type) ?: emptyMap() + @Suppress("UNCHECKED_CAST") + Gson().fromJson(errorBody, Map::class.java) as? Map ?: emptyMap() } else { emptyMap() } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt index 61bc60c38f..2c291d4d6e 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt @@ -41,12 +41,12 @@ import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ResponseResource +import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.entity.ExchangeRate import org.dash.wallet.common.data.entity.GiftCard import org.dash.wallet.common.services.* import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.util.Constants -import org.dash.wallet.common.util.toBigDecimal import org.dash.wallet.features.exploredash.data.ctxspend.model.DenominationType import org.dash.wallet.features.exploredash.data.ctxspend.model.GetMerchantResponse import org.dash.wallet.features.exploredash.data.ctxspend.model.GiftCardResponse @@ -148,11 +148,11 @@ class CTXSpendViewModel @Inject constructor( suspend fun purchaseGiftCard(): GiftCardResponse { giftCardMerchant?.merchantId?.let { val amountValue = giftCardPaymentValue.value - + val fiatAmount = MonetaryFormat.FIAT.noCode().format(amountValue).toString() val response = try { repository.purchaseGiftCard( merchantId = it, - fiatAmount = MonetaryFormat.FIAT.noCode().format(amountValue).toString(), + fiatAmount = fiatAmount, fiatCurrency = "USD", cryptoCurrency = Constants.DASH_CURRENCY ) @@ -173,19 +173,23 @@ class CTXSpendViewModel @Inject constructor( is ResponseResource.Failure -> { log.error("purchaseGiftCard error ${response.errorCode}: ${response.errorBody}") throw CTXSpendException( - "purchaseGiftCard error ${response.errorCode}: ${response.errorBody}", + "/gift-cards POST { fiatAmount=$fiatAmount, merchantId = $it}", response.errorCode, - response.errorBody + response.errorBody, + response.throwable ) } // else -> {} } } - throw CTXSpendException("purchaseGiftCard error") + throw CTXSpendException("purchaseGiftCard error: no merchant") } suspend fun createSendingRequestFromDashUri(paymentUri: String): Sha256Hash { - val transaction = sendPaymentService.payWithDashUrl(paymentUri) + val transaction = sendPaymentService.payWithDashUrl( + paymentUri, + giftCardMerchant?.source?.lowercase() ?: ServiceName.CTXSpend + ) log.info("ctx spend transaction: ${transaction.txId}") transactionMetadata.markGiftCardTransaction(transaction.txId, giftCardMerchant?.logoLocation) @@ -273,13 +277,13 @@ class CTXSpendViewModel @Inject constructor( suspend fun logout() = repository.logout() - fun saveGiftCardDummy(txId: Sha256Hash, giftCardId: String) { + fun saveGiftCardDummy(txId: Sha256Hash, giftCardResponse: GiftCardResponse) { val giftCard = GiftCard( txId = txId, merchantName = giftCardMerchant?.name ?: "", - price = giftCardPaymentValue.value.toBigDecimal().toDouble(), + price = giftCardResponse.cardFiatAmount?.toDouble() ?: 0.0, merchantUrl = giftCardMerchant?.website, - note = giftCardId + note = giftCardResponse.id ) viewModelScope.launch { giftCardDao.insertGiftCard(giftCard) @@ -329,10 +333,11 @@ class CTXSpendViewModel @Inject constructor( fun createEmailIntent( subject: String, - ex: CTXSpendException + sentToCTX: Boolean, + ex: CTXSpendException? ) = Intent(Intent.ACTION_SEND).apply { setType("message/rfc822") - putExtra(Intent.EXTRA_EMAIL, arrayOf(CTXSpendConstants.REPORT_EMAIL)) + putExtra(Intent.EXTRA_EMAIL, arrayOf(if (sentToCTX) CTXSpendConstants.REPORT_EMAIL else "support@dash.org")) putExtra(Intent.EXTRA_SUBJECT, subject) putExtra(Intent.EXTRA_TEXT, createReportEmail(ex)) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) @@ -342,7 +347,7 @@ class CTXSpendViewModel @Inject constructor( return savedStateHandle.get(MERCHANT_ID_KEY) } - private fun createReportEmail(ex: CTXSpendException): String { + private fun createReportEmail(ex: CTXSpendException?): String { val report = StringBuilder() report.append("CTX Issue Report").append("\n") giftCardMerchant?.let { merchant -> @@ -358,15 +363,42 @@ class CTXSpendViewModel @Inject constructor( } ?: run { report.append("No merchant selected").append("\n") } + report.append("\n") report.append("Purchase Details").append("\n") - report.append("amount: ").append(giftCardPaymentValue.value.toFriendlyString()).append("\n") + report.append("amount entered: ").append(giftCardPaymentValue.value.toFriendlyString()).append("\n") report.append("\n") - ex.errorCode?.let { - report.append("code: ").append(it).append("\n") - } - ex.errorBody?.let { - report.append("body:\n").append(it).append("\n") + ex?.let { exception -> + exception.message?.let { + report.append(it).append("\n") + } + exception.errorCode?.let { + report.append("code: ").append(it).append("\n") + } + exception.errorBody?.let { + report.append("body:\n").append(it).append("\n") + } + exception.giftCardResponse?.let { giftCard -> + report.append("Gift Card Information: ").append("\n") + .append("id: ").append(giftCard.id).append("\n") + .append("status: ").append(giftCard.status).append("\n") + .append("barcodeUrl: ").append(giftCard.barcodeUrl ?: "N/A").append("\n") + .append("cardNumber: ").append(giftCard.cardNumber ?: "N/A").append("\n") + .append("cardPin: ").append(giftCard.cardPin ?: "N/A").append("\n") + .append("cryptoAmount: ").append(giftCard.cryptoAmount ?: "N/A").append("\n") + .append("cryptoCurrency: ").append(giftCard.cryptoCurrency ?: "N/A").append("\n") + .append("paymentCryptoNetwork: ").append(giftCard.paymentCryptoNetwork).append("\n") + .append("paymentId: ").append(giftCard.paymentId).append("\n") + .append("percentDiscount: ").append(giftCard.percentDiscount).append("\n") + .append("rate: ").append(giftCard.rate).append("\n") + .append("redeemUrl: ").append(giftCard.redeemUrl).append("\n") + .append("fiatAmount: ").append(giftCard.fiatAmount ?: "N/A").append("\n") + .append("fiatCurrency: ").append(giftCard.fiatCurrency ?: "N/A").append("\n") + .append("paymentUrls: ").append(giftCard.paymentUrls?.toString() ?: "N/A").append("\n") + } + exception.cause?.let { + report.append("Stack trace\n").append(exception.stackTraceToString()) + } } return report.toString() } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt index 11e5040af3..160ed08778 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt @@ -353,20 +353,20 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi } private fun exceedsBalance(): Boolean { - val rate = viewModel.usdExchangeRate.value + val rate = viewModel.usdExchangeRate.value ?: return false - if (rate == null) { - return false - } - - val balanceWithDiscount = viewModel.balanceWithDiscount - - if (balanceWithDiscount == null) { - return false - } + val balanceWithDiscount = viewModel.balanceWithDiscount ?: return false + var paymentValue = viewModel.giftCardPaymentValue.value val myRate = ExchangeRate(rate.fiat) - val amountDash = myRate.fiatToCoin(viewModel.giftCardPaymentValue.value) + // this is called when the after a purchase with the user's selected currency, not USD + if (paymentValue.currencyCode != Constants.USD_CURRENCY) { + paymentValue = Fiat.valueOf( + Constants.USD_CURRENCY, + paymentValue.value + ) + } + val amountDash = myRate.fiatToCoin(paymentValue) return amountDash.isGreaterThan(balanceWithDiscount) } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt index 1db1d8cd63..83fe4ab8f0 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.util.Size import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StyleRes import androidx.core.net.toUri import androidx.core.os.bundleOf @@ -55,6 +56,7 @@ import org.dash.wallet.features.exploredash.R import org.dash.wallet.features.exploredash.data.ctxspend.model.Barcode import org.dash.wallet.features.exploredash.databinding.DialogGiftCardDetailsBinding import org.dash.wallet.features.exploredash.repository.CTXSpendException +import org.dash.wallet.features.exploredash.ui.ctxspend.CTXSpendViewModel import org.slf4j.LoggerFactory import java.text.DecimalFormat import java.text.NumberFormat @@ -79,6 +81,7 @@ class GiftCardDetailsDialog : OffsetDialogFragment(R.layout.dialog_gift_card_det override val forceExpand = true private val binding by viewBinding(DialogGiftCardDetailsBinding::bind) private val viewModel by viewModels() + private val ctxSpendViewModel by viewModels() private var originalBrightness: Float = -1f private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() { @@ -92,6 +95,10 @@ class GiftCardDetailsDialog : OffsetDialogFragment(R.layout.dialog_gift_card_det } } + private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + // Optionally handle result here + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -162,8 +169,10 @@ class GiftCardDetailsDialog : OffsetDialogFragment(R.layout.dialog_gift_card_det binding.cardError.isVisible = true binding.cardError.text = message ?: getString(R.string.gift_card_details_error) + binding.contactSupport.isVisible = true } else { binding.cardError.isVisible = false + binding.contactSupport.isVisible = false } } @@ -174,6 +183,19 @@ class GiftCardDetailsDialog : OffsetDialogFragment(R.layout.dialog_gift_card_det binding.viewTransactionDetailsCard.setOnClickListener { deepLinkNavigate(DeepLinkDestination.Transaction(viewModel.transactionId.toString())) } + binding.contactSupport.setOnClickListener { + val intent = ctxSpendViewModel.createEmailIntent( + "CTX Issue with tx: ${viewModel.transactionId.toStringBase58()}", + sentToCTX = true, + viewModel.uiState.value.error as? CTXSpendException + ) + + val chooser = Intent.createChooser( + intent, + getString(R.string.report_issue_dialog_mail_intent_chooser) + ) + launcher.launch(chooser) + } subscribeToBottomSheetCallback() } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt index ebefff048f..23f16d1c47 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt @@ -168,7 +168,8 @@ class GiftCardDetailsViewModel @Inject constructor( ResourceString( R.string.gift_card_redeem_url_not_supported, listOf(giftCard.id, giftCard.paymentId, txid) - ) + ), + giftCard ) ) } @@ -183,7 +184,8 @@ class GiftCardDetailsViewModel @Inject constructor( ResourceString( R.string.gift_card_rejected, listOf(giftCard.id, giftCard.paymentId, txid) - ) + ), + giftCard ) ) } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt index 03f12629f4..2723b27a8e 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt @@ -129,7 +129,21 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi getString(R.string.gift_card_purchase_failed), getString(R.string.gift_card_error), getString(R.string.button_close) - ).show(requireActivity()) + ).show(requireActivity()) { result -> + if (result == true) { + val intent = viewModel.createEmailIntent( + "DashPay DashSpend Issue: Network Error", + sentToCTX = true, + ex + ) + + val chooser = Intent.createChooser( + intent, + getString(R.string.report_issue_dialog_mail_intent_chooser) + ) + launcher.launch(chooser) + } + } } ex.errorCode == 400 && ex.isLimitError -> { AdaptiveDialog.create( @@ -140,9 +154,32 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi getString(R.string.gift_card_contact_ctx) ).show(requireActivity()) { result -> if (result == true) { - // TODO: share val intent = viewModel.createEmailIntent( "CTX Issue: Spending Limit Problem", + sentToCTX = true, + ex + ) + + val chooser = Intent.createChooser( + intent, + getString(R.string.report_issue_dialog_mail_intent_chooser) + ) + launcher.launch(chooser) + } + } + } + ex.errorCode == 500 -> { + AdaptiveDialog.create( + R.drawable.ic_error, + getString(R.string.gift_card_purchase_failed), + getString(R.string.gift_card_server_error), + getString(R.string.button_close), + getString(R.string.gift_card_contact_ctx) + ).show(requireActivity()) { result -> + if (result == true) { + val intent = viewModel.createEmailIntent( + "CTX Issue: Purchase, Internal Server Error", + sentToCTX = true, ex ) @@ -160,7 +197,21 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi getString(R.string.gift_card_purchase_failed), ex.message ?: getString(R.string.gift_card_error), getString(R.string.button_close) - ).show(requireActivity()) + ).show(requireActivity()) { result -> + if (result == true) { + val intent = viewModel.createEmailIntent( + subject = "DashPay DashSpend Issue: Purchase Error", + sentToCTX = false, + ex + ) + + val chooser = Intent.createChooser( + intent, + getString(R.string.report_issue_dialog_mail_intent_chooser) + ) + launcher.launch(chooser) + } + } } } return@launch @@ -178,7 +229,7 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi val transactionId = createSendingRequestFromDashUri(data.paymentUrls?.get("DASH.DASH")!!) transactionId?.let { enterAmountViewModel.clearSavedState() - viewModel.saveGiftCardDummy(transactionId, data.id) + viewModel.saveGiftCardDummy(transactionId, data) showGiftCardDetailsDialog(transactionId, data.id) } } diff --git a/features/exploredash/src/main/res/layout/dialog_gift_card_details.xml b/features/exploredash/src/main/res/layout/dialog_gift_card_details.xml index 56cf9b528d..03f0680c9d 100644 --- a/features/exploredash/src/main/res/layout/dialog_gift_card_details.xml +++ b/features/exploredash/src/main/res/layout/dialog_gift_card_details.xml @@ -321,7 +321,9 @@ android:layout_marginVertical="22dp" android:layout_marginHorizontal="22dp" android:layout_marginStart="15dp" + android:text="@string/gift_card_details_error" android:visibility="gone" + tools:visibility="visible" app:layout_constraintTop_toBottomOf="@+id/copy_card_pin" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> @@ -360,6 +362,38 @@ app:layout_constraintTop_toTopOf="parent" /> + + + + + + +