diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 463fb94b49d..f72f48c6e92 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,4 +1,8 @@ *** PLEASE FOLLOW THIS FORMAT: [] [] +17.2 +----- +- [**] [Available for users with WooCommerce version of 8.7+, which is not released yet] Every order have a receipt now. The receipts can be shared via many apps installed on the phone [https://github.com/woocommerce/woocommerce-android/pull/10650] + 17.1 ----- diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentDialogFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentDialogFragment.kt index a317c226d28..ae5b3ce8354 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentDialogFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentDialogFragment.kt @@ -22,7 +22,6 @@ import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.databinding.CardReaderPaymentDialogBinding import com.woocommerce.android.extensions.navigateBackWithNotice -import com.woocommerce.android.model.UiString import com.woocommerce.android.support.help.HelpOrigin import com.woocommerce.android.support.requests.SupportRequestFormActivity import com.woocommerce.android.ui.base.UIMessageResolver @@ -32,7 +31,6 @@ import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInR import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ExternalReaderPaymentSuccessfulReceiptSentAutomaticallyState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ExternalReaderPaymentSuccessfulState import com.woocommerce.android.ui.payments.refunds.RefundSummaryFragment.Companion.KEY_INTERAC_SUCCESS -import com.woocommerce.android.util.ActivityUtils import com.woocommerce.android.util.PrintHtmlHelper import com.woocommerce.android.util.UiHelpers import com.woocommerce.android.util.UiHelpers.getTextOfUiString @@ -86,7 +84,6 @@ class CardReaderPaymentDialogFragment : PaymentsBaseDialogFragment(R.layout.card event.documentName ) InteracRefundSuccessful -> navigateBackWithNotice(KEY_INTERAC_SUCCESS) - is SendReceipt -> composeEmail(event.address, event.subject, event.content) is ShowSnackbar -> uiMessageResolver.showSnack(event.message) is ShowSnackbarInDialog -> Snackbar.make( requireView(), event.message, BaseTransientBottomBar.LENGTH_LONG @@ -186,12 +183,6 @@ class CardReaderPaymentDialogFragment : PaymentsBaseDialogFragment(R.layout.card mp.start() } - private fun composeEmail(address: String, subject: UiString, content: UiString) { - ActivityUtils.sendEmail(requireActivity(), address, subject, content) { - viewModel.onEmailActivityNotFound() - } - } - override fun onResume() { super.onResume() AnalyticsTracker.trackViewShown(this) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModel.kt index 339e8b351a1..de62a7ab603 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModel.kt @@ -68,6 +68,7 @@ import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ReFetchi import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.RefundLoadingDataState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.RefundSuccessfulState import com.woocommerce.android.ui.payments.receipt.PaymentReceiptHelper +import com.woocommerce.android.ui.payments.receipt.PaymentReceiptShare import com.woocommerce.android.ui.payments.tracking.CardReaderTrackingInfoKeeper import com.woocommerce.android.ui.payments.tracking.PaymentsFlowTracker import com.woocommerce.android.util.CoroutineDispatchers @@ -117,6 +118,7 @@ class CardReaderPaymentViewModel private val paymentReceiptHelper: PaymentReceiptHelper, private val cardReaderOnboardingChecker: CardReaderOnboardingChecker, private val cardReaderConfigProvider: CardReaderCountryConfigProvider, + private val paymentReceiptShare: PaymentReceiptShare, ) : ScopedViewModel(savedState) { private val arguments: CardReaderPaymentDialogFragmentArgs by savedState.navArgs() @@ -611,9 +613,7 @@ class CardReaderPaymentViewModel val onSaveUserClicked = { onSaveForLaterClicked() } - val onSendReceiptClicked = { - onSendReceiptClicked(order.billingAddress.email) - } + val onSendReceiptClicked = { onSendReceiptClicked() } if (order.billingAddress.email.isBlank()) { viewState.postValue( @@ -723,27 +723,36 @@ class CardReaderPaymentViewModel } } - private fun onSendReceiptClicked(billingEmail: String) { + private fun onSendReceiptClicked() { launch { tracker.trackEmailReceiptTapped() + val stateBeforeLoading = viewState.value!! + viewState.postValue(ViewState.SharingReceiptState) val receiptResult = paymentReceiptHelper.getReceiptUrl(orderId) + if (receiptResult.isSuccess) { - triggerEvent( - SendReceipt( - content = UiStringRes( - R.string.card_reader_payment_receipt_email_content, - listOf(UiStringText(receiptResult.getOrThrow())) - ), - subject = UiStringRes( - R.string.card_reader_payment_receipt_email_subject, - listOf(UiStringText(selectedSite.get().name.orEmpty())) - ), - address = billingEmail - ) - ) + when (val sharingResult = paymentReceiptShare(receiptResult.getOrThrow(), orderId)) { + is PaymentReceiptShare.ReceiptShareResult.Error.FileCreation -> { + tracker.trackPaymentsReceiptSharingFailed(sharingResult) + triggerEvent(ShowSnackbar(R.string.card_reader_payment_receipt_can_not_be_stored)) + } + is PaymentReceiptShare.ReceiptShareResult.Error.FileDownload -> { + tracker.trackPaymentsReceiptSharingFailed(sharingResult) + triggerEvent(ShowSnackbar(R.string.card_reader_payment_receipt_can_not_be_downloaded)) + } + is PaymentReceiptShare.ReceiptShareResult.Error.Sharing -> { + tracker.trackPaymentsReceiptSharingFailed(sharingResult) + triggerEvent(ShowSnackbar(R.string.card_reader_payment_email_client_not_found)) + } + PaymentReceiptShare.ReceiptShareResult.Success -> { + // no-op + } + } } else { triggerEvent(ShowSnackbar(R.string.receipt_fetching_error)) } + + viewState.postValue(stateBeforeLoading) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModelEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModelEvent.kt index 5a44a75ebb3..6ac68063777 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModelEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModelEvent.kt @@ -1,7 +1,6 @@ package com.woocommerce.android.ui.payments.cardreader.payment import androidx.annotation.StringRes -import com.woocommerce.android.model.UiString import com.woocommerce.android.viewmodel.MultiLiveEvent.Event class ShowSnackbarInDialog(@StringRes val message: Int) : Event() @@ -17,5 +16,3 @@ object EnableNfc : Event() data class PurchaseCardReader(val url: String) : Event() data class PrintReceipt(val receiptUrl: String, val documentName: String) : Event() - -data class SendReceipt(val content: UiString, val subject: UiString, val address: String) : Event() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewState.kt index d7eaa0c5446..1f73c0126df 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewState.kt @@ -219,6 +219,15 @@ sealed class ViewState( override val isProgressVisible = true } + object SharingReceiptState : ViewState( + headerLabel = R.string.card_reader_payment_completed_payment_header, + illustration = null, + primaryActionLabel = null, + secondaryActionLabel = null, + ) { + override val isProgressVisible = true + } + object ReFetchingOrderState : ViewState( headerLabel = R.string.card_reader_payment_fetch_order_loading_header, hintLabel = R.string.card_reader_payment_fetch_order_loading_hint, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptHelper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptHelper.kt index 53dea128b02..8832e65cfeb 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptHelper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptHelper.kt @@ -110,7 +110,7 @@ class PaymentReceiptHelper @Inject constructor( const val WCPAY_RECEIPTS_SENDING_SUPPORT_VERSION = "4.0.0" const val WC_CAN_GENERATE_RECEIPTS_VERSION = "8.7.0" - const val RECEIPT_EXPIRATION_DAYS = 365 + const val RECEIPT_EXPIRATION_DAYS = 2 } class IsDevSiteSupported @Inject constructor() { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptShare.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptShare.kt new file mode 100644 index 00000000000..d2a1c502470 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptShare.kt @@ -0,0 +1,59 @@ +package com.woocommerce.android.ui.payments.receipt + +import android.app.Application +import android.content.Intent +import android.os.Environment +import androidx.core.content.FileProvider +import com.woocommerce.android.media.FileUtils +import com.woocommerce.android.util.FileDownloader +import javax.inject.Inject + +class PaymentReceiptShare @Inject constructor( + private val fileUtils: FileUtils, + private val fileDownloader: FileDownloader, + private val context: Application, +) { + @Suppress("TooGenericExceptionCaught") + suspend operator fun invoke(receiptUrl: String, orderNumber: Long): ReceiptShareResult { + val receiptFile = fileUtils.createTempTimeStampedFile( + storageDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + ?: context.filesDir, + prefix = "receipt_$orderNumber", + fileExtension = "html" + ) + return if (receiptFile == null) { + ReceiptShareResult.Error.FileCreation + } else if (!fileDownloader.downloadFile(receiptUrl, receiptFile)) { + ReceiptShareResult.Error.FileDownload + } else { + val uri = FileProvider.getUriForFile( + context, + context.packageName + ".provider", + receiptFile + ) + val intent = Intent(Intent.ACTION_SEND).apply { + type = "application/*" + putExtra(Intent.EXTRA_STREAM, uri) + } + try { + context.startActivity( + Intent.createChooser(intent, null).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + ReceiptShareResult.Success + } catch (e: Exception) { + ReceiptShareResult.Error.Sharing(e) + } + } + } + + sealed class ReceiptShareResult { + object Success : ReceiptShareResult() + sealed class Error : ReceiptShareResult() { + data class Sharing(val exception: Exception) : Error() + object FileCreation : Error() + object FileDownload : Error() + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewFragment.kt index 9523b67ec65..30753e17d69 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewFragment.kt @@ -14,7 +14,6 @@ import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.databinding.FragmentReceiptPreviewBinding import com.woocommerce.android.ui.base.BaseFragment import com.woocommerce.android.ui.base.UIMessageResolver -import com.woocommerce.android.util.ActivityUtils import com.woocommerce.android.util.PrintHtmlHelper import com.woocommerce.android.util.UiHelpers import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ShowSnackbar @@ -52,7 +51,7 @@ class ReceiptPreviewFragment : BaseFragment(R.layout.fragment_receipt_preview), true } R.id.menu_send -> { - viewModel.onSendEmailClicked() + viewModel.onShareClicked() true } else -> false @@ -102,16 +101,9 @@ class ReceiptPreviewFragment : BaseFragment(R.layout.fragment_receipt_preview), when (it) { is LoadUrl -> binding.receiptPreviewPreviewWebview.loadUrl(it.url) is PrintReceipt -> printHtmlHelper.printReceipt(requireActivity(), it.receiptUrl, it.documentName) - is SendReceipt -> composeEmail(it) is ShowSnackbar -> uiMessageResolver.showSnack(it.message) else -> it.isHandled = false } } } - - private fun composeEmail(event: SendReceipt) { - ActivityUtils.sendEmail(requireActivity(), event.address, event.subject, event.content) { - viewModel.onEmailActivityNotFound() - } - } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModel.kt index e609731afa8..b74846b8e71 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModel.kt @@ -4,18 +4,16 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import com.woocommerce.android.R.string -import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_EMAIL_FAILED import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_EMAIL_TAPPED import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_PRINT_CANCELED import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_PRINT_FAILED import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_PRINT_SUCCESS import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_PRINT_TAPPED import com.woocommerce.android.analytics.AnalyticsTrackerWrapper -import com.woocommerce.android.model.UiString.UiStringRes -import com.woocommerce.android.model.UiString.UiStringText -import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.payments.receipt.PaymentReceiptShare import com.woocommerce.android.ui.payments.receipt.preview.ReceiptPreviewViewModel.ReceiptPreviewViewState.Content import com.woocommerce.android.ui.payments.receipt.preview.ReceiptPreviewViewModel.ReceiptPreviewViewState.Loading +import com.woocommerce.android.ui.payments.tracking.PaymentsFlowTracker import com.woocommerce.android.util.PrintHtmlHelper.PrintJobResult import com.woocommerce.android.util.PrintHtmlHelper.PrintJobResult.CANCELLED import com.woocommerce.android.util.PrintHtmlHelper.PrintJobResult.FAILED @@ -32,7 +30,8 @@ class ReceiptPreviewViewModel @Inject constructor( savedState: SavedStateHandle, private val tracker: AnalyticsTrackerWrapper, - private val selectedSite: SelectedSite, + private val paymentsFlowTracker: PaymentsFlowTracker, + private val paymentReceiptShare: PaymentReceiptShare, ) : ScopedViewModel(savedState) { private val args: ReceiptPreviewFragmentArgs by savedState.navArgs() @@ -52,28 +51,31 @@ class ReceiptPreviewViewModel triggerEvent(PrintReceipt(args.receiptUrl, "receipt-order-${args.orderId}")) } - fun onSendEmailClicked() { + fun onShareClicked() { launch { + viewState.value = Loading + tracker.track(RECEIPT_EMAIL_TAPPED) - triggerEvent( - SendReceipt( - content = UiStringRes( - string.card_reader_payment_receipt_email_content, - listOf(UiStringText(args.receiptUrl)) - ), - subject = UiStringRes( - string.card_reader_payment_receipt_email_subject, - listOf(UiStringText(selectedSite.get().name.orEmpty())) - ), - address = args.billingEmail - ) - ) - } - } + when (val sharingResult = paymentReceiptShare(args.receiptUrl, args.orderId)) { + is PaymentReceiptShare.ReceiptShareResult.Error.FileCreation -> { + paymentsFlowTracker.trackPaymentsReceiptSharingFailed(sharingResult) + triggerEvent(ShowSnackbar(string.card_reader_payment_receipt_can_not_be_stored)) + } + is PaymentReceiptShare.ReceiptShareResult.Error.FileDownload -> { + paymentsFlowTracker.trackPaymentsReceiptSharingFailed(sharingResult) + triggerEvent(ShowSnackbar(string.card_reader_payment_receipt_can_not_be_downloaded)) + } + is PaymentReceiptShare.ReceiptShareResult.Error.Sharing -> { + paymentsFlowTracker.trackPaymentsReceiptSharingFailed(sharingResult) + triggerEvent(ShowSnackbar(string.card_reader_payment_email_client_not_found)) + } + PaymentReceiptShare.ReceiptShareResult.Success -> { + // no-op + } + } - fun onEmailActivityNotFound() { - tracker.track(RECEIPT_EMAIL_FAILED) - triggerEvent(ShowSnackbar(string.card_reader_payment_email_client_not_found)) + viewState.value = Content + } } fun onPrintResult(result: PrintJobResult) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModelEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModelEvent.kt index 5edf89f0559..de027028483 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModelEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModelEvent.kt @@ -1,10 +1,7 @@ package com.woocommerce.android.ui.payments.receipt.preview -import com.woocommerce.android.model.UiString import com.woocommerce.android.viewmodel.MultiLiveEvent data class LoadUrl(val url: String) : MultiLiveEvent.Event() data class PrintReceipt(val receiptUrl: String, val documentName: String) : MultiLiveEvent.Event() - -data class SendReceipt(val content: UiString, val subject: UiString, val address: String) : MultiLiveEvent.Event() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/tracking/PaymentsFlowTracker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/tracking/PaymentsFlowTracker.kt index 0e7b2739a57..9cdce47de8a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/tracking/PaymentsFlowTracker.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/tracking/PaymentsFlowTracker.kt @@ -73,6 +73,7 @@ import com.woocommerce.android.ui.payments.cardreader.onboarding.PluginType import com.woocommerce.android.ui.payments.cardreader.onboarding.PluginType.STRIPE_EXTENSION_GATEWAY import com.woocommerce.android.ui.payments.cardreader.onboarding.PluginType.WOOCOMMERCE_PAYMENTS import com.woocommerce.android.ui.payments.hub.PaymentsHubViewModel.CashOnDeliverySource +import com.woocommerce.android.ui.payments.receipt.PaymentReceiptShare import com.woocommerce.android.ui.payments.taptopay.TapToPayAvailabilityStatus.Result.NotAvailable import javax.inject.Inject @@ -580,6 +581,32 @@ class PaymentsFlowTracker @Inject constructor( ) } + fun trackPaymentsReceiptSharingFailed(sharingResult: PaymentReceiptShare.ReceiptShareResult.Error) { + when (sharingResult) { + is PaymentReceiptShare.ReceiptShareResult.Error.FileCreation -> { + track( + RECEIPT_EMAIL_FAILED, + errorType = "file_creation_failed", + errorDescription = "File creation failed" + ) + } + is PaymentReceiptShare.ReceiptShareResult.Error.FileDownload -> { + track( + RECEIPT_EMAIL_FAILED, + errorType = "file_download_failed", + errorDescription = "File download failed" + ) + } + is PaymentReceiptShare.ReceiptShareResult.Error.Sharing -> { + track( + RECEIPT_EMAIL_FAILED, + errorType = "no_app_found", + errorDescription = sharingResult.exception.message + ) + } + } + } + private fun getAndResetFlowsDuration(): MutableMap { val result = mutableMapOf() .also { mutableMap -> diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index df91217db7d..7ef7a5426aa 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -1507,7 +1507,9 @@ In-Person Payment for Order #%1$s for %2$s blog_id %3$s. Your receipt from %s Thank you for your purchase! Click the link below for your payment receipt.\n\n%s - Can\'t detect your email client app + Unable to detect any application to which the receipt can be shared + Unable to download the receipt + Unable to store the receipt Error fetching order. Order state in the app might be outdated. The order is already paid Please make sure that the card reader is connected. diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt index 74bab73e34a..d735c8f6aa2 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt @@ -68,7 +68,6 @@ import com.woocommerce.android.ui.payments.cardreader.payment.PaymentFlowError.U import com.woocommerce.android.ui.payments.cardreader.payment.PlayChaChing import com.woocommerce.android.ui.payments.cardreader.payment.PrintReceipt import com.woocommerce.android.ui.payments.cardreader.payment.PurchaseCardReader -import com.woocommerce.android.ui.payments.cardreader.payment.SendReceipt import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInReaderCapturingPaymentState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInReaderCollectPaymentState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInReaderFailedPaymentState @@ -90,6 +89,7 @@ import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ReFetchi import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.RefundLoadingDataState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.RefundSuccessfulState import com.woocommerce.android.ui.payments.receipt.PaymentReceiptHelper +import com.woocommerce.android.ui.payments.receipt.PaymentReceiptShare import com.woocommerce.android.ui.payments.tracking.CardReaderTrackingInfoKeeper import com.woocommerce.android.ui.payments.tracking.PaymentsFlowTracker import com.woocommerce.android.util.CurrencyFormatter @@ -182,6 +182,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { private val cardReaderOnboardingChecker: CardReaderOnboardingChecker = mock() private val cardReaderConfigProvider: CardReaderCountryConfigProvider = mock() private val cardReaderConfig: CardReaderConfigForSupportedCountry = CardReaderConfigForUSA + private val paymentReceiptShare: PaymentReceiptShare = mock() @Suppress("LongMethod") @Before @@ -209,6 +210,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { paymentReceiptHelper = paymentReceiptHelper, cardReaderOnboardingChecker = cardReaderOnboardingChecker, cardReaderConfigProvider = cardReaderConfigProvider, + paymentReceiptShare = paymentReceiptShare, ) whenever(orderRepository.getOrderById(any())).thenReturn(mockedOrder) @@ -2356,20 +2358,23 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } @Test - fun `given external reader and receipt fetching success, when user clicks on send receipt button, then SendReceipt event emitted`() = + fun `given external reader and receipt fetching and sharing success, when user clicks on send receipt button, then PlayChaChing emitted`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(PaymentCompleted("")) } + flow { emit(PaymentCompleted("url")) } } + whenever(paymentReceiptShare("test url", 1L)).thenReturn( + PaymentReceiptShare.ReceiptShareResult.Success + ) viewModel.start() (viewModel.viewStateData.value as ExternalReaderPaymentSuccessfulState).onSecondaryActionClicked.invoke() - assertThat(viewModel.event.value).isInstanceOf(SendReceipt::class.java) + assertThat(viewModel.event.value).isEqualTo(PlayChaChing) } @Test - fun `given built in reader and receipt fetching success, when user clicks on send receipt button, then SendReceipt event emitted`() = + fun `given built in reader and receipt fetching and sharing success, when user clicks on send receipt button, then PlayChaChing emitted`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { flow { emit(PaymentCompleted("")) } @@ -2381,7 +2386,63 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { (viewModel.viewStateData.value as BuiltInReaderPaymentSuccessfulState).onSecondaryActionClicked.invoke() - assertThat(viewModel.event.value).isInstanceOf(SendReceipt::class.java) + assertThat(viewModel.event.value).isEqualTo(PlayChaChing) + } + + @Test + fun `given receipt fetching success and receipt file not created, when user clicks on send receipt button, then ShowSnackbar emitted`() = + testBlocking { + whenever(cardReaderManager.collectPayment(any())).thenAnswer { + flow { emit(PaymentCompleted("url")) } + } + whenever(paymentReceiptShare("test url", 1L)).thenReturn( + PaymentReceiptShare.ReceiptShareResult.Error.FileCreation + ) + viewModel.start() + + (viewModel.viewStateData.value as ExternalReaderPaymentSuccessfulState).onSecondaryActionClicked.invoke() + + assertThat((viewModel.event.value as ShowSnackbar).message).isEqualTo( + R.string.card_reader_payment_receipt_can_not_be_stored + ) + verify(tracker).trackPaymentsReceiptSharingFailed(PaymentReceiptShare.ReceiptShareResult.Error.FileCreation) + } + + @Test + fun `given receipt fetching success and receipt file not downloaded, when user clicks on send receipt button, then ShowSnackbar emitted`() = + testBlocking { + whenever(cardReaderManager.collectPayment(any())).thenAnswer { + flow { emit(PaymentCompleted("url")) } + } + whenever(paymentReceiptShare("test url", 1L)).thenReturn( + PaymentReceiptShare.ReceiptShareResult.Error.FileDownload + ) + viewModel.start() + + (viewModel.viewStateData.value as ExternalReaderPaymentSuccessfulState).onSecondaryActionClicked.invoke() + + assertThat((viewModel.event.value as ShowSnackbar).message).isEqualTo( + R.string.card_reader_payment_receipt_can_not_be_downloaded + ) + verify(tracker).trackPaymentsReceiptSharingFailed(PaymentReceiptShare.ReceiptShareResult.Error.FileDownload) + } + + @Test + fun `given receipt fetching success and receipt file not shared, when user clicks on send receipt button, then ShowSnackbar emitted`() = + testBlocking { + whenever(cardReaderManager.collectPayment(any())).thenAnswer { + flow { emit(PaymentCompleted("url")) } + } + val sharing = PaymentReceiptShare.ReceiptShareResult.Error.Sharing(Exception()) + whenever(paymentReceiptShare("test url", 1L)).thenReturn(sharing) + viewModel.start() + + (viewModel.viewStateData.value as ExternalReaderPaymentSuccessfulState).onSecondaryActionClicked.invoke() + + assertThat((viewModel.event.value as ShowSnackbar).message).isEqualTo( + R.string.card_reader_payment_email_client_not_found + ) + verify(tracker).trackPaymentsReceiptSharingFailed(sharing) } @Test @@ -4401,6 +4462,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { paymentReceiptHelper = paymentReceiptHelper, cardReaderOnboardingChecker = cardReaderOnboardingChecker, cardReaderConfigProvider = cardReaderConfigProvider, + paymentReceiptShare = paymentReceiptShare, ) } @@ -4433,6 +4495,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { paymentReceiptHelper = paymentReceiptHelper, cardReaderOnboardingChecker = cardReaderOnboardingChecker, cardReaderConfigProvider = cardReaderConfigProvider, + paymentReceiptShare = paymentReceiptShare, ) } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptHelperTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptHelperTest.kt index 8aa5087bcc1..18b25902f08 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptHelperTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptHelperTest.kt @@ -102,7 +102,7 @@ class PaymentReceiptHelperTest : BaseUnitTest() { WooCommerceStore.WooPlugin.WOO_CORE ) ).thenReturn(plugin) - whenever(orderStore.fetchOrdersReceipt(site, 1, expirationDays = 365)).thenReturn( + whenever(orderStore.fetchOrdersReceipt(site, 1, expirationDays = 2)).thenReturn( WooPayload(OrderReceiptResponse("url", "date")) ) @@ -126,7 +126,7 @@ class PaymentReceiptHelperTest : BaseUnitTest() { WooCommerceStore.WooPlugin.WOO_CORE ) ).thenReturn(plugin) - whenever(orderStore.fetchOrdersReceipt(site, 1, expirationDays = 365)).thenReturn( + whenever(orderStore.fetchOrdersReceipt(site, 1, expirationDays = 2)).thenReturn( WooPayload( WooError( type = WooErrorType.API_ERROR, @@ -156,7 +156,7 @@ class PaymentReceiptHelperTest : BaseUnitTest() { selectedSite.get(), ) ).thenReturn(listOf(plugin)) - whenever(orderStore.fetchOrdersReceipt(site, 1, expirationDays = 365)).thenReturn( + whenever(orderStore.fetchOrdersReceipt(site, 1, expirationDays = 2)).thenReturn( WooPayload( WooError( type = WooErrorType.API_ERROR, @@ -187,7 +187,7 @@ class PaymentReceiptHelperTest : BaseUnitTest() { selectedSite.get(), ) ).thenReturn(listOf(plugin)) - whenever(orderStore.fetchOrdersReceipt(site, 1, expirationDays = 365)).thenReturn( + whenever(orderStore.fetchOrdersReceipt(site, 1, expirationDays = 2)).thenReturn( WooPayload(OrderReceiptResponse("url", "date")) ) whenever(isDevSiteSupported()).thenReturn(true) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptShareTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptShareTest.kt new file mode 100644 index 00000000000..966918a2284 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/PaymentReceiptShareTest.kt @@ -0,0 +1,68 @@ +package com.woocommerce.android.ui.payments.receipt + +import android.app.Application +import com.woocommerce.android.media.FileUtils +import com.woocommerce.android.util.FileDownloader +import com.woocommerce.android.viewmodel.BaseUnitTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.io.File + +@ExperimentalCoroutinesApi +class PaymentReceiptShareTest : BaseUnitTest() { + private val fileUtils: FileUtils = mock() + private val fileDownloader: FileDownloader = mock() + + private val file: File = mock() + private val context: Application = mock { + on { getExternalFilesDir(anyOrNull()) }.thenReturn(file) + } + + private val sut = PaymentReceiptShare( + fileUtils = fileUtils, + fileDownloader = fileDownloader, + context = context, + ) + + @Test + fun `given file not created, when invoke, then FileCreation error returned`() = testBlocking { + // GIVEN + whenever( + fileUtils.createTempTimeStampedFile( + storageDir = anyOrNull(), + prefix = eq("receipt_999"), + fileExtension = eq("html"), + ) + ).thenReturn(null) + + // WHEN + val result = sut("receiptUrl", 999L) + + // THEN + assertThat(result).isInstanceOf(PaymentReceiptShare.ReceiptShareResult.Error.FileCreation::class.java) + } + + @Test + fun `given file created but not downloaded, when invoke, then FileDownload error returned`() = testBlocking { + // GIVEN + whenever( + fileUtils.createTempTimeStampedFile( + storageDir = anyOrNull(), + prefix = eq("receipt_999"), + fileExtension = eq("html"), + ) + ).thenReturn(file) + whenever(fileDownloader.downloadFile(eq("receiptUrl"), eq(file))).thenReturn(false) + + // WHEN + val result = sut("receiptUrl", 999L) + + // THEN + assertThat(result).isInstanceOf(PaymentReceiptShare.ReceiptShareResult.Error.FileDownload::class.java) + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModelTest.kt index e0845ba0b96..b4e002726cd 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/receipt/preview/ReceiptPreviewViewModelTest.kt @@ -1,21 +1,22 @@ package com.woocommerce.android.ui.payments.receipt.preview import androidx.lifecycle.SavedStateHandle -import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_EMAIL_FAILED +import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_EMAIL_TAPPED import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_PRINT_CANCELED import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_PRINT_FAILED import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_PRINT_SUCCESS import com.woocommerce.android.analytics.AnalyticsEvent.RECEIPT_PRINT_TAPPED import com.woocommerce.android.analytics.AnalyticsTrackerWrapper -import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.payments.receipt.PaymentReceiptShare import com.woocommerce.android.ui.payments.receipt.preview.ReceiptPreviewViewModel.ReceiptPreviewViewState.Content import com.woocommerce.android.ui.payments.receipt.preview.ReceiptPreviewViewModel.ReceiptPreviewViewState.Loading +import com.woocommerce.android.ui.payments.tracking.PaymentsFlowTracker import com.woocommerce.android.util.PrintHtmlHelper.PrintJobResult.CANCELLED import com.woocommerce.android.util.PrintHtmlHelper.PrintJobResult.FAILED import com.woocommerce.android.util.PrintHtmlHelper.PrintJobResult.STARTED import com.woocommerce.android.viewmodel.BaseUnitTest -import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ShowSnackbar +import com.woocommerce.android.viewmodel.MultiLiveEvent import kotlinx.coroutines.ExperimentalCoroutinesApi import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -23,14 +24,14 @@ import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import org.wordpress.android.fluxc.model.SiteModel @ExperimentalCoroutinesApi class ReceiptPreviewViewModelTest : BaseUnitTest() { private lateinit var viewModel: ReceiptPreviewViewModel - private val selectedSite: SelectedSite = mock() private val tracker: AnalyticsTrackerWrapper = mock() + private val paymentsFlowTracker: PaymentsFlowTracker = mock() + private val paymentReceiptShare: PaymentReceiptShare = mock() private val savedState: SavedStateHandle = ReceiptPreviewFragmentArgs( receiptUrl = "testing url", @@ -40,8 +41,7 @@ class ReceiptPreviewViewModelTest : BaseUnitTest() { @Before fun setUp() { - viewModel = ReceiptPreviewViewModel(savedState, tracker, selectedSite) - whenever(selectedSite.get()).thenReturn(SiteModel().apply { name = "testName" }) + viewModel = ReceiptPreviewViewModel(savedState, tracker, paymentsFlowTracker, paymentReceiptShare) } @Test @@ -73,35 +73,85 @@ class ReceiptPreviewViewModelTest : BaseUnitTest() { } @Test - fun `when user clicks on send email, then send receipt event emitted`() = + fun `when user clicks on send email, then event tracked`() = testBlocking { - viewModel.onSendEmailClicked() + viewModel.onShareClicked() - assertThat(viewModel.event.value).isInstanceOf(SendReceipt::class.java) + verify(tracker).track(RECEIPT_EMAIL_TAPPED) } @Test - fun `when user clicks on send email, then event tracked`() = + fun `given sharing success, when onShareClicked, then no events emitted`() = testBlocking { - viewModel.onSendEmailClicked() + // GIVEN + whenever(paymentReceiptShare("testing url", 999L)).thenReturn( + PaymentReceiptShare.ReceiptShareResult.Success + ) - verify(tracker).track(RECEIPT_EMAIL_TAPPED) + // WHEN + viewModel.onShareClicked() + + // THEN + assertThat(viewModel.event.value).isInstanceOf(LoadUrl::class.java) } @Test - fun `when email application not found, then SnackBar with error shown`() = + fun `given sharing failed with file cretion, when onShareClicked, then ShowSnackbar emitted`() = testBlocking { - viewModel.onEmailActivityNotFound() - - assertThat(viewModel.event.value).isInstanceOf(ShowSnackbar::class.java) + // GIVEN + whenever(paymentReceiptShare("testing url", 999L)).thenReturn( + PaymentReceiptShare.ReceiptShareResult.Error.FileCreation + ) + + // WHEN + viewModel.onShareClicked() + + // THEN + assertThat((viewModel.event.value as MultiLiveEvent.Event.ShowSnackbar).message).isEqualTo( + R.string.card_reader_payment_receipt_can_not_be_stored + ) + verify(paymentsFlowTracker).trackPaymentsReceiptSharingFailed( + PaymentReceiptShare.ReceiptShareResult.Error.FileCreation + ) } @Test - fun `when email application not found, then event tracked`() = + fun `given sharing failed with file downloading, when onShareClicked, then ShowSnackbar emitted`() = testBlocking { - viewModel.onEmailActivityNotFound() + // GIVEN + whenever(paymentReceiptShare("testing url", 999L)).thenReturn( + PaymentReceiptShare.ReceiptShareResult.Error.FileDownload + ) + + // WHEN + viewModel.onShareClicked() + + // THEN + assertThat((viewModel.event.value as MultiLiveEvent.Event.ShowSnackbar).message).isEqualTo( + R.string.card_reader_payment_receipt_can_not_be_downloaded + ) + verify(paymentsFlowTracker).trackPaymentsReceiptSharingFailed( + PaymentReceiptShare.ReceiptShareResult.Error.FileDownload + ) + } - verify(tracker).track(RECEIPT_EMAIL_FAILED) + @Test + fun `given sharing failed with file sharing, when onShareClicked, then ShowSnackbar emitted`() = + testBlocking { + // GIVEN + val sharing = PaymentReceiptShare.ReceiptShareResult.Error.Sharing(Exception()) + whenever(paymentReceiptShare("testing url", 999L)).thenReturn(sharing) + + // WHEN + viewModel.onShareClicked() + + // THEN + assertThat((viewModel.event.value as MultiLiveEvent.Event.ShowSnackbar).message).isEqualTo( + R.string.card_reader_payment_email_client_not_found + ) + verify(paymentsFlowTracker).trackPaymentsReceiptSharingFailed( + sharing + ) } @Test diff --git a/build.gradle b/build.gradle index 55a46108b90..47c8eb55add 100644 --- a/build.gradle +++ b/build.gradle @@ -96,7 +96,7 @@ tasks.register("installGitHooks", Copy) { } ext { - fluxCVersion = '2948-235834ae9467ae3ca367a4b13c4f9c6aea0f31bf' + fluxCVersion = 'trunk-b34cc5eed609ca5274fbb442991f657f2380de17' glideVersion = '4.13.2' coilVersion = '2.1.0' constraintLayoutVersion = '1.2.0'