Bug 1899365 - Remove review quality checker feature and strings r=android-reviewers,delphine,007
Differential Revision: https://phabricator.services.mozilla.com/D234089
@@ -3,12 +3,12 @@
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<color name="mozac_ui_icons_fill">#FFFFFF</color>
|
||||
|
||||
<!-- Star icon fill colors for mozac_ic_star_one_half_fill_20 -->
|
||||
<color name="mozac_ic_star_filled">#000000</color>
|
||||
<color name="mozac_ic_star_unfilled">#FFFFFF</color>
|
||||
<color name="mozac_ic_star_filled" tools:ignore="UnusedResources">#000000</color>
|
||||
<color name="mozac_ic_star_unfilled" tools:ignore="UnusedResources">#FFFFFF</color>
|
||||
|
||||
<!-- Private Mode mask icon circle fill colors for mozac_ic_private_mode_circle_fill_stroke_20 -->
|
||||
<color name="mozac_ui_private_mode_circle_fill">#000000</color>
|
||||
|
||||
@@ -202,17 +202,6 @@ search-term-groups:
|
||||
enabled:
|
||||
type: boolean
|
||||
description: "If true, the feature shows up on the homescreen and on the new tab screen."
|
||||
shopping-experience:
|
||||
description: A feature that shows product review quality information.
|
||||
hasExposure: true
|
||||
exposureDescription: ""
|
||||
variables:
|
||||
enabled:
|
||||
type: boolean
|
||||
description: "if true, the shopping experience feature is shown to the user."
|
||||
product-recommendations:
|
||||
type: boolean
|
||||
description: "if true, recommended products feature is enabled to be shown to the user based on their preference."
|
||||
splash-screen:
|
||||
description: "A feature that extends splash screen duration, allowing additional data fetching time for the app's initial run."
|
||||
hasExposure: true
|
||||
|
||||
@@ -11427,586 +11427,6 @@ sync:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
|
||||
shopping:
|
||||
address_bar_icon_displayed:
|
||||
type: event
|
||||
description: |
|
||||
The shopping icon was displayed in the address bar.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
address_bar_icon_clicked:
|
||||
type: event
|
||||
description: |
|
||||
The shopping icon from the address bar was clicked.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_closed:
|
||||
type: event
|
||||
description: |
|
||||
The shopping bottom sheet was closed.
|
||||
extra_keys:
|
||||
source:
|
||||
description: |
|
||||
The method used to close the bottom sheet. Possible values are: `sheet_slide`,
|
||||
`link_opened`, `click_outside`, `back_pressed`, `handle_clicked`, `not_now`
|
||||
or `opt_out`.
|
||||
type: string
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1855150
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4060
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_displayed:
|
||||
type: event
|
||||
description: |
|
||||
The shopping bottom sheet was displayed.
|
||||
extra_keys:
|
||||
view:
|
||||
description: |
|
||||
State of the bottom sheet. Possible values are: `half` or `full`.
|
||||
type: string
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1855150
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4060
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_onboarding_displayed:
|
||||
type: event
|
||||
description: |
|
||||
The shopping contextual onboarding card was displayed.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_review_quality_explainer_url_clicked:
|
||||
type: event
|
||||
description: |
|
||||
User interacted with the learn more link from the shopping explanation card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_show_terms_clicked:
|
||||
type: event
|
||||
description: |
|
||||
User interacted with the terms and conditions link from the shopping contextual onboarding card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_show_privacy_policy_clicked:
|
||||
type: event
|
||||
description: |
|
||||
User interacted with the privacy policy link from the shopping contextual onboarding card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_not_now_clicked:
|
||||
type: event
|
||||
description: |
|
||||
User interacted with the opt out button from the shopping contextual onboarding card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_opt_in_accepted:
|
||||
type: event
|
||||
description: |
|
||||
User interacted with the opt in button from the shopping contextual onboarding card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_show_more_recent_reviews_clicked:
|
||||
type: event
|
||||
description: |
|
||||
User interacted with the expand "show more" button from the shopping highlights card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_learn_more_clicked:
|
||||
type: event
|
||||
description: |
|
||||
The user clicked on learn more link from the onboarding card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_expand_settings:
|
||||
type: event
|
||||
description: |
|
||||
Settings card from the bottom sheet was expanded.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_no_review_reliability_available:
|
||||
type: event
|
||||
description: |
|
||||
No analysis card was displayed to the user.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_analyze_reviews_none_available_clicked:
|
||||
type: event
|
||||
description: |
|
||||
User clicked on the launch analyzer link from the no analysis card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_reanalyze_clicked:
|
||||
type: event
|
||||
description: |
|
||||
User interacted with the launch analyzer link from the "New info to check" warning card.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_reactivated_button_clicked:
|
||||
type: event
|
||||
description: |
|
||||
User interacted with the button to report a product is back in stock.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
product_page_visits:
|
||||
type: counter
|
||||
description: |
|
||||
Counts number of visits to a supported retailer product page
|
||||
while enrolled in either the control or treatment branches
|
||||
of the shopping experiment.
|
||||
send_in_pings:
|
||||
- metrics
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1854501
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4120#issuecomment-1768423370
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_powered_by_fakespot_link_clicked:
|
||||
type: event
|
||||
description: |
|
||||
The user clicked the "Fakespot by Mozilla" link at the bottom of review checker
|
||||
sheet.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862775
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4354#issuecomment-1794341141
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_stale_analysis_shown:
|
||||
type: event
|
||||
description: |
|
||||
Records an event when the "New info to check" card is shown.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862776
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4379#issuecomment-1794925138
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_ads_impression:
|
||||
type: event
|
||||
description: |
|
||||
The user viewed an ad in review checker for at least 1.5 seconds.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1865854
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4604#issuecomment-1827890623
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_ads_clicked:
|
||||
type: event
|
||||
description: |
|
||||
The user clicked an ad in review checker.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1865854
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4604#issuecomment-1827890623
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_ads_setting_toggled:
|
||||
type: event
|
||||
description: |
|
||||
The user toggled the ads display setting.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1865854
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4604#issuecomment-1827890623
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
extra_keys:
|
||||
action:
|
||||
type: string
|
||||
description: |
|
||||
Whether the toggle was used to enable or disable ads. Possible values
|
||||
are `enabled` and `disabled`.
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
ads_exposure:
|
||||
type: event
|
||||
description: |
|
||||
On a supported product page, the review checker showed analysis,
|
||||
and the ads exposure pref was enabled, or review checker ads were enabled,
|
||||
and when we tried to fetch an ad from the ad server, an ad was available.
|
||||
Does not indicate whether the ad was actually shown.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866992
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4622#issuecomment-1829905076
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
surface_no_ads_available:
|
||||
type: event
|
||||
description: |
|
||||
On a supported product page, the review checker
|
||||
showed analysis, and review checker ads were enabled,
|
||||
but when we tried to fetch an ad from the ad server,
|
||||
no ad was available.
|
||||
send_in_pings:
|
||||
- events
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866992
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4622#issuecomment-1829905076
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
|
||||
shopping.settings:
|
||||
component_opted_out:
|
||||
type: boolean
|
||||
lifetime: application
|
||||
description: |
|
||||
Whether or not the user opted out of review quality check feature.
|
||||
send_in_pings:
|
||||
- metrics
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
nimbus_disabled_shopping:
|
||||
type: boolean
|
||||
lifetime: application
|
||||
description: |
|
||||
Whether or not Nimbus has disabled the use of the shopping component.
|
||||
send_in_pings:
|
||||
- metrics
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
user_has_onboarded:
|
||||
type: boolean
|
||||
lifetime: application
|
||||
description: |
|
||||
Whether or the user has completed the review quality check onboarding.
|
||||
send_in_pings:
|
||||
- metrics
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1843508
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/3619
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4039
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
disabled_ads:
|
||||
type: boolean
|
||||
lifetime: application
|
||||
description: |
|
||||
Indicates if the user has disabled ads.
|
||||
send_in_pings:
|
||||
- metrics
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1865854
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/firefox-android/pull/4604#issuecomment-1827890623
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- android-probes@mozilla.com
|
||||
expires: never
|
||||
metadata:
|
||||
tags:
|
||||
- Shopping
|
||||
fx_suggest:
|
||||
ping_type:
|
||||
type: string
|
||||
|
||||
@@ -295,28 +295,6 @@ features:
|
||||
type: Boolean
|
||||
default: false
|
||||
|
||||
shopping-experience:
|
||||
description: A feature that shows product review quality information.
|
||||
variables:
|
||||
enabled:
|
||||
description: if true, the shopping experience feature is shown to the user.
|
||||
type: Boolean
|
||||
default: false
|
||||
product-recommendations:
|
||||
description: if true, recommended products feature is enabled to be shown to the user based on their preference.
|
||||
type: Boolean
|
||||
default: false
|
||||
product-recommendations-exposure:
|
||||
description: if true, we want to record recommended products inventory for opted-in users, even if product recommendations are disabled.
|
||||
type: Boolean
|
||||
default: false
|
||||
defaults:
|
||||
- channel: developer
|
||||
value:
|
||||
enabled: true
|
||||
product-recommendations: true
|
||||
product-recommendations-exposure: true
|
||||
|
||||
print:
|
||||
description: A feature for printing from the share or browser menu.
|
||||
variables:
|
||||
|
||||
@@ -37,7 +37,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
|
||||
FromLoginDetailFragment(R.id.loginDetailFragment),
|
||||
FromTabsTray(R.id.tabsTrayFragment),
|
||||
FromRecentlyClosed(R.id.recentlyClosedFragment),
|
||||
FromReviewQualityCheck(R.id.reviewQualityCheckFragment),
|
||||
FromAddonsManagementFragment(R.id.addonsManagementFragment),
|
||||
FromTranslationsDialogFragment(R.id.translationsDialogFragment),
|
||||
FromDownloadLanguagesPreferenceFragment(R.id.downloadLanguagesPreferenceFragment),
|
||||
|
||||
@@ -80,7 +80,6 @@ import org.mozilla.fenix.GleanMetrics.Metrics
|
||||
import org.mozilla.fenix.GleanMetrics.PerfStartup
|
||||
import org.mozilla.fenix.GleanMetrics.Preferences
|
||||
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
|
||||
import org.mozilla.fenix.GleanMetrics.ShoppingSettings
|
||||
import org.mozilla.fenix.GleanMetrics.TopSites
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.components.Core
|
||||
@@ -826,13 +825,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
|
||||
}
|
||||
|
||||
setAutofillMetrics()
|
||||
|
||||
with(ShoppingSettings) {
|
||||
componentOptedOut.set(!settings.isReviewQualityCheckEnabled)
|
||||
nimbusDisabledShopping.set(!FxNimbus.features.shoppingExperience.value().enabled)
|
||||
userHasOnboarded.set(settings.reviewQualityCheckOptInTimeInMillis != 0L)
|
||||
disabledAds.set(!settings.isReviewQualityCheckProductRecommendationsEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
@@ -36,12 +35,10 @@ import mozilla.components.support.utils.ext.isLandscape
|
||||
import mozilla.telemetry.glean.private.NoExtras
|
||||
import org.mozilla.fenix.GleanMetrics.AddressToolbar
|
||||
import org.mozilla.fenix.GleanMetrics.ReaderMode
|
||||
import org.mozilla.fenix.GleanMetrics.Shopping
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled
|
||||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.SnackbarAction
|
||||
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
|
||||
import org.mozilla.fenix.components.toolbar.ToolbarMenu
|
||||
@@ -59,8 +56,6 @@ import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
import org.mozilla.fenix.nimbus.FxNimbus
|
||||
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.getCookieBannerUIMode
|
||||
import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature
|
||||
import org.mozilla.fenix.shopping.ReviewQualityCheckFeature
|
||||
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
|
||||
@@ -71,11 +66,9 @@ import org.mozilla.fenix.theme.ThemeManager
|
||||
class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
||||
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
|
||||
private val openInAppOnboardingObserver = ViewBoundFeatureWrapper<OpenInAppOnboardingObserver>()
|
||||
private val reviewQualityCheckFeature = ViewBoundFeatureWrapper<ReviewQualityCheckFeature>()
|
||||
private val translationsBinding = ViewBoundFeatureWrapper<TranslationsBinding>()
|
||||
|
||||
private var readerModeAvailable = false
|
||||
private var reviewQualityCheckAvailable = false
|
||||
private var translationsAvailable = false
|
||||
|
||||
private var pwaOnboardingObserver: PwaOnboardingObserver? = null
|
||||
@@ -124,7 +117,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
||||
|
||||
initReaderMode(context, view)
|
||||
initTranslationsAction(context, view)
|
||||
initReviewQualityCheck(context, view)
|
||||
initSharePageAction(context)
|
||||
initReloadAction(context)
|
||||
|
||||
@@ -294,7 +286,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
||||
contentDescription = context.getString(R.string.browser_menu_read),
|
||||
contentDescriptionSelected = context.getString(R.string.browser_menu_read_close),
|
||||
visible = {
|
||||
readerModeAvailable && !reviewQualityCheckAvailable
|
||||
readerModeAvailable
|
||||
},
|
||||
weight = { READER_MODE_WEIGHT },
|
||||
selected = getSafeCurrentTab()?.let {
|
||||
@@ -327,62 +319,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
||||
)
|
||||
}
|
||||
|
||||
private fun initReviewQualityCheck(context: Context, view: View) {
|
||||
val reviewQualityCheck =
|
||||
BrowserToolbar.ToggleButton(
|
||||
image = AppCompatResources.getDrawable(
|
||||
context,
|
||||
R.drawable.mozac_ic_shopping_24,
|
||||
)!!.apply {
|
||||
setTint(ContextCompat.getColor(context, R.color.fx_mobile_text_color_primary))
|
||||
},
|
||||
imageSelected = AppCompatResources.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_shopping_selected,
|
||||
)!!,
|
||||
contentDescription = context.getString(R.string.review_quality_check_open_handle_content_description),
|
||||
contentDescriptionSelected =
|
||||
context.getString(R.string.review_quality_check_close_handle_content_description),
|
||||
visible = { reviewQualityCheckAvailable },
|
||||
weight = { REVIEW_QUALITY_CHECK_WEIGHT },
|
||||
listener = { _ ->
|
||||
requireComponents.appStore.dispatch(
|
||||
ShoppingAction.ShoppingSheetStateUpdated(expanded = true),
|
||||
)
|
||||
|
||||
findNavController().navigate(
|
||||
BrowserFragmentDirections.actionBrowserFragmentToReviewQualityCheckDialogFragment(),
|
||||
)
|
||||
Shopping.addressBarIconClicked.record()
|
||||
},
|
||||
)
|
||||
|
||||
browserToolbarView.view.addPageAction(reviewQualityCheck)
|
||||
|
||||
reviewQualityCheckFeature.set(
|
||||
feature = ReviewQualityCheckFeature(
|
||||
appStore = requireComponents.appStore,
|
||||
browserStore = context.components.core.store,
|
||||
shoppingExperienceFeature = DefaultShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {
|
||||
if (!reviewQualityCheckAvailable && it) {
|
||||
Shopping.addressBarIconDisplayed.record()
|
||||
}
|
||||
reviewQualityCheckAvailable = it
|
||||
safeInvalidateBrowserToolbarView()
|
||||
},
|
||||
onBottomSheetStateChange = {
|
||||
reviewQualityCheck.setSelected(selected = it, notifyListener = false)
|
||||
},
|
||||
onProductPageDetected = {
|
||||
Shopping.productPageVisits.add()
|
||||
},
|
||||
),
|
||||
owner = this,
|
||||
view = view,
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun addLeadingAction(
|
||||
context: Context,
|
||||
|
||||
@@ -48,7 +48,6 @@ class RecordedNimbusContext(
|
||||
val utmCampaign: String,
|
||||
val utmTerm: String,
|
||||
val utmContent: String,
|
||||
val isReviewCheckerEnabled: Boolean,
|
||||
val androidSdkVersion: String = Build.VERSION.SDK_INT.toString(),
|
||||
val appVersion: String?,
|
||||
val locale: String,
|
||||
@@ -87,7 +86,6 @@ class RecordedNimbusContext(
|
||||
installReferrerResponseUtmCampaign = utmCampaign,
|
||||
installReferrerResponseUtmTerm = utmTerm,
|
||||
installReferrerResponseUtmContent = utmContent,
|
||||
isReviewCheckerEnabled = isReviewCheckerEnabled,
|
||||
androidSdkVersion = androidSdkVersion,
|
||||
appVersion = appVersion,
|
||||
locale = locale,
|
||||
@@ -130,7 +128,6 @@ class RecordedNimbusContext(
|
||||
"install_referrer_response_utm_campaign" to utmCampaign,
|
||||
"install_referrer_response_utm_term" to utmTerm,
|
||||
"install_referrer_response_utm_content" to utmContent,
|
||||
"is_review_checker_enabled" to isReviewCheckerEnabled,
|
||||
"android_sdk_version" to androidSdkVersion,
|
||||
"app_version" to appVersion,
|
||||
"locale" to locale,
|
||||
@@ -179,7 +176,6 @@ class RecordedNimbusContext(
|
||||
utmCampaign = settings.utmCampaign,
|
||||
utmTerm = settings.utmTerm,
|
||||
utmContent = settings.utmContent,
|
||||
isReviewCheckerEnabled = settings.isReviewQualityCheckEnabled,
|
||||
appVersion = packageInfo.versionName,
|
||||
locale = deviceInfo.localeTag,
|
||||
daysSinceInstall = calculatedAttributes.daysSinceInstall,
|
||||
@@ -209,7 +205,6 @@ class RecordedNimbusContext(
|
||||
utmCampaign = "",
|
||||
utmTerm = "",
|
||||
utmContent = "",
|
||||
isReviewCheckerEnabled = false,
|
||||
appVersion = "",
|
||||
locale = "",
|
||||
daysSinceInstall = 5,
|
||||
|
||||
@@ -52,7 +52,6 @@ import org.mozilla.fenix.settings.search.SearchEngineFragmentDirections
|
||||
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
|
||||
import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections
|
||||
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
|
||||
import org.mozilla.fenix.shopping.ReviewQualityCheckFragmentDirections
|
||||
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
|
||||
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
|
||||
import org.mozilla.fenix.translations.TranslationsDialogFragmentDirections
|
||||
@@ -322,8 +321,6 @@ private fun getHomeNavDirections(
|
||||
|
||||
BrowserDirection.FromStudiesFragment -> StudiesFragmentDirections.actionGlobalBrowser()
|
||||
|
||||
BrowserDirection.FromReviewQualityCheck -> ReviewQualityCheckFragmentDirections.actionGlobalBrowser()
|
||||
|
||||
BrowserDirection.FromAddonsManagementFragment -> AddonsManagementFragmentDirections.actionGlobalBrowser()
|
||||
|
||||
BrowserDirection.FromTranslationsDialogFragment -> TranslationsDialogFragmentDirections.actionGlobalBrowser()
|
||||
|
||||
@@ -39,12 +39,10 @@ object CustomAttributeProvider : JexlAttributeProvider {
|
||||
fun getCustomTargetingAttributes(context: Context): JSONObject {
|
||||
val settings = context.settings()
|
||||
val isFirstRun = settings.isFirstNimbusRun
|
||||
val isReviewCheckerEnabled = settings.isReviewQualityCheckEnabled
|
||||
return JSONObject(
|
||||
mapOf(
|
||||
// By convention, we should use snake case.
|
||||
"is_first_run" to isFirstRun,
|
||||
"is_review_checker_enabled" to isReviewCheckerEnabled,
|
||||
"install_referrer_response_utm_source" to settings.utmSource,
|
||||
"install_referrer_response_utm_medium" to settings.utmMedium,
|
||||
"install_referrer_response_utm_campaign" to settings.utmCampaign,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.scan
|
||||
import mozilla.components.lib.state.helpers.AbstractBinding
|
||||
import org.mozilla.fenix.shopping.store.BottomSheetViewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
|
||||
|
||||
/**
|
||||
* View-bound feature that requests the bottom sheet state to be changed to expanded or collapsed when
|
||||
* the store state changes from [ReviewQualityCheckState.Initial] to [ReviewQualityCheckState.NotOptedIn].
|
||||
*
|
||||
* @param store The store to observe.
|
||||
* @param isScreenReaderEnabled Used to fully expand bottom sheet when a screen reader is on.
|
||||
* @param onRequestStateUpdate Callback to request the bottom sheet to be updated.
|
||||
*/
|
||||
class ReviewQualityCheckBottomSheetStateFeature(
|
||||
store: ReviewQualityCheckStore,
|
||||
private val isScreenReaderEnabled: Boolean,
|
||||
private val onRequestStateUpdate: (expanded: BottomSheetViewState) -> Unit,
|
||||
) : AbstractBinding<ReviewQualityCheckState>(store) {
|
||||
override suspend fun onState(flow: Flow<ReviewQualityCheckState>) {
|
||||
if (isScreenReaderEnabled) {
|
||||
onRequestStateUpdate(BottomSheetViewState.FULL_VIEW)
|
||||
} else {
|
||||
val initial = Pair<ReviewQualityCheckState?, ReviewQualityCheckState?>(null, null)
|
||||
flow.scan(initial) { acc, value ->
|
||||
Pair(acc.second, value)
|
||||
}.filter {
|
||||
it.first is ReviewQualityCheckState.Initial
|
||||
}.map {
|
||||
when (it.second) {
|
||||
is ReviewQualityCheckState.NotOptedIn -> BottomSheetViewState.FULL_VIEW
|
||||
else -> BottomSheetViewState.HALF_VIEW
|
||||
}
|
||||
}.collect {
|
||||
onRequestStateUpdate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.state.ContentState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.lib.state.ext.flowScoped
|
||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
|
||||
private const val DEBOUNCE_TIMEOUT_MILLIS = 200L
|
||||
|
||||
/**
|
||||
* Feature implementation that provides review quality check information for supported product
|
||||
* pages.
|
||||
*
|
||||
* @param appStore Reference to the application's [AppStore].
|
||||
* @param browserStore Reference to the application's [BrowserStore].
|
||||
* @param shoppingExperienceFeature Reference to the [ShoppingExperienceFeature].
|
||||
* @param onIconVisibilityChange Invoked when shopping icon visibility changes based on feature
|
||||
* flag and when the loaded page is a supported product page.
|
||||
* @param onBottomSheetStateChange Invoked when the bottom sheet is collapsed or expanded.
|
||||
* @param debounceTimeoutMillis Function that returns the debounce timeout in milliseconds. This
|
||||
* make it possible to wait till [ContentState.isProductUrl] is stable before invoking
|
||||
* [onIconVisibilityChange].
|
||||
* @param onProductPageDetected Invoked when a product page is detected and loaded. Used to
|
||||
* detect when to send telemetry for shopping.product_page_visits.
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
class ReviewQualityCheckFeature(
|
||||
private val appStore: AppStore,
|
||||
private val browserStore: BrowserStore,
|
||||
private val shoppingExperienceFeature: ShoppingExperienceFeature,
|
||||
private val onIconVisibilityChange: (isAvailable: Boolean) -> Unit,
|
||||
private val onBottomSheetStateChange: (isExpanded: Boolean) -> Unit,
|
||||
private val debounceTimeoutMillis: (Boolean) -> Long = { if (it) DEBOUNCE_TIMEOUT_MILLIS else 0 },
|
||||
private val onProductPageDetected: () -> Unit,
|
||||
) : LifecycleAwareFeature {
|
||||
private var scope: CoroutineScope? = null
|
||||
private var appStoreScope: CoroutineScope? = null
|
||||
|
||||
override fun start() {
|
||||
scope = browserStore.flowScoped { flow ->
|
||||
flow.mapNotNull { it.selectedTab }
|
||||
.map { it.content.isProductUrl && !it.content.loading }
|
||||
.distinctUntilChanged()
|
||||
.debounce(debounceTimeoutMillis)
|
||||
.collect {
|
||||
if (it) {
|
||||
onProductPageDetected()
|
||||
}
|
||||
|
||||
onIconVisibilityChange(shoppingExperienceFeature.isEnabled && it)
|
||||
}
|
||||
}
|
||||
|
||||
appStoreScope = appStore.flowScoped { flow ->
|
||||
flow.mapNotNull { it.shoppingState.shoppingSheetExpanded }
|
||||
.distinctUntilChanged()
|
||||
.collect(onBottomSheetStateChange)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
scope?.cancel()
|
||||
appStoreScope?.cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.addCallback
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.navigation.fragment.NavHostFragment.Companion.findNavController
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import mozilla.components.support.ktx.android.content.isScreenReaderEnabled
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.lazyStore
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.shopping.di.ReviewQualityCheckMiddlewareProvider
|
||||
import org.mozilla.fenix.shopping.store.BottomSheetDismissSource
|
||||
import org.mozilla.fenix.shopping.store.BottomSheetViewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
|
||||
import org.mozilla.fenix.shopping.ui.ReviewQualityCheckBottomSheet
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* A bottom sheet fragment displaying Review Quality Check information.
|
||||
*/
|
||||
class ReviewQualityCheckFragment : BottomSheetDialogFragment() {
|
||||
|
||||
private var behavior: BottomSheetBehavior<View>? = null
|
||||
private val bottomSheetStateFeature =
|
||||
ViewBoundFeatureWrapper<ReviewQualityCheckBottomSheetStateFeature>()
|
||||
private val store by lazyStore { viewModelScope ->
|
||||
ReviewQualityCheckStore(
|
||||
middleware = ReviewQualityCheckMiddlewareProvider.provideMiddleware(
|
||||
settings = requireComponents.settings,
|
||||
browserStore = requireComponents.core.store,
|
||||
appStore = requireComponents.appStore,
|
||||
context = requireContext().applicationContext,
|
||||
scope = viewModelScope,
|
||||
),
|
||||
)
|
||||
}
|
||||
private var dismissSource: BottomSheetDismissSource =
|
||||
BottomSheetDismissSource.CLICK_OUTSIDE
|
||||
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
// The bottom sheet enters STATE_HIDDEN when onDismiss is called. Because this is not
|
||||
// the case when the user clicks outside, we should check for any other dismissal sources.
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN &&
|
||||
dismissSource == BottomSheetDismissSource.CLICK_OUTSIDE
|
||||
) {
|
||||
dismissSource = BottomSheetDismissSource.SLIDE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
super.onCreateDialog(savedInstanceState).apply {
|
||||
setOnShowListener {
|
||||
val bottomSheet =
|
||||
findViewById<View?>(com.google.android.material.R.id.design_bottom_sheet)
|
||||
bottomSheet?.setBackgroundResource(android.R.color.transparent)
|
||||
behavior = BottomSheetBehavior.from(bottomSheet).apply {
|
||||
addBottomSheetCallback(bottomSheetCallback)
|
||||
setPeekHeightToHalfScreenHeight()
|
||||
}
|
||||
}
|
||||
(this as? BottomSheetDialog)?.onBackPressedDispatcher?.addCallback(this@ReviewQualityCheckFragment) {
|
||||
dismissSource = BottomSheetDismissSource.BACK_PRESSED
|
||||
findNavController(this@ReviewQualityCheckFragment).navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
behavior?.setPeekHeightToHalfScreenHeight()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
FirefoxTheme {
|
||||
ReviewQualityCheckBottomSheet(
|
||||
store = store,
|
||||
onRequestDismiss = {
|
||||
dismissSource = it
|
||||
behavior?.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
},
|
||||
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
behavior?.removeBottomSheetCallback(bottomSheetCallback)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
bottomSheetStateFeature.set(
|
||||
feature = ReviewQualityCheckBottomSheetStateFeature(
|
||||
store,
|
||||
requireContext().isScreenReaderEnabled,
|
||||
) { bottomSheetState ->
|
||||
if (bottomSheetState == BottomSheetViewState.FULL_VIEW) {
|
||||
behavior?.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
store.dispatch(
|
||||
ReviewQualityCheckAction.BottomSheetDisplayed(bottomSheetState),
|
||||
)
|
||||
},
|
||||
owner = viewLifecycleOwner,
|
||||
view = view,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
requireComponents.appStore.dispatch(AppAction.ShoppingAction.ShoppingSheetStateUpdated(expanded = false))
|
||||
store.dispatch(ReviewQualityCheckAction.BottomSheetClosed(dismissSource))
|
||||
}
|
||||
|
||||
private fun BottomSheetBehavior<View>.setPeekHeightToHalfScreenHeight() {
|
||||
peekHeight = resources.displayMetrics.heightPixels / 2
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping
|
||||
|
||||
import org.mozilla.fenix.nimbus.FxNimbus
|
||||
|
||||
/**
|
||||
* An abstraction for shopping experience feature flag.
|
||||
*/
|
||||
interface ShoppingExperienceFeature {
|
||||
|
||||
/**
|
||||
* Returns true if the shopping experience feature is enabled.
|
||||
*/
|
||||
val isEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Returns true if product recommendations exposure nimbus flag is enabled.
|
||||
*/
|
||||
val isProductRecommendationsExposureEnabled: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of [ShoppingExperienceFeature].
|
||||
*/
|
||||
class DefaultShoppingExperienceFeature : ShoppingExperienceFeature {
|
||||
|
||||
override val isEnabled
|
||||
get() = FxNimbus.features.shoppingExperience.value().enabled
|
||||
|
||||
override val isProductRecommendationsExposureEnabled: Boolean
|
||||
get() = FxNimbus.features.shoppingExperience.value().productRecommendationsExposure
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.di
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature
|
||||
import org.mozilla.fenix.shopping.middleware.DefaultNetworkChecker
|
||||
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckPreferences
|
||||
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckService
|
||||
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckTelemetryService
|
||||
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckVendorsService
|
||||
import org.mozilla.fenix.shopping.middleware.GetReviewQualityCheckSumoUrl
|
||||
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckNavigationMiddleware
|
||||
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckNetworkMiddleware
|
||||
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckPreferencesMiddleware
|
||||
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckTelemetryMiddleware
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
/**
|
||||
* Provides middleware for review quality check store.
|
||||
*/
|
||||
object ReviewQualityCheckMiddlewareProvider {
|
||||
|
||||
/**
|
||||
* Provides middlewares for review quality check feature.
|
||||
*
|
||||
* @param settings The [Settings] instance to use.
|
||||
* @param browserStore The [BrowserStore] instance to access state.
|
||||
* @param appStore The [AppStore] instance to access state.
|
||||
* @param context The [Context] instance to use.
|
||||
* @param scope The [CoroutineScope] to use for launching coroutines.
|
||||
*/
|
||||
fun provideMiddleware(
|
||||
settings: Settings,
|
||||
browserStore: BrowserStore,
|
||||
appStore: AppStore,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
): List<ReviewQualityCheckMiddleware> =
|
||||
listOf(
|
||||
providePreferencesMiddleware(settings, browserStore, appStore, scope),
|
||||
provideNetworkMiddleware(browserStore, context, scope),
|
||||
provideNavigationMiddleware(TabsUseCases.SelectOrAddUseCase(browserStore), context),
|
||||
provideTelemetryMiddleware(browserStore, appStore, scope),
|
||||
)
|
||||
|
||||
private fun providePreferencesMiddleware(
|
||||
settings: Settings,
|
||||
browserStore: BrowserStore,
|
||||
appStore: AppStore,
|
||||
scope: CoroutineScope,
|
||||
) = ReviewQualityCheckPreferencesMiddleware(
|
||||
reviewQualityCheckPreferences = DefaultReviewQualityCheckPreferences(settings),
|
||||
reviewQualityCheckVendorsService = DefaultReviewQualityCheckVendorsService(browserStore),
|
||||
appStore = appStore,
|
||||
shoppingExperienceFeature = DefaultShoppingExperienceFeature(),
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
private fun provideNetworkMiddleware(
|
||||
browserStore: BrowserStore,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
) = ReviewQualityCheckNetworkMiddleware(
|
||||
reviewQualityCheckService = DefaultReviewQualityCheckService(browserStore),
|
||||
networkChecker = DefaultNetworkChecker(context),
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
private fun provideNavigationMiddleware(
|
||||
selectOrAddUseCase: TabsUseCases.SelectOrAddUseCase,
|
||||
context: Context,
|
||||
) = ReviewQualityCheckNavigationMiddleware(
|
||||
selectOrAddUseCase = selectOrAddUseCase,
|
||||
GetReviewQualityCheckSumoUrl(context),
|
||||
)
|
||||
|
||||
private fun provideTelemetryMiddleware(
|
||||
browserStore: BrowserStore,
|
||||
appStore: AppStore,
|
||||
scope: CoroutineScope,
|
||||
) =
|
||||
ReviewQualityCheckTelemetryMiddleware(
|
||||
telemetryService = DefaultReviewQualityCheckTelemetryService(browserStore),
|
||||
browserStore = browserStore,
|
||||
appStore = appStore,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
/**
|
||||
* Converts a string to an enum value, ignoring case and replacing spaces with underscores.
|
||||
* If the string does not match any of the enum values, the default value is returned.
|
||||
*/
|
||||
inline fun <reified T : Enum<T>> String.asEnumOrDefault(defaultValue: T? = null): T? =
|
||||
enumValues<T>().firstOrNull { it.name.equals(this.replace(" ", "_"), ignoreCase = true) } ?: defaultValue
|
||||
@@ -1,39 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
|
||||
private const val PARAM_UTM_CAMPAIGN_KEY = "utm_campaign"
|
||||
private const val PARAM_UTM_CAMPAIGN_VALUE = "fakespot-by-mozilla"
|
||||
private const val PARAM_UTM_TERM_KEY = "utm_term"
|
||||
private const val PARAM_UTM_TERM_VALUE = "core-sheet"
|
||||
|
||||
/**
|
||||
* Class used to retrieve the SUMO review quality check link.
|
||||
*
|
||||
* @param context Context used to localize the SUMO url.
|
||||
*/
|
||||
class GetReviewQualityCheckSumoUrl(
|
||||
private val context: Context,
|
||||
) {
|
||||
private val url by lazy {
|
||||
appendUTMParams(
|
||||
SupportUtils.getSumoURLForTopic(context, SupportUtils.SumoTopic.REVIEW_QUALITY_CHECK),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the review quality check SUMO url.
|
||||
*/
|
||||
operator fun invoke(): String = url
|
||||
|
||||
private fun appendUTMParams(url: String): String = Uri.parse(url).buildUpon()
|
||||
.appendQueryParameter(PARAM_UTM_CAMPAIGN_KEY, PARAM_UTM_CAMPAIGN_VALUE)
|
||||
.appendQueryParameter(PARAM_UTM_TERM_KEY, PARAM_UTM_TERM_VALUE)
|
||||
.build().toString()
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import androidx.core.content.getSystemService
|
||||
import org.mozilla.fenix.ext.isOnline
|
||||
|
||||
/**
|
||||
* Checks if the device is connected to the internet.
|
||||
*/
|
||||
interface NetworkChecker {
|
||||
|
||||
/**
|
||||
* @return true if the device is connected to the internet, false otherwise.
|
||||
*/
|
||||
fun isConnected(): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @see [NetworkChecker].
|
||||
*/
|
||||
class DefaultNetworkChecker(private val context: Context) : NetworkChecker {
|
||||
|
||||
private val connectivityManager by lazy { context.getSystemService<ConnectivityManager>() }
|
||||
|
||||
override fun isConnected(): Boolean {
|
||||
return connectivityManager?.isOnline() ?: false
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import mozilla.components.concept.engine.shopping.Highlight
|
||||
import mozilla.components.concept.engine.shopping.ProductAnalysis
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.HighlightType
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.HighlightsInfo
|
||||
|
||||
/**
|
||||
* Maps [ProductAnalysis] to [ProductReviewState].
|
||||
*/
|
||||
fun ProductAnalysis?.toProductReviewState(): ProductReviewState =
|
||||
this?.toProductReview() ?: ProductReviewState.Error.GenericError
|
||||
|
||||
private fun ProductAnalysis.toProductReview(): ProductReviewState =
|
||||
if (pageNotSupported) {
|
||||
ProductReviewState.Error.UnsupportedProductTypeError
|
||||
} else if (productId == null) {
|
||||
if (needsAnalysis) {
|
||||
ProductReviewState.NoAnalysisPresent()
|
||||
} else {
|
||||
ProductReviewState.Error.GenericError
|
||||
}
|
||||
} else if (deletedProductReported) {
|
||||
ProductReviewState.Error.ProductAlreadyReported
|
||||
} else if (deletedProduct) {
|
||||
ProductReviewState.Error.ProductNotAvailable
|
||||
} else if (notEnoughReviews && !needsAnalysis) {
|
||||
ProductReviewState.Error.NotEnoughReviews
|
||||
} else {
|
||||
val mappedRating = adjustedRating?.toFloat()
|
||||
val mappedGrade = grade?.asEnumOrDefault<ReviewQualityCheckState.Grade>()
|
||||
val mappedHighlights = highlights?.toHighlights()?.toSortedMap()
|
||||
|
||||
if (mappedGrade == null && mappedRating == null && mappedHighlights == null) {
|
||||
ProductReviewState.NoAnalysisPresent()
|
||||
} else {
|
||||
ProductReviewState.AnalysisPresent(
|
||||
productId = productId!!,
|
||||
reviewGrade = mappedGrade,
|
||||
analysisStatus = needsAnalysis.toAnalysisStatus(),
|
||||
adjustedRating = mappedRating,
|
||||
productUrl = analysisURL!!,
|
||||
highlightsInfo = mappedHighlights?.let { HighlightsInfo(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Boolean.toAnalysisStatus(): AnalysisStatus =
|
||||
when (this) {
|
||||
true -> AnalysisStatus.NeedsAnalysis
|
||||
false -> AnalysisStatus.UpToDate
|
||||
}
|
||||
|
||||
private fun Highlight.toHighlights(): Map<HighlightType, List<String>>? =
|
||||
HighlightType.entries
|
||||
.associateWith { highlightsForType(it) }
|
||||
.filterValues { it != null }
|
||||
.mapValues { it.value!! }
|
||||
.ifEmpty { null }
|
||||
|
||||
private fun Highlight.highlightsForType(highlightType: HighlightType) =
|
||||
when (highlightType) {
|
||||
HighlightType.QUALITY -> quality
|
||||
HighlightType.PRICE -> price
|
||||
HighlightType.SHIPPING -> shipping
|
||||
HighlightType.PACKAGING_AND_APPEARANCE -> appearance
|
||||
HighlightType.COMPETITIVENESS -> competitiveness
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import mozilla.components.concept.engine.shopping.ProductRecommendation
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.RecommendedProductState
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
private const val MINIMUM_FRACTION_DIGITS = 0
|
||||
private const val MAXIMUM_FRACTION_DIGITS = 2
|
||||
|
||||
/**
|
||||
* Maps [ProductRecommendation] to [RecommendedProductState].
|
||||
*/
|
||||
fun ProductRecommendation?.toRecommendedProductState(): RecommendedProductState =
|
||||
this?.toRecommendedProduct() ?: RecommendedProductState.Initial
|
||||
|
||||
private fun ProductRecommendation.toRecommendedProduct(): RecommendedProductState.Product =
|
||||
RecommendedProductState.Product(
|
||||
aid = aid,
|
||||
name = name,
|
||||
productUrl = url,
|
||||
imageUrl = imageUrl,
|
||||
formattedPrice = price.toDouble().toFormattedAmount(currency),
|
||||
reviewGrade = grade.asEnumOrDefault<ReviewQualityCheckState.Grade>()!!,
|
||||
adjustedRating = adjustedRating.toFloat(),
|
||||
isSponsored = sponsored,
|
||||
analysisUrl = analysisUrl,
|
||||
)
|
||||
|
||||
private fun Double.toFormattedAmount(currencyCode: String): String =
|
||||
mapCurrencyCodeToNumberFormat(currencyCode).apply {
|
||||
minimumFractionDigits = MINIMUM_FRACTION_DIGITS
|
||||
maximumFractionDigits = MAXIMUM_FRACTION_DIGITS
|
||||
}.format(this)
|
||||
|
||||
private fun mapCurrencyCodeToNumberFormat(currencyCode: String): NumberFormat =
|
||||
try {
|
||||
val currency = Currency.getInstance(currencyCode)
|
||||
NumberFormat.getCurrencyInstance(Locale.getDefault()).apply {
|
||||
this.currency = currency
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
NumberFormat.getNumberInstance()
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val MAX_RETRIES = 10
|
||||
private const val INITIAL_DELAY_MS = 2000L
|
||||
private const val MAX_DELAY_MS = 20000L
|
||||
private const val FACTOR = 2.0
|
||||
|
||||
/**
|
||||
* Retry a suspend function until it returns a value that satisfies the predicate.
|
||||
*
|
||||
* @param maxRetries The maximum number of retries.
|
||||
* @param initialDelayMs The initial delay in milliseconds.
|
||||
* @param maxDelayMs The maximum delay in milliseconds.
|
||||
* @param factor The factor to increase the delay by.
|
||||
* @param predicate The predicate to check the result against.
|
||||
* @param block The function to retry.
|
||||
*/
|
||||
suspend fun <T> retry(
|
||||
maxRetries: Int = MAX_RETRIES,
|
||||
initialDelayMs: Long = INITIAL_DELAY_MS,
|
||||
maxDelayMs: Long = MAX_DELAY_MS,
|
||||
factor: Double = FACTOR,
|
||||
predicate: (T) -> Boolean,
|
||||
block: suspend () -> T,
|
||||
): T {
|
||||
var delayTime = initialDelayMs
|
||||
var data: T = block()
|
||||
|
||||
repeat(maxRetries - 1) {
|
||||
if (predicate(data)) {
|
||||
delay(delayTime)
|
||||
data = block()
|
||||
delayTime = (delayTime * factor).toLong().coerceAtMost(maxDelayMs)
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
|
||||
private const val POWERED_BY_URL =
|
||||
"https://www.fakespot.com/review-checker?utm_source=review-checker" +
|
||||
"&utm_campaign=fakespot-by-mozilla&utm_medium=inproduct&utm_term=core-sheet"
|
||||
private const val PRIVACY_POLICY_URL = "https://www.mozilla.org/en-US/privacy/firefox/#review-checker" +
|
||||
"?utm_source=review-checker&utm_campaign=privacy-policy&utm_medium=in-product&utm_term=opt-in-screen"
|
||||
private const val TERMS_OF_USE_URL = "https://www.fakespot.com/terms"
|
||||
|
||||
/**
|
||||
* Middleware that handles navigation events for the review quality check feature.
|
||||
*
|
||||
* @param selectOrAddUseCase UseCase instance used to open new tabs.
|
||||
* @param getReviewQualityCheckSumoUrl Instance used to retrieve the learn more SUMO link.
|
||||
*/
|
||||
class ReviewQualityCheckNavigationMiddleware(
|
||||
private val selectOrAddUseCase: TabsUseCases.SelectOrAddUseCase,
|
||||
private val getReviewQualityCheckSumoUrl: GetReviewQualityCheckSumoUrl,
|
||||
) : ReviewQualityCheckMiddleware {
|
||||
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<ReviewQualityCheckState, ReviewQualityCheckAction>,
|
||||
next: (ReviewQualityCheckAction) -> Unit,
|
||||
action: ReviewQualityCheckAction,
|
||||
) {
|
||||
next(action)
|
||||
when (action) {
|
||||
is ReviewQualityCheckAction.NavigationMiddlewareAction -> processAction(action)
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processAction(
|
||||
action: ReviewQualityCheckAction.NavigationMiddlewareAction,
|
||||
) {
|
||||
selectOrAddUseCase.invoke(actionToUrl(action))
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to find the corresponding url to the open link action.
|
||||
*
|
||||
* @param action Used to find the corresponding url.
|
||||
*/
|
||||
private fun actionToUrl(
|
||||
action: ReviewQualityCheckAction.NavigationMiddlewareAction,
|
||||
) = when (action) {
|
||||
is ReviewQualityCheckAction.OpenExplainerLearnMoreLink,
|
||||
ReviewQualityCheckAction.OpenOnboardingLearnMoreLink,
|
||||
-> getReviewQualityCheckSumoUrl()
|
||||
|
||||
is ReviewQualityCheckAction.OpenOnboardingTermsLink -> TERMS_OF_USE_URL
|
||||
|
||||
is ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink -> PRIVACY_POLICY_URL
|
||||
|
||||
is ReviewQualityCheckAction.OpenPoweredByLink -> POWERED_BY_URL
|
||||
|
||||
is ReviewQualityCheckAction.RecommendedProductClick -> action.productUrl
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction.FetchProductAnalysis
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction.RetryProductAnalysis
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction.UpdateRecommendedProduct
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus
|
||||
|
||||
/**
|
||||
* Middleware that handles network requests for the review quality check feature.
|
||||
*
|
||||
* @param reviewQualityCheckService The service that handles the network requests.
|
||||
* @param networkChecker The [NetworkChecker] instance to check the network status.
|
||||
* @param scope The [CoroutineScope] that will be used to launch coroutines.
|
||||
*/
|
||||
class ReviewQualityCheckNetworkMiddleware(
|
||||
private val reviewQualityCheckService: ReviewQualityCheckService,
|
||||
private val networkChecker: NetworkChecker,
|
||||
private val scope: CoroutineScope,
|
||||
) : ReviewQualityCheckMiddleware {
|
||||
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<ReviewQualityCheckState, ReviewQualityCheckAction>,
|
||||
next: (ReviewQualityCheckAction) -> Unit,
|
||||
action: ReviewQualityCheckAction,
|
||||
) {
|
||||
next(action)
|
||||
when (action) {
|
||||
is ReviewQualityCheckAction.NetworkAction -> processAction(context.store, action)
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processAction(
|
||||
store: Store<ReviewQualityCheckState, ReviewQualityCheckAction>,
|
||||
action: ReviewQualityCheckAction.NetworkAction,
|
||||
) {
|
||||
if (!networkChecker.isConnected()) {
|
||||
store.updateProductReviewState(ProductReviewState.Error.NetworkError)
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
when (action) {
|
||||
FetchProductAnalysis, RetryProductAnalysis -> {
|
||||
store.onFetch()
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ReanalyzeProduct,
|
||||
ReviewQualityCheckAction.AnalyzeProduct,
|
||||
ReviewQualityCheckAction.RestoreReanalysis,
|
||||
-> {
|
||||
store.onReanalyze()
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ReportProductBackInStock -> {
|
||||
val status = reviewQualityCheckService.reportBackInStock()
|
||||
if (status == ReportBackInStockStatusDto.NOT_DELETED) {
|
||||
store.onFetch()
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ToggleProductRecommendation -> {
|
||||
val state = store.state
|
||||
if (state is ReviewQualityCheckState.OptedIn &&
|
||||
state.productReviewState is ProductReviewState.AnalysisPresent &&
|
||||
state.productRecommendationsPreference == true
|
||||
) {
|
||||
store.updateRecommendedProductState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.onFetch() {
|
||||
val productAnalysis = reviewQualityCheckService.fetchProductReview()
|
||||
val productReviewState = productAnalysis.toProductReviewState()
|
||||
|
||||
// Here the ProductReviewState should only updated after the analysis status API
|
||||
// returns a result. This makes sure that the UI doesn't show the reanalyse
|
||||
// button in case the product analysis is already in progress on the backend.
|
||||
if (productReviewState.isAnalysisPresentOrNoAnalysisPresent() &&
|
||||
reviewQualityCheckService.analysisStatus()?.status.isPendingOrInProgress()
|
||||
) {
|
||||
updateProductReviewState(productReviewState, true)
|
||||
dispatch(ReviewQualityCheckAction.RestoreReanalysis)
|
||||
} else {
|
||||
updateProductReviewState(productReviewState)
|
||||
}
|
||||
|
||||
if (productReviewState is ProductReviewState.AnalysisPresent) {
|
||||
updateRecommendedProductState()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.onReanalyze() {
|
||||
val reanalysis = reviewQualityCheckService.reanalyzeProduct()
|
||||
|
||||
if (reanalysis == null) {
|
||||
updateProductReviewState(ProductReviewState.Error.GenericError)
|
||||
return
|
||||
}
|
||||
|
||||
val statusProgress = pollForAnalysisStatus {
|
||||
dispatch(ReviewQualityCheckAction.UpdateAnalysisProgress(it))
|
||||
}
|
||||
|
||||
if (statusProgress == null ||
|
||||
statusProgress.status == AnalysisStatusDto.PENDING ||
|
||||
statusProgress.status == AnalysisStatusDto.IN_PROGRESS
|
||||
) {
|
||||
// poll failed, reset to previous state
|
||||
val state = this.state
|
||||
if (state is ReviewQualityCheckState.OptedIn) {
|
||||
if (state.productReviewState is ProductReviewState.NoAnalysisPresent) {
|
||||
updateProductReviewState(ProductReviewState.NoAnalysisPresent())
|
||||
} else if (state.productReviewState is ProductReviewState.AnalysisPresent) {
|
||||
updateProductReviewState(
|
||||
state.productReviewState.copy(analysisStatus = AnalysisStatus.NeedsAnalysis),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// poll succeeded, update state
|
||||
val productAnalysis = reviewQualityCheckService.fetchProductReview()
|
||||
val productReviewState = productAnalysis.toProductReviewState()
|
||||
updateProductReviewState(productReviewState)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pollForAnalysisStatus(
|
||||
onEachSuccessfulPoll: (progress: Double) -> Unit,
|
||||
): AnalysisStatusProgressDto? =
|
||||
retry(
|
||||
predicate = { it?.status.isPendingOrInProgress() },
|
||||
block = {
|
||||
reviewQualityCheckService.analysisStatus()?.also {
|
||||
onEachSuccessfulPoll(it.progress)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.updateProductReviewState(
|
||||
productReviewState: ProductReviewState,
|
||||
restoreAnalysis: Boolean = false,
|
||||
) {
|
||||
dispatch(ReviewQualityCheckAction.UpdateProductReview(productReviewState, restoreAnalysis))
|
||||
}
|
||||
|
||||
private fun ProductReviewState.isAnalysisPresentOrNoAnalysisPresent() =
|
||||
this is ProductReviewState.AnalysisPresent || this is ProductReviewState.NoAnalysisPresent
|
||||
|
||||
private suspend fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.updateRecommendedProductState() {
|
||||
val currentState = state
|
||||
if (currentState is ReviewQualityCheckState.OptedIn &&
|
||||
(currentState.productRecommendationsExposure || (currentState.productRecommendationsPreference == true))
|
||||
) {
|
||||
val productRecommendation = reviewQualityCheckService.productRecommendation(
|
||||
currentState.productRecommendationsPreference ?: false,
|
||||
)
|
||||
if (currentState.productRecommendationsPreference == true) {
|
||||
productRecommendation.toRecommendedProductState().also {
|
||||
dispatch(UpdateRecommendedProduct(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnalysisStatusDto?.isPendingOrInProgress(): Boolean =
|
||||
this == AnalysisStatusDto.PENDING || this == AnalysisStatusDto.IN_PROGRESS
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.mozilla.fenix.nimbus.FxNimbus
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
/**
|
||||
* Interface to get and set preferences for the review quality check feature.
|
||||
*/
|
||||
interface ReviewQualityCheckPreferences {
|
||||
/**
|
||||
* Returns true if the user has opted in to the review quality check feature.
|
||||
*/
|
||||
suspend fun enabled(): Boolean
|
||||
|
||||
/**
|
||||
* Returns true if the user has turned on product recommendations, false if turned off by the
|
||||
* user, null if the product recommendations feature is disabled.
|
||||
*/
|
||||
suspend fun productRecommendationsEnabled(): Boolean?
|
||||
|
||||
/**
|
||||
* Sets whether the user has opted in to the review quality check feature.
|
||||
*/
|
||||
suspend fun setEnabled(isEnabled: Boolean)
|
||||
|
||||
/**
|
||||
* Sets user preference to turn on/off product recommendations.
|
||||
*/
|
||||
suspend fun setProductRecommendationsEnabled(isEnabled: Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of [ReviewQualityCheckPreferences] that uses [Settings] to store/fetch
|
||||
* preferences.
|
||||
*
|
||||
* @param settings The [Settings] instance to use.
|
||||
*/
|
||||
class DefaultReviewQualityCheckPreferences(
|
||||
private val settings: Settings,
|
||||
) : ReviewQualityCheckPreferences {
|
||||
|
||||
override suspend fun enabled(): Boolean = withContext(Dispatchers.IO) {
|
||||
settings.isReviewQualityCheckEnabled
|
||||
}
|
||||
|
||||
override suspend fun productRecommendationsEnabled(): Boolean? = withContext(Dispatchers.IO) {
|
||||
if (FxNimbus.features.shoppingExperience.value().productRecommendations) {
|
||||
settings.isReviewQualityCheckProductRecommendationsEnabled
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setEnabled(isEnabled: Boolean) {
|
||||
settings.isReviewQualityCheckEnabled = isEnabled
|
||||
}
|
||||
|
||||
override suspend fun setProductRecommendationsEnabled(isEnabled: Boolean) {
|
||||
settings.isReviewQualityCheckProductRecommendationsEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction.HighlightsCardExpanded
|
||||
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction.InfoCardExpanded
|
||||
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction.SettingsCardExpanded
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.components.appstate.shopping.ShoppingState.CardState
|
||||
import org.mozilla.fenix.shopping.ShoppingExperienceFeature
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn
|
||||
|
||||
/**
|
||||
* Middleware for getting and setting review quality check user preferences.
|
||||
*
|
||||
* @param reviewQualityCheckPreferences The [ReviewQualityCheckPreferences] instance to get and
|
||||
* set preferences for the review quality check feature.
|
||||
* @param reviewQualityCheckVendorsService The [ReviewQualityCheckVendorsService] instance for
|
||||
* getting the list of product vendors.
|
||||
* @param appStore The [AppStore] instance for dispatching [ShoppingAction]s.
|
||||
* @param shoppingExperienceFeature The [ShoppingExperienceFeature] instance to get feature flags.
|
||||
* @param scope The [CoroutineScope] to use for launching coroutines.
|
||||
*/
|
||||
class ReviewQualityCheckPreferencesMiddleware(
|
||||
private val reviewQualityCheckPreferences: ReviewQualityCheckPreferences,
|
||||
private val reviewQualityCheckVendorsService: ReviewQualityCheckVendorsService,
|
||||
private val appStore: AppStore,
|
||||
private val shoppingExperienceFeature: ShoppingExperienceFeature,
|
||||
private val scope: CoroutineScope,
|
||||
) : ReviewQualityCheckMiddleware {
|
||||
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<ReviewQualityCheckState, ReviewQualityCheckAction>,
|
||||
next: (ReviewQualityCheckAction) -> Unit,
|
||||
action: ReviewQualityCheckAction,
|
||||
) {
|
||||
next(action)
|
||||
when (action) {
|
||||
is ReviewQualityCheckAction.PreferencesMiddlewareAction -> {
|
||||
processAction(context.store, action)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun processAction(
|
||||
store: Store<ReviewQualityCheckState, ReviewQualityCheckAction>,
|
||||
action: ReviewQualityCheckAction.PreferencesMiddlewareAction,
|
||||
) {
|
||||
when (action) {
|
||||
is ReviewQualityCheckAction.Init -> {
|
||||
scope.launch {
|
||||
val hasUserOptedIn = reviewQualityCheckPreferences.enabled()
|
||||
val isProductRecommendationsEnabled =
|
||||
reviewQualityCheckPreferences.productRecommendationsEnabled()
|
||||
|
||||
val updateUserPreferences = if (hasUserOptedIn) {
|
||||
val savedCardState =
|
||||
reviewQualityCheckVendorsService.selectedTabUrl()?.let {
|
||||
appStore.state.shoppingState.productCardState.getOrElse(it) { CardState() }
|
||||
} ?: CardState()
|
||||
|
||||
ReviewQualityCheckAction.OptInCompleted(
|
||||
isProductRecommendationsEnabled = isProductRecommendationsEnabled,
|
||||
productRecommendationsExposure =
|
||||
shoppingExperienceFeature.isProductRecommendationsExposureEnabled,
|
||||
productVendor = reviewQualityCheckVendorsService.productVendor(),
|
||||
isHighlightsExpanded = savedCardState.isHighlightsExpanded,
|
||||
isInfoExpanded = savedCardState.isInfoExpanded,
|
||||
isSettingsExpanded = savedCardState.isSettingsExpanded,
|
||||
)
|
||||
} else {
|
||||
val productVendors = reviewQualityCheckVendorsService.productVendors()
|
||||
ReviewQualityCheckAction.OptOutCompleted(productVendors)
|
||||
}
|
||||
store.dispatch(updateUserPreferences)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.OptIn -> {
|
||||
scope.launch {
|
||||
val isProductRecommendationsEnabled =
|
||||
reviewQualityCheckPreferences.productRecommendationsEnabled()
|
||||
store.dispatch(
|
||||
ReviewQualityCheckAction.OptInCompleted(
|
||||
isProductRecommendationsEnabled = isProductRecommendationsEnabled,
|
||||
productRecommendationsExposure =
|
||||
shoppingExperienceFeature.isProductRecommendationsExposureEnabled,
|
||||
productVendor = reviewQualityCheckVendorsService.productVendor(),
|
||||
isHighlightsExpanded = false,
|
||||
isInfoExpanded = false,
|
||||
isSettingsExpanded = false,
|
||||
),
|
||||
)
|
||||
|
||||
// Update the preference
|
||||
reviewQualityCheckPreferences.setEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.OptOut -> {
|
||||
scope.launch {
|
||||
// Update the preference
|
||||
reviewQualityCheckPreferences.setEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ToggleProductRecommendation -> {
|
||||
scope.launch {
|
||||
val productRecommendationsEnabled =
|
||||
reviewQualityCheckPreferences.productRecommendationsEnabled()
|
||||
if (productRecommendationsEnabled != null) {
|
||||
reviewQualityCheckPreferences.setProductRecommendationsEnabled(
|
||||
!productRecommendationsEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ExpandCollapseHighlights -> {
|
||||
appStore.dispatchShoppingAction(
|
||||
reviewQualityCheckState = store.state,
|
||||
action = { productPageUrl, optedIn ->
|
||||
HighlightsCardExpanded(
|
||||
productPageUrl = productPageUrl,
|
||||
expanded = optedIn.isHighlightsExpanded,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ExpandCollapseInfo -> {
|
||||
appStore.dispatchShoppingAction(
|
||||
reviewQualityCheckState = store.state,
|
||||
action = { productPageUrl, optedIn ->
|
||||
InfoCardExpanded(
|
||||
productPageUrl = productPageUrl,
|
||||
expanded = optedIn.isInfoExpanded,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ExpandCollapseSettings -> {
|
||||
appStore.dispatchShoppingAction(
|
||||
reviewQualityCheckState = store.state,
|
||||
action = { productPageUrl, optedIn ->
|
||||
SettingsCardExpanded(
|
||||
productPageUrl = productPageUrl,
|
||||
expanded = optedIn.isSettingsExpanded,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Store<AppState, AppAction>.dispatchShoppingAction(
|
||||
reviewQualityCheckState: ReviewQualityCheckState,
|
||||
action: (productPageUrl: String, optedIn: OptedIn) -> ShoppingAction,
|
||||
) {
|
||||
if (reviewQualityCheckState is OptedIn) {
|
||||
reviewQualityCheckVendorsService.selectedTabUrl()?.let {
|
||||
dispatch(action(it, reviewQualityCheckState))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.shopping.ProductAnalysis
|
||||
import mozilla.components.concept.engine.shopping.ProductRecommendation
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.mozilla.fenix.GleanMetrics.Shopping
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* Service that handles the network requests for the review quality check feature.
|
||||
*/
|
||||
interface ReviewQualityCheckService {
|
||||
|
||||
/**
|
||||
* Fetches the product review for the current tab.
|
||||
*
|
||||
* @return [ProductAnalysis] if the request succeeds, null otherwise.
|
||||
*/
|
||||
suspend fun fetchProductReview(): ProductAnalysis?
|
||||
|
||||
/**
|
||||
* Triggers a reanalysis of the product review for the current tab.
|
||||
*
|
||||
* @return [AnalysisStatusDto] if the request succeeds, null otherwise.
|
||||
*/
|
||||
suspend fun reanalyzeProduct(): AnalysisStatusDto?
|
||||
|
||||
/**
|
||||
* Fetches the status of the product review for the current tab.
|
||||
*
|
||||
* @return [AnalysisStatusProgressDto] if the request succeeds, null otherwise.
|
||||
*/
|
||||
suspend fun analysisStatus(): AnalysisStatusProgressDto?
|
||||
|
||||
/**
|
||||
* Fetches product recommendations related to the product user is browsing in the current tab.
|
||||
*
|
||||
* @return [ProductRecommendation] if request succeeds, null otherwise.
|
||||
*/
|
||||
suspend fun productRecommendation(shouldRecordAvailableTelemetry: Boolean): ProductRecommendation?
|
||||
|
||||
/**
|
||||
* Reports that a product is back in stock.
|
||||
*
|
||||
* @return [ReportBackInStockStatusDto] if the request succeeds, null otherwise.
|
||||
*/
|
||||
suspend fun reportBackInStock(): ReportBackInStockStatusDto?
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that handles the network requests for the review quality check feature.
|
||||
*
|
||||
* @param browserStore Reference to the application's [BrowserStore] to access state.
|
||||
*/
|
||||
class DefaultReviewQualityCheckService(
|
||||
private val browserStore: BrowserStore,
|
||||
) : ReviewQualityCheckService {
|
||||
|
||||
private val recommendationsCache: MutableMap<String, ProductRecommendation> = mutableMapOf()
|
||||
private val logger = Logger("DefaultReviewQualityCheckService")
|
||||
|
||||
override suspend fun fetchProductReview(): ProductAnalysis? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
browserStore.state.selectedTab?.let { tab ->
|
||||
tab.engineState.engineSession?.requestProductAnalysis(
|
||||
url = tab.content.url,
|
||||
onResult = {
|
||||
continuation.resume(it)
|
||||
},
|
||||
onException = {
|
||||
logger.error("Error fetching product review", it)
|
||||
continuation.resume(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reanalyzeProduct(): AnalysisStatusDto? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
browserStore.state.selectedTab?.let { tab ->
|
||||
tab.engineState.engineSession?.reanalyzeProduct(
|
||||
url = tab.content.url,
|
||||
onResult = {
|
||||
continuation.resume(it.asEnumOrDefault(AnalysisStatusDto.OTHER))
|
||||
},
|
||||
onException = {
|
||||
logger.error("Error starting reanalysis", it)
|
||||
continuation.resume(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun analysisStatus(): AnalysisStatusProgressDto? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
browserStore.state.selectedTab?.let { tab ->
|
||||
tab.engineState.engineSession?.requestAnalysisStatus(
|
||||
url = tab.content.url,
|
||||
onResult = {
|
||||
continuation.resume(
|
||||
AnalysisStatusProgressDto(
|
||||
status = it.status.asEnumOrDefault(AnalysisStatusDto.OTHER)!!,
|
||||
progress = it.progress,
|
||||
),
|
||||
)
|
||||
},
|
||||
onException = {
|
||||
logger.error("Error fetching analysis status", it)
|
||||
continuation.resume(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun productRecommendation(shouldRecordAvailableTelemetry: Boolean): ProductRecommendation? =
|
||||
withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
browserStore.state.selectedTab?.let { tab ->
|
||||
|
||||
if (recommendationsCache.containsKey(tab.content.url)) {
|
||||
continuation.resume(recommendationsCache[tab.content.url])
|
||||
} else {
|
||||
tab.engineState.engineSession?.requestProductRecommendations(
|
||||
url = tab.content.url,
|
||||
onResult = {
|
||||
if (it.isEmpty()) {
|
||||
if (shouldRecordAvailableTelemetry) {
|
||||
Shopping.surfaceNoAdsAvailable.record()
|
||||
}
|
||||
} else {
|
||||
Shopping.adsExposure.record()
|
||||
}
|
||||
// Return the first available recommendation since ui requires only
|
||||
// one recommendation.
|
||||
continuation.resume(
|
||||
it.firstOrNull()?.also { recommendation ->
|
||||
recommendationsCache[tab.content.url] = recommendation
|
||||
},
|
||||
)
|
||||
},
|
||||
onException = {
|
||||
logger.error("Error fetching product recommendation", it)
|
||||
continuation.resume(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reportBackInStock(): ReportBackInStockStatusDto? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
browserStore.state.selectedTab?.let { tab ->
|
||||
tab.engineState.engineSession?.reportBackInStock(
|
||||
url = tab.content.url,
|
||||
onResult = {
|
||||
continuation.resume(it.asEnumOrDefault<ReportBackInStockStatusDto>())
|
||||
},
|
||||
onException = {
|
||||
logger.error("Error reporting product back in stock", it)
|
||||
continuation.resume(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum that represents the status of the product review analysis.
|
||||
*/
|
||||
enum class AnalysisStatusDto {
|
||||
/**
|
||||
* Analysis is waiting to be picked up.
|
||||
*/
|
||||
PENDING,
|
||||
|
||||
/**
|
||||
* Analysis is in progress.
|
||||
*/
|
||||
IN_PROGRESS,
|
||||
|
||||
/**
|
||||
* Analysis is completed.
|
||||
*/
|
||||
COMPLETED,
|
||||
|
||||
/**
|
||||
* Any other status.
|
||||
*/
|
||||
OTHER,
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that represents the analysis status response of the product review analysis.
|
||||
*
|
||||
* @property status Enum indicating the current status of the analysis
|
||||
* @property progress Number indicating the progress of the analysis
|
||||
*/
|
||||
data class AnalysisStatusProgressDto(
|
||||
val status: AnalysisStatusDto,
|
||||
val progress: Double,
|
||||
)
|
||||
|
||||
/**
|
||||
* Enum that represents the status returned from reporting a product is back in stock.
|
||||
*/
|
||||
enum class ReportBackInStockStatusDto {
|
||||
/**
|
||||
* Report created.
|
||||
*/
|
||||
REPORT_CREATED,
|
||||
|
||||
/**
|
||||
* Product is already reported to be back in stock.
|
||||
*/
|
||||
ALREADY_REPORTED,
|
||||
|
||||
/**
|
||||
* Product was not actually marked as deleted.
|
||||
*/
|
||||
NOT_DELETED,
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.fenix.GleanMetrics.Shopping
|
||||
import org.mozilla.fenix.GleanMetrics.ShoppingSettings
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction
|
||||
import org.mozilla.fenix.components.appstate.shopping.ShoppingState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus
|
||||
|
||||
private const val ACTION_ENABLED = "enabled"
|
||||
private const val ACTION_DISABLED = "disabled"
|
||||
|
||||
/**
|
||||
* Middleware that captures telemetry events for the review quality check feature.
|
||||
*
|
||||
* @param telemetryService The service that handles telemetry events for review checker.
|
||||
* @param browserStore The [BrowserStore] instance to access the current tab.
|
||||
* @param appStore The [AppStore] instance to access [ShoppingState].
|
||||
* @param scope The [CoroutineScope] to use for launching coroutines.
|
||||
*/
|
||||
class ReviewQualityCheckTelemetryMiddleware(
|
||||
private val telemetryService: ReviewQualityCheckTelemetryService,
|
||||
private val browserStore: BrowserStore,
|
||||
private val appStore: AppStore,
|
||||
private val scope: CoroutineScope,
|
||||
) : ReviewQualityCheckMiddleware {
|
||||
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<ReviewQualityCheckState, ReviewQualityCheckAction>,
|
||||
next: (ReviewQualityCheckAction) -> Unit,
|
||||
action: ReviewQualityCheckAction,
|
||||
) {
|
||||
next(action)
|
||||
|
||||
when (action) {
|
||||
is ReviewQualityCheckAction.TelemetryAction -> processAction(context.store, action)
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun processAction(
|
||||
store: Store<ReviewQualityCheckState, ReviewQualityCheckAction>,
|
||||
action: ReviewQualityCheckAction.TelemetryAction,
|
||||
) {
|
||||
when (action) {
|
||||
is ReviewQualityCheckAction.OptIn -> {
|
||||
Shopping.surfaceOptInAccepted.record()
|
||||
ShoppingSettings.userHasOnboarded.set(true)
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.OptOut -> {
|
||||
ShoppingSettings.componentOptedOut.set(true)
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.BottomSheetClosed -> {
|
||||
Shopping.surfaceClosed.record(
|
||||
Shopping.SurfaceClosedExtra(action.source.sourceName),
|
||||
)
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.BottomSheetDisplayed -> {
|
||||
Shopping.surfaceDisplayed.record(
|
||||
Shopping.SurfaceDisplayedExtra(action.view.state),
|
||||
)
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.OpenExplainerLearnMoreLink -> {
|
||||
Shopping.surfaceReviewQualityExplainerUrlClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.OpenOnboardingLearnMoreLink -> {
|
||||
Shopping.surfaceLearnMoreClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink -> {
|
||||
Shopping.surfaceShowPrivacyPolicyClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.OpenOnboardingTermsLink -> {
|
||||
Shopping.surfaceShowTermsClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.NotNowClicked -> {
|
||||
Shopping.surfaceNotNowClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.ExpandCollapseHighlights -> {
|
||||
val state = store.state
|
||||
if (state is ReviewQualityCheckState.OptedIn && state.isHighlightsExpanded) {
|
||||
Shopping.surfaceShowMoreRecentReviewsClicked.record()
|
||||
}
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.ExpandCollapseSettings -> {
|
||||
val state = store.state
|
||||
if (state is ReviewQualityCheckState.OptedIn && state.isSettingsExpanded) {
|
||||
Shopping.surfaceExpandSettings.record()
|
||||
}
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.NoAnalysisDisplayed -> {
|
||||
Shopping.surfaceNoReviewReliabilityAvailable.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.UpdateProductReview -> {
|
||||
val state = store.state
|
||||
if (state.isStaleAnalysis() && !action.restoreAnalysis) {
|
||||
Shopping.surfaceStaleAnalysisShown.record()
|
||||
}
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.AnalyzeProduct -> {
|
||||
Shopping.surfaceAnalyzeReviewsNoneAvailableClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.ReanalyzeProduct -> {
|
||||
Shopping.surfaceReanalyzeClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.ReportProductBackInStock -> {
|
||||
Shopping.surfaceReactivatedButtonClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.OptOutCompleted -> {
|
||||
Shopping.surfaceOnboardingDisplayed.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.OpenPoweredByLink -> {
|
||||
Shopping.surfacePoweredByFakespotLinkClicked.record()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.RecommendedProductImpression -> {
|
||||
browserStore.state.selectedTab?.let { tabSessionState ->
|
||||
val key = ShoppingState.ProductRecommendationImpressionKey(
|
||||
tabId = tabSessionState.id,
|
||||
productUrl = tabSessionState.content.url,
|
||||
aid = action.productAid,
|
||||
)
|
||||
|
||||
val recordedImpressions =
|
||||
appStore.state.shoppingState.recordedProductRecommendationImpressions
|
||||
|
||||
if (!recordedImpressions.contains(key)) {
|
||||
Shopping.surfaceAdsImpression.record()
|
||||
scope.launch {
|
||||
val result =
|
||||
telemetryService.recordRecommendedProductImpression(action.productAid)
|
||||
if (result != null) {
|
||||
appStore.dispatch(ShoppingAction.ProductRecommendationImpression(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.RecommendedProductClick -> {
|
||||
Shopping.surfaceAdsClicked.record()
|
||||
scope.launch {
|
||||
telemetryService.recordRecommendedProductClick(action.productAid)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ToggleProductRecommendation -> {
|
||||
val state = store.state
|
||||
if (state is ReviewQualityCheckState.OptedIn &&
|
||||
state.productRecommendationsPreference != null
|
||||
) {
|
||||
val toggleAction = if (state.productRecommendationsPreference) {
|
||||
ACTION_ENABLED
|
||||
} else {
|
||||
ACTION_DISABLED
|
||||
}
|
||||
Shopping.surfaceAdsSettingToggled.record(
|
||||
Shopping.SurfaceAdsSettingToggledExtra(
|
||||
action = toggleAction,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ReviewQualityCheckState.isStaleAnalysis(): Boolean =
|
||||
this is ReviewQualityCheckState.OptedIn &&
|
||||
this.productReviewState is ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent &&
|
||||
this.productReviewState.analysisStatus == AnalysisStatus.NeedsAnalysis
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* Service that handles telemetry events for review checker.
|
||||
*/
|
||||
interface ReviewQualityCheckTelemetryService {
|
||||
|
||||
/**
|
||||
* Sends a click attribution event for a given product aid.
|
||||
*/
|
||||
suspend fun recordRecommendedProductClick(productAid: String): Unit?
|
||||
|
||||
/**
|
||||
* Sends an impression attribution event for a given product aid.
|
||||
*/
|
||||
suspend fun recordRecommendedProductImpression(productAid: String): Unit?
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that handles the network requests for the review quality check feature.
|
||||
*
|
||||
* @param browserStore Reference to the application's [BrowserStore] to access state.
|
||||
*/
|
||||
class DefaultReviewQualityCheckTelemetryService(
|
||||
private val browserStore: BrowserStore,
|
||||
) : ReviewQualityCheckTelemetryService {
|
||||
|
||||
private val logger = Logger(TAG)
|
||||
|
||||
override suspend fun recordRecommendedProductClick(productAid: String) =
|
||||
withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
browserStore.state.selectedTab?.engineState?.engineSession?.sendClickAttributionEvent(
|
||||
aid = productAid,
|
||||
onResult = {
|
||||
continuation.resume(Unit)
|
||||
},
|
||||
onException = {
|
||||
logger.error("Error sending click attribution event", it)
|
||||
continuation.resume(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun recordRecommendedProductImpression(productAid: String) =
|
||||
withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
browserStore.state.selectedTab?.engineState?.engineSession?.sendImpressionAttributionEvent(
|
||||
aid = productAid,
|
||||
onResult = {
|
||||
continuation.resume(Unit)
|
||||
},
|
||||
onException = {
|
||||
logger.error("Error sending impression attribution event", it)
|
||||
continuation.resume(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ReviewQualityCheckTelemetryService"
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
||||
private const val AMAZON_COM = "amazon.com"
|
||||
private const val BEST_BUY_COM = "bestbuy.com"
|
||||
private const val WALMART_COM = "walmart.com"
|
||||
private const val AMAZON_DE = "amazon.de"
|
||||
private const val AMAZON_FR = "amazon.fr"
|
||||
private val defaultVendorsList = enumValues<ProductVendor>().toList()
|
||||
|
||||
/**
|
||||
* Service for getting the list of product vendors.
|
||||
*/
|
||||
interface ReviewQualityCheckVendorsService {
|
||||
|
||||
/**
|
||||
* Returns the selected tab url.
|
||||
*/
|
||||
fun selectedTabUrl(): String?
|
||||
|
||||
/**
|
||||
* Returns the list of product vendors in order.
|
||||
*/
|
||||
fun productVendors(): List<ProductVendor>
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of [ReviewQualityCheckVendorsService] that uses the [BrowserStore] to
|
||||
* identify the selected tab.
|
||||
*
|
||||
* @param browserStore The [BrowserStore] instance to use.
|
||||
*/
|
||||
class DefaultReviewQualityCheckVendorsService(
|
||||
private val browserStore: BrowserStore,
|
||||
) : ReviewQualityCheckVendorsService {
|
||||
|
||||
override fun selectedTabUrl(): String? =
|
||||
browserStore.state.selectedTab?.content?.url
|
||||
|
||||
override fun productVendors(): List<ProductVendor> {
|
||||
val selectedTabUrl = selectedTabUrl()
|
||||
|
||||
return if (selectedTabUrl == null) {
|
||||
defaultVendorsList
|
||||
} else {
|
||||
val host = selectedTabUrl.toJavaUri()?.host
|
||||
when {
|
||||
host == null -> defaultVendorsList
|
||||
host.contains(AMAZON_COM) -> createProductVendorsList(ProductVendor.AMAZON)
|
||||
host.contains(BEST_BUY_COM) -> createProductVendorsList(ProductVendor.BEST_BUY)
|
||||
host.contains(WALMART_COM) -> createProductVendorsList(ProductVendor.WALMART)
|
||||
host.contains(AMAZON_DE) || host.contains(AMAZON_FR) -> listOf(ProductVendor.AMAZON)
|
||||
else -> defaultVendorsList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates list of product vendors using the firstVendor param as the first item in the list.
|
||||
*/
|
||||
private fun createProductVendorsList(firstVendor: ProductVendor): List<ProductVendor> =
|
||||
listOf(firstVendor) + defaultVendorsList.filterNot { it == firstVendor }
|
||||
|
||||
/**
|
||||
* Convenience function to converts a given string to a [URI] instance. Returns null if the
|
||||
* string is not a valid URI.
|
||||
*/
|
||||
private fun String.toJavaUri(): URI? {
|
||||
return try {
|
||||
URI.create(this)
|
||||
} catch (e: URISyntaxException) {
|
||||
Logger.error("Unable to create URI with the given string $this", e)
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.error("Unable to create URI with the given string $this", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first matching product vendor for the selected tab.
|
||||
*/
|
||||
fun ReviewQualityCheckVendorsService.productVendor(): ProductVendor =
|
||||
productVendors().first()
|
||||
@@ -1,30 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.store
|
||||
|
||||
/**
|
||||
* States the review quality check bottom sheet can be opened in.
|
||||
*
|
||||
* @property state Name of the state to be used in review quality check telemetry.
|
||||
*/
|
||||
enum class BottomSheetViewState(val state: String) {
|
||||
FULL_VIEW("full"),
|
||||
HALF_VIEW("half"),
|
||||
}
|
||||
|
||||
/**
|
||||
* The source of the bottom sheet dismiss.
|
||||
*
|
||||
* @property sourceName Name of the dismiss source to be used in review quality check telemetry.
|
||||
*/
|
||||
enum class BottomSheetDismissSource(val sourceName: String) {
|
||||
BACK_PRESSED("back_pressed"),
|
||||
SLIDE("sheet_slide"),
|
||||
HANDLE_CLICKED("handle_clicked"),
|
||||
CLICK_OUTSIDE("click_outside"),
|
||||
LINK_OPENED("link_opened"),
|
||||
OPT_OUT("opt_out"),
|
||||
NOT_NOW("not_now"),
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.store
|
||||
|
||||
import mozilla.components.lib.state.Action
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.RecommendedProductState
|
||||
|
||||
/**
|
||||
* Actions for review quality check feature.
|
||||
*/
|
||||
sealed interface ReviewQualityCheckAction : Action {
|
||||
|
||||
/**
|
||||
* Actions that cause updates to state.
|
||||
*/
|
||||
sealed interface UpdateAction : ReviewQualityCheckAction
|
||||
|
||||
/**
|
||||
* Actions related to preferences.
|
||||
*/
|
||||
sealed interface PreferencesMiddlewareAction : ReviewQualityCheckAction
|
||||
|
||||
/**
|
||||
* Actions related to navigation events.
|
||||
*/
|
||||
sealed interface NavigationMiddlewareAction : ReviewQualityCheckAction
|
||||
|
||||
/**
|
||||
* Actions related to network requests.
|
||||
*/
|
||||
sealed interface NetworkAction : ReviewQualityCheckAction
|
||||
|
||||
/**
|
||||
* Actions related to telemetry events.
|
||||
*/
|
||||
sealed interface TelemetryAction : ReviewQualityCheckAction
|
||||
|
||||
/**
|
||||
* Triggered when the store is initialized.
|
||||
*/
|
||||
object Init : PreferencesMiddlewareAction
|
||||
|
||||
/**
|
||||
* Triggered when the user has opted in to the review quality check feature.
|
||||
*/
|
||||
object OptIn : PreferencesMiddlewareAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user has opted out of the review quality check feature.
|
||||
*/
|
||||
object OptOut : PreferencesMiddlewareAction, UpdateAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user has enabled or disabled product recommendations.
|
||||
*/
|
||||
object ToggleProductRecommendation : PreferencesMiddlewareAction, UpdateAction, NetworkAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered as a result of a [OptIn] or [Init] whe user has opted in for shopping experience.
|
||||
*
|
||||
* @property isProductRecommendationsEnabled Reflects the user preference update to display
|
||||
* recommended product. Null when product recommendations feature is disabled.
|
||||
* @property productRecommendationsExposure Whether product recommendations exposure is enabled.
|
||||
* @property productVendor The vendor of the product.
|
||||
* @property isHighlightsExpanded Whether the highlights card should be expanded.
|
||||
* @property isInfoExpanded Whether the info card should be expanded.
|
||||
* @property isSettingsExpanded Whether the settings card should be expanded.
|
||||
*/
|
||||
data class OptInCompleted(
|
||||
val isProductRecommendationsEnabled: Boolean?,
|
||||
val productRecommendationsExposure: Boolean,
|
||||
val productVendor: ReviewQualityCheckState.ProductVendor,
|
||||
val isHighlightsExpanded: Boolean,
|
||||
val isInfoExpanded: Boolean,
|
||||
val isSettingsExpanded: Boolean,
|
||||
) : UpdateAction
|
||||
|
||||
/**
|
||||
* Triggered as a result of [Init] when user has opted out of shopping experience.
|
||||
*
|
||||
* @property productVendors List of product vendors in relevant order.
|
||||
*/
|
||||
data class OptOutCompleted(
|
||||
val productVendors: List<ReviewQualityCheckState.ProductVendor>,
|
||||
) : UpdateAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered as a result of a [NetworkAction] to update the [ProductReviewState].
|
||||
*
|
||||
* @property productReviewState The product review state to update.
|
||||
* @property restoreAnalysis Signals whether the analysis will be restored right after the update.
|
||||
*/
|
||||
data class UpdateProductReview(
|
||||
val productReviewState: ProductReviewState,
|
||||
val restoreAnalysis: Boolean,
|
||||
) : UpdateAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered as a result of a [NetworkAction] to update the [RecommendedProductState].
|
||||
*/
|
||||
data class UpdateRecommendedProduct(
|
||||
val recommendedProductState: RecommendedProductState,
|
||||
) : UpdateAction
|
||||
|
||||
/**
|
||||
* Triggered when the user has opted in to the review quality check feature and the UI is opened.
|
||||
*/
|
||||
object FetchProductAnalysis : NetworkAction, UpdateAction
|
||||
|
||||
/**
|
||||
* Triggered when the user retries to fetch product analysis after a failure.
|
||||
*/
|
||||
object RetryProductAnalysis : NetworkAction, UpdateAction
|
||||
|
||||
/**
|
||||
* Triggered when the user triggers product re-analysis.
|
||||
*/
|
||||
object ReanalyzeProduct : NetworkAction, UpdateAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the product was previously known to be in reanalysis
|
||||
* process when the sheet was closed and the state should be restored.
|
||||
*/
|
||||
object RestoreReanalysis : NetworkAction, UpdateAction
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on the analyze button
|
||||
*/
|
||||
object AnalyzeProduct : NetworkAction, UpdateAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the analysis status is updated.
|
||||
*
|
||||
* @property progress The progress of the analysis ranging from 0.0-100.0.
|
||||
*/
|
||||
data class UpdateAnalysisProgress(val progress: Double) : UpdateAction
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on the recommended product.
|
||||
*
|
||||
* @property productAid The product's aid.
|
||||
* @property productUrl The product's link to open.
|
||||
*/
|
||||
data class RecommendedProductClick(
|
||||
val productAid: String,
|
||||
val productUrl: String,
|
||||
) : NavigationMiddlewareAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user views the recommended product.
|
||||
*
|
||||
* @property productAid The product's aid.
|
||||
*/
|
||||
data class RecommendedProductImpression(
|
||||
val productAid: String,
|
||||
) : TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on learn more link on the explainer card.
|
||||
*/
|
||||
object OpenExplainerLearnMoreLink : NavigationMiddlewareAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on the "Powered by" link in the footer.
|
||||
*/
|
||||
object OpenPoweredByLink : NavigationMiddlewareAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on learn more link on the opt in card.
|
||||
*/
|
||||
object OpenOnboardingLearnMoreLink : NavigationMiddlewareAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on terms and conditions link on the opt in card.
|
||||
*/
|
||||
object OpenOnboardingTermsLink : NavigationMiddlewareAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on privacy policy link on the opt in card.
|
||||
*/
|
||||
object OpenOnboardingPrivacyPolicyLink : NavigationMiddlewareAction, TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the bottom sheet is closed.
|
||||
*
|
||||
* @property source The source of dismissal.
|
||||
*/
|
||||
data class BottomSheetClosed(val source: BottomSheetDismissSource) : TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the bottom sheet is opened.
|
||||
*
|
||||
* @property view The state of the bottom sheet when opened.
|
||||
*/
|
||||
data class BottomSheetDisplayed(val view: BottomSheetViewState) : TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on the "Not now" button from the contextual onboarding card.
|
||||
*/
|
||||
object NotNowClicked : TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user expands the recent reviews card.
|
||||
*/
|
||||
object ExpandCollapseHighlights : TelemetryAction, UpdateAction, PreferencesMiddlewareAction
|
||||
|
||||
/**
|
||||
* Triggered when the user expands or collapses the settings card.
|
||||
*/
|
||||
object ExpandCollapseSettings : TelemetryAction, UpdateAction, PreferencesMiddlewareAction
|
||||
|
||||
/**
|
||||
* Triggered when the user expands or collapses the info card.
|
||||
*/
|
||||
object ExpandCollapseInfo : UpdateAction, PreferencesMiddlewareAction
|
||||
|
||||
/**
|
||||
* Triggered when the No analysis card is displayed to the user.
|
||||
*/
|
||||
object NoAnalysisDisplayed : TelemetryAction
|
||||
|
||||
/**
|
||||
* Triggered when the user reports a product is back in stock.
|
||||
*/
|
||||
object ReportProductBackInStock : NetworkAction, UpdateAction, TelemetryAction
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.store
|
||||
|
||||
import mozilla.components.lib.state.Middleware
|
||||
|
||||
/**
|
||||
* Middleware typealias for review quality check.
|
||||
*/
|
||||
typealias ReviewQualityCheckMiddleware = Middleware<ReviewQualityCheckState, ReviewQualityCheckAction>
|
||||
@@ -1,287 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.store
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import mozilla.components.lib.state.State
|
||||
import java.text.NumberFormat
|
||||
|
||||
private const val NUMBER_OF_HIGHLIGHTS_FOR_COMPACT_MODE = 2
|
||||
|
||||
/**
|
||||
* UI state of the review quality check feature.
|
||||
*/
|
||||
sealed interface ReviewQualityCheckState : State {
|
||||
/**
|
||||
* The initial state of the feature, it's also the default state set in the store.
|
||||
*/
|
||||
object Initial : ReviewQualityCheckState
|
||||
|
||||
/**
|
||||
* The state when the user has not opted in for the feature.
|
||||
*
|
||||
* @property productVendors List of vendors to be displayed in order in the onboarding UI.
|
||||
*/
|
||||
data class NotOptedIn(
|
||||
val productVendors: List<ProductVendor> = enumValues<ProductVendor>().toList(),
|
||||
) : ReviewQualityCheckState
|
||||
|
||||
/**
|
||||
* Supported product retailers.
|
||||
*/
|
||||
enum class ProductVendor {
|
||||
AMAZON, BEST_BUY, WALMART,
|
||||
}
|
||||
|
||||
/**
|
||||
* The state when the user has opted in for the feature.
|
||||
*
|
||||
* @property productReviewState The state of the product the user is browsing.
|
||||
* @property productRecommendationsPreference User preference whether to show product
|
||||
* recommendations. True if product recommendations should be shown. Null indicates that product
|
||||
* recommendations are disabled.
|
||||
* @property productRecommendationsExposure Whether product recommendations exposure is enabled.
|
||||
* @property productVendor The vendor of the product.
|
||||
* @property isSettingsExpanded Whether or not the settings card is expanded.
|
||||
* @property isInfoExpanded Whether or not the info card is expanded.
|
||||
* @property isHighlightsExpanded Whether or not the highlights card is expanded.
|
||||
*/
|
||||
data class OptedIn(
|
||||
val productReviewState: ProductReviewState = ProductReviewState.Loading,
|
||||
val productRecommendationsPreference: Boolean?,
|
||||
val productRecommendationsExposure: Boolean,
|
||||
val productVendor: ProductVendor,
|
||||
val isSettingsExpanded: Boolean = false,
|
||||
val isInfoExpanded: Boolean = false,
|
||||
val isHighlightsExpanded: Boolean = false,
|
||||
) : ReviewQualityCheckState {
|
||||
|
||||
/**
|
||||
* The state of the product the user is browsing.
|
||||
*/
|
||||
sealed interface ProductReviewState {
|
||||
/**
|
||||
* Denotes content is loading.
|
||||
*/
|
||||
object Loading : ProductReviewState
|
||||
|
||||
/**
|
||||
* Denotes an error has occurred.
|
||||
*/
|
||||
sealed interface Error : ProductReviewState {
|
||||
/**
|
||||
* Denotes a network error has occurred.
|
||||
*/
|
||||
object NetworkError : Error
|
||||
|
||||
/**
|
||||
* Denotes a product is not supported.
|
||||
*/
|
||||
object UnsupportedProductTypeError : Error
|
||||
|
||||
/**
|
||||
* Denotes a product does not have enough reviews to be analyzed.
|
||||
*/
|
||||
object NotEnoughReviews : Error
|
||||
|
||||
/**
|
||||
* Denotes a generic error has occurred.
|
||||
*/
|
||||
object GenericError : Error
|
||||
|
||||
/**
|
||||
* Denotes a product is not available.
|
||||
*/
|
||||
object ProductNotAvailable : Error
|
||||
|
||||
/**
|
||||
* Denotes current user reported a product is back in stock.
|
||||
*/
|
||||
object ThanksForReporting : Error
|
||||
|
||||
/**
|
||||
* Denotes another user has already reported the product is back in stock.
|
||||
*/
|
||||
object ProductAlreadyReported : Error
|
||||
}
|
||||
|
||||
/**
|
||||
* Denotes no analysis is present for the product the user is browsing.
|
||||
*
|
||||
* @property progress The [Progress] of the analysis, ranges from 0-100.
|
||||
* Default value is -1, which means analysis is not in progress.
|
||||
*/
|
||||
data class NoAnalysisPresent(
|
||||
val progress: Progress = Progress(-1f),
|
||||
) : ProductReviewState {
|
||||
|
||||
/**
|
||||
* Whether or not the progress bar is visible.
|
||||
*/
|
||||
val isProgressBarVisible: Boolean = progress.value != -1f
|
||||
}
|
||||
|
||||
/**
|
||||
* Denotes the state where analysis of the product is fetched and present.
|
||||
*
|
||||
* @property productId The id of the product, e.g ASIN, SKU.
|
||||
* @property reviewGrade The review grade of the product.
|
||||
* @property analysisStatus The status of the product analysis.
|
||||
* @property adjustedRating The adjusted rating taking review quality into consideration.
|
||||
* @property productUrl The url of the product the user is browsing.
|
||||
* @property highlightsInfo Optional highlights based on recent reviews of the product.
|
||||
* @property recommendedProductState The state of the recommended product.
|
||||
*/
|
||||
data class AnalysisPresent(
|
||||
val productId: String,
|
||||
val reviewGrade: Grade?,
|
||||
val analysisStatus: AnalysisStatus,
|
||||
val adjustedRating: Float?,
|
||||
val productUrl: String,
|
||||
val highlightsInfo: HighlightsInfo?,
|
||||
val recommendedProductState: RecommendedProductState = RecommendedProductState.Initial,
|
||||
) : ProductReviewState {
|
||||
init {
|
||||
require(!(highlightsInfo == null && reviewGrade == null && adjustedRating == null)) {
|
||||
"AnalysisPresent state should only be created when at least one of " +
|
||||
"reviewGrade, adjustedRating or highlights is not null"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for highlights and it's derived properties
|
||||
*
|
||||
* @property highlights highlights based on recent reviews of the product.
|
||||
*/
|
||||
@Immutable
|
||||
data class HighlightsInfo(
|
||||
val highlights: Map<HighlightType, List<String>>,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Highlights to display in compact mode that contains first 2 highlights of the
|
||||
* first highlight type.
|
||||
*/
|
||||
val highlightsForCompactMode: Map<HighlightType, List<String>> =
|
||||
highlights.entries.first().let { entry ->
|
||||
mapOf(
|
||||
entry.key to entry.value.take(NUMBER_OF_HIGHLIGHTS_FOR_COMPACT_MODE),
|
||||
)
|
||||
}
|
||||
|
||||
val showMoreButtonVisible: Boolean = highlights != highlightsForCompactMode
|
||||
|
||||
val highlightsFadeVisible: Boolean =
|
||||
showMoreButtonVisible && highlightsForCompactMode.entries.first().value.size > 1
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the product analysis.
|
||||
*/
|
||||
sealed interface AnalysisStatus {
|
||||
/**
|
||||
* Denotes reanalysis is in progress.
|
||||
*
|
||||
* @property progress The [Progress] of the analysis, ranges from 0-100.
|
||||
*/
|
||||
data class Reanalyzing(val progress: Progress) : AnalysisStatus
|
||||
|
||||
/**
|
||||
* Denotes a product needs analysis.
|
||||
*/
|
||||
object NeedsAnalysis : AnalysisStatus
|
||||
|
||||
/**
|
||||
* Denotes a product analysis is up to date.
|
||||
*/
|
||||
object UpToDate : AnalysisStatus
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress of the analysis, ranges from 0-100.
|
||||
*
|
||||
* @property value The value of the progress.
|
||||
*/
|
||||
data class Progress(val value: Float) {
|
||||
/**
|
||||
* Normalized progress, ranges from 0-1.
|
||||
*/
|
||||
val normalizedProgress: Float = value / 100f
|
||||
|
||||
/**
|
||||
* Percentage formatted progress ranging from 0-100%.
|
||||
*/
|
||||
val formattedProgress: String = FORMATTER.format(normalizedProgress)
|
||||
|
||||
companion object {
|
||||
private val FORMATTER = NumberFormat.getPercentInstance().apply {
|
||||
maximumFractionDigits = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Review Grade of the product - A being the best and F being the worst. There is no grade E.
|
||||
*/
|
||||
enum class Grade {
|
||||
A, B, C, D, F
|
||||
}
|
||||
|
||||
/**
|
||||
* Factors for which highlights are available based on recent reviews of the product.
|
||||
*/
|
||||
enum class HighlightType {
|
||||
QUALITY, PRICE, SHIPPING, PACKAGING_AND_APPEARANCE, COMPETITIVENESS
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the recommended product.
|
||||
*/
|
||||
sealed interface RecommendedProductState {
|
||||
/**
|
||||
* The initial state of the recommended product.
|
||||
*/
|
||||
object Initial : RecommendedProductState
|
||||
|
||||
/**
|
||||
* The state when the recommended product is available.
|
||||
*
|
||||
* @property aid The unique identifier of the product.
|
||||
* @property name The name of the product.
|
||||
* @property productUrl The url of the product.
|
||||
* @property imageUrl The url of the image of the product.
|
||||
* @property formattedPrice The formatted price of the product.
|
||||
* @property reviewGrade The review grade of the product.
|
||||
* @property adjustedRating The adjusted rating of the product.
|
||||
* @property isSponsored True if the product is sponsored.
|
||||
* @property analysisUrl The url of the analysis of the product.
|
||||
*/
|
||||
data class Product(
|
||||
val aid: String,
|
||||
val name: String,
|
||||
val productUrl: String,
|
||||
val imageUrl: String,
|
||||
val formattedPrice: String,
|
||||
val reviewGrade: Grade,
|
||||
val adjustedRating: Float,
|
||||
val isSponsored: Boolean,
|
||||
val analysisUrl: String,
|
||||
) : RecommendedProductState
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [ReviewQualityCheckState] applying the given [transform] function if the current
|
||||
* state is [OptedIn].
|
||||
*/
|
||||
fun mapIfOptedIn(transform: (OptedIn) -> ReviewQualityCheckState): ReviewQualityCheckState =
|
||||
if (this is OptedIn) {
|
||||
transform(this)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.store
|
||||
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus
|
||||
|
||||
/**
|
||||
* Store for review quality check feature.
|
||||
*
|
||||
* @param initialState The initial state of the store.
|
||||
* @param middleware The list of middlewares to use.
|
||||
*/
|
||||
class ReviewQualityCheckStore(
|
||||
initialState: ReviewQualityCheckState = ReviewQualityCheckState.Initial,
|
||||
middleware: List<ReviewQualityCheckMiddleware>,
|
||||
) : Store<ReviewQualityCheckState, ReviewQualityCheckAction>(
|
||||
initialState = initialState,
|
||||
middleware = middleware,
|
||||
reducer = ::reducer,
|
||||
) {
|
||||
init {
|
||||
dispatch(ReviewQualityCheckAction.Init)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reducer(
|
||||
state: ReviewQualityCheckState,
|
||||
action: ReviewQualityCheckAction,
|
||||
): ReviewQualityCheckState {
|
||||
if (action is ReviewQualityCheckAction.UpdateAction) {
|
||||
return mapStateForUpdateAction(state, action)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun mapStateForUpdateAction(
|
||||
state: ReviewQualityCheckState,
|
||||
action: ReviewQualityCheckAction.UpdateAction,
|
||||
): ReviewQualityCheckState {
|
||||
return when (action) {
|
||||
is ReviewQualityCheckAction.OptInCompleted -> {
|
||||
if (state is ReviewQualityCheckState.OptedIn) {
|
||||
state.copy(
|
||||
productRecommendationsPreference = action.isProductRecommendationsEnabled,
|
||||
productRecommendationsExposure = action.productRecommendationsExposure,
|
||||
isHighlightsExpanded = action.isHighlightsExpanded,
|
||||
isInfoExpanded = action.isInfoExpanded,
|
||||
isSettingsExpanded = action.isSettingsExpanded,
|
||||
)
|
||||
} else {
|
||||
ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = action.isProductRecommendationsEnabled,
|
||||
productRecommendationsExposure = action.productRecommendationsExposure,
|
||||
productVendor = action.productVendor,
|
||||
isHighlightsExpanded = action.isHighlightsExpanded,
|
||||
isInfoExpanded = action.isInfoExpanded,
|
||||
isSettingsExpanded = action.isSettingsExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.OptOutCompleted -> {
|
||||
ReviewQualityCheckState.NotOptedIn(action.productVendors)
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.OptOut -> {
|
||||
ReviewQualityCheckState.NotOptedIn()
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ExpandCollapseSettings -> {
|
||||
state.mapIfOptedIn {
|
||||
it.copy(isSettingsExpanded = !it.isSettingsExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ExpandCollapseInfo -> {
|
||||
state.mapIfOptedIn {
|
||||
it.copy(isInfoExpanded = !it.isInfoExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ExpandCollapseHighlights -> {
|
||||
state.mapIfOptedIn {
|
||||
it.copy(isHighlightsExpanded = !it.isHighlightsExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ToggleProductRecommendation -> {
|
||||
if (state is ReviewQualityCheckState.OptedIn && state.productRecommendationsPreference != null) {
|
||||
if (state.productReviewState is ProductReviewState.AnalysisPresent &&
|
||||
state.productRecommendationsPreference
|
||||
) {
|
||||
// Removes any existing product recommendation from UI
|
||||
state.copy(
|
||||
productRecommendationsPreference = false,
|
||||
productReviewState = state.productReviewState.copy(
|
||||
recommendedProductState = ReviewQualityCheckState.RecommendedProductState.Initial,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
state.copy(productRecommendationsPreference = !state.productRecommendationsPreference)
|
||||
}
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.UpdateProductReview -> {
|
||||
state.mapIfOptedIn {
|
||||
it.copy(productReviewState = action.productReviewState)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.FetchProductAnalysis, ReviewQualityCheckAction.RetryProductAnalysis -> {
|
||||
state.mapIfOptedIn {
|
||||
it.copy(productReviewState = ProductReviewState.Loading)
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ReanalyzeProduct,
|
||||
ReviewQualityCheckAction.AnalyzeProduct,
|
||||
ReviewQualityCheckAction.RestoreReanalysis,
|
||||
-> {
|
||||
state.mapIfOptedIn {
|
||||
when (it.productReviewState) {
|
||||
is ProductReviewState.AnalysisPresent -> {
|
||||
val productReviewState =
|
||||
it.productReviewState.copy(
|
||||
analysisStatus = AnalysisStatus.Reanalyzing(
|
||||
ProductReviewState.Progress(0f),
|
||||
),
|
||||
)
|
||||
it.copy(productReviewState = productReviewState)
|
||||
}
|
||||
|
||||
is ProductReviewState.NoAnalysisPresent -> {
|
||||
it.copy(
|
||||
productReviewState = it.productReviewState.copy(
|
||||
progress = ProductReviewState.Progress(0f),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.UpdateRecommendedProduct -> {
|
||||
state.mapIfOptedIn {
|
||||
if (it.productReviewState is ProductReviewState.AnalysisPresent &&
|
||||
it.productRecommendationsPreference == true
|
||||
) {
|
||||
it.copy(
|
||||
productReviewState = it.productReviewState.copy(
|
||||
recommendedProductState = action.recommendedProductState,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReviewQualityCheckAction.UpdateAnalysisProgress -> {
|
||||
state.mapIfOptedIn {
|
||||
when (it.productReviewState) {
|
||||
is ProductReviewState.NoAnalysisPresent -> {
|
||||
it.copy(
|
||||
productReviewState = it.productReviewState.copy(
|
||||
progress = ProductReviewState.Progress(action.progress.toFloat()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is ProductReviewState.AnalysisPresent -> {
|
||||
it.copy(
|
||||
productReviewState = it.productReviewState.copy(
|
||||
analysisStatus = AnalysisStatus.Reanalyzing(
|
||||
ProductReviewState.Progress(action.progress.toFloat()),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReviewQualityCheckAction.ReportProductBackInStock -> {
|
||||
state.mapIfOptedIn {
|
||||
if (it.productReviewState is ProductReviewState.Error.ProductNotAvailable) {
|
||||
it.copy(
|
||||
productReviewState = ProductReviewState.Error.ThanksForReporting,
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.InfoCardContainer
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.NoAnalysisPresent
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.Progress
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* No analysis UI for review quality check content.
|
||||
*
|
||||
* @param noAnalysisPresent The state of the analysis progress.
|
||||
* @param productRecommendationsEnabled The current state of the product recommendations toggle.
|
||||
* @param productVendor The vendor of the product.
|
||||
* @param isSettingsExpanded Whether or not the settings card is expanded.
|
||||
* @param isInfoExpanded Whether or not the info card is expanded.
|
||||
* @param onAnalyzeClick Invoked when the user clicks on the check review button.
|
||||
* @param onReviewGradeLearnMoreClick Invoked when the user clicks to learn more about review grades.
|
||||
* @param onOptOutClick Invoked when the user opts out of the review quality check feature.
|
||||
* @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product
|
||||
* recommendations toggle state.
|
||||
* @param onSettingsExpandToggleClick Invoked when the user expands or collapses the settings card.
|
||||
* @param onInfoExpandToggleClick Invoked when the user expands or collapses the info card.
|
||||
* @param onFooterLinkClick Invoked when the user clicks on the footer link.
|
||||
* @param modifier Modifier to be applied to the composable.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun NoAnalysis(
|
||||
noAnalysisPresent: NoAnalysisPresent,
|
||||
productRecommendationsEnabled: Boolean?,
|
||||
productVendor: ReviewQualityCheckState.ProductVendor,
|
||||
isSettingsExpanded: Boolean,
|
||||
isInfoExpanded: Boolean,
|
||||
onAnalyzeClick: () -> Unit,
|
||||
onReviewGradeLearnMoreClick: () -> Unit,
|
||||
onOptOutClick: () -> Unit,
|
||||
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
|
||||
onSettingsExpandToggleClick: () -> Unit,
|
||||
onInfoExpandToggleClick: () -> Unit,
|
||||
onFooterLinkClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
ReviewQualityNoAnalysisCard(noAnalysisPresent, onAnalyzeClick)
|
||||
|
||||
ReviewQualityInfoCard(
|
||||
productVendor = productVendor,
|
||||
isExpanded = isInfoExpanded,
|
||||
onLearnMoreClick = onReviewGradeLearnMoreClick,
|
||||
onExpandToggleClick = onInfoExpandToggleClick,
|
||||
)
|
||||
|
||||
ReviewQualityCheckSettingsCard(
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
isExpanded = isSettingsExpanded,
|
||||
onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange,
|
||||
onTurnOffReviewQualityCheckClick = onOptOutClick,
|
||||
onExpandToggleClick = onSettingsExpandToggleClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
ReviewQualityCheckFooter(
|
||||
onLinkClick = onFooterLinkClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewQualityNoAnalysisCard(
|
||||
noAnalysisPresent: NoAnalysisPresent,
|
||||
onAnalyzeClick: () -> Unit,
|
||||
) {
|
||||
InfoCardContainer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.shopping_no_analysis),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(all = 10.dp),
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (noAnalysisPresent.isProgressBarVisible) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
DeterminateProgressIndicator(
|
||||
progress = noAnalysisPresent.progress.normalizedProgress,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.review_quality_check_analysis_in_progress_warning_title_2,
|
||||
noAnalysisPresent.progress.formattedProgress,
|
||||
),
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.review_quality_check_analysis_in_progress_warning_body),
|
||||
style = FirefoxTheme.typography.body2,
|
||||
color = FirefoxTheme.colors.textSecondary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(id = R.string.review_quality_check_no_analysis_title),
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.review_quality_check_no_analysis_body),
|
||||
style = FirefoxTheme.typography.body2,
|
||||
color = FirefoxTheme.colors.textSecondary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
TextButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = FirefoxTheme.colors.actionPrimary,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
),
|
||||
onClick = onAnalyzeClick,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.review_quality_check_no_analysis_link),
|
||||
color = FirefoxTheme.colors.textOnColorPrimary,
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NoAnalysisPreviewModelParameterProvider :
|
||||
PreviewParameterProvider<NoAnalysisPresent> {
|
||||
override val values: Sequence<NoAnalysisPresent>
|
||||
get() = sequenceOf(
|
||||
NoAnalysisPresent(),
|
||||
NoAnalysisPresent(Progress(50f)),
|
||||
NoAnalysisPresent(Progress(100f)),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun NoAnalysisPreview(
|
||||
@PreviewParameter(NoAnalysisPreviewModelParameterProvider::class) noAnalysisPresent: NoAnalysisPresent,
|
||||
) {
|
||||
FirefoxTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = FirefoxTheme.colors.layer1)
|
||||
.padding(all = 16.dp),
|
||||
) {
|
||||
var isAnalyzing by remember { mutableStateOf(false) }
|
||||
var productRecommendationsEnabled by remember { mutableStateOf(false) }
|
||||
var isSettingsExpanded by remember { mutableStateOf(false) }
|
||||
var isInfoExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
NoAnalysis(
|
||||
noAnalysisPresent = noAnalysisPresent,
|
||||
onAnalyzeClick = { isAnalyzing = !isAnalyzing },
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
isSettingsExpanded = isSettingsExpanded,
|
||||
isInfoExpanded = isInfoExpanded,
|
||||
onReviewGradeLearnMoreClick = {},
|
||||
onOptOutClick = {},
|
||||
onProductRecommendationsEnabledStateChange = { productRecommendationsEnabled = it },
|
||||
onSettingsExpandToggleClick = { isSettingsExpanded = !isSettingsExpanded },
|
||||
onInfoExpandToggleClick = { isInfoExpanded = !isInfoExpanded },
|
||||
onFooterLinkClick = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,761 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.CollectionInfo
|
||||
import androidx.compose.ui.semantics.CollectionItemInfo
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.collectionInfo
|
||||
import androidx.compose.ui.semantics.collectionItemInfo
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.Divider
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.Image
|
||||
import org.mozilla.fenix.compose.InfoCard
|
||||
import org.mozilla.fenix.compose.InfoCardButtonText
|
||||
import org.mozilla.fenix.compose.InfoCardContainer
|
||||
import org.mozilla.fenix.compose.InfoType
|
||||
import org.mozilla.fenix.compose.button.SecondaryButton
|
||||
import org.mozilla.fenix.compose.ext.onShown
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.HighlightType
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.HighlightsInfo
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.RecommendedProductState
|
||||
import org.mozilla.fenix.shopping.ui.ext.headingResource
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
private val combinedParentHorizontalPadding = 32.dp
|
||||
private val productRecommendationImageSize = 60.dp
|
||||
|
||||
private const val PRODUCT_RECOMMENDATION_SETTLE_TIME_MS = 1500
|
||||
private const val PRODUCT_RECOMMENDATION_IMPRESSION_THRESHOLD = 0.5f
|
||||
|
||||
/**
|
||||
* UI for review quality check content displaying product analysis.
|
||||
*
|
||||
* @param productRecommendationsEnabled The current state of the product recommendations toggle.
|
||||
* @param productAnalysis The product analysis to display.
|
||||
* @param productVendor The vendor of the product.
|
||||
* @param isSettingsExpanded Whether or not the settings card is expanded.
|
||||
* @param isInfoExpanded Whether or not the info card is expanded.
|
||||
* @param isHighlightsExpanded Whether or not the highlights card is expanded.
|
||||
* @param onOptOutClick Invoked when the user opts out of the review quality check feature.
|
||||
* @param onReanalyzeClick Invoked when the user clicks to re-analyze a product.
|
||||
* @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product
|
||||
* recommendations toggle state.
|
||||
* @param onReviewGradeLearnMoreClick Invoked when the user clicks to learn more about review grades.
|
||||
* @param onFooterLinkClick Invoked when the user clicks on the footer link.
|
||||
* @param onHighlightsExpandToggleClick Invoked when the user clicks to show more recent reviews.
|
||||
* @param onSettingsExpandToggleClick Invoked when the user expands or collapses the settings card.
|
||||
* @param onInfoExpandToggleClick Invoked when the user expands or collapses the info card.
|
||||
* @param onRecommendedProductClick Invoked when the user clicks on the product recommendation.
|
||||
* @param onRecommendedProductImpression Invoked when the user has seen the product recommendation.
|
||||
* @param modifier The modifier to be applied to the Composable.
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun ProductAnalysis(
|
||||
productRecommendationsEnabled: Boolean?,
|
||||
productAnalysis: AnalysisPresent,
|
||||
productVendor: ReviewQualityCheckState.ProductVendor,
|
||||
isSettingsExpanded: Boolean,
|
||||
isInfoExpanded: Boolean,
|
||||
isHighlightsExpanded: Boolean,
|
||||
onOptOutClick: () -> Unit,
|
||||
onReanalyzeClick: () -> Unit,
|
||||
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
|
||||
onReviewGradeLearnMoreClick: () -> Unit,
|
||||
onFooterLinkClick: () -> Unit,
|
||||
onHighlightsExpandToggleClick: () -> Unit,
|
||||
onSettingsExpandToggleClick: () -> Unit,
|
||||
onInfoExpandToggleClick: () -> Unit,
|
||||
onRecommendedProductClick: (aid: String, url: String) -> Unit,
|
||||
onRecommendedProductImpression: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
when (val analysisStatus = productAnalysis.analysisStatus) {
|
||||
is AnalysisStatus.NeedsAnalysis -> {
|
||||
ReanalyzeCard(onReanalyzeClick = onReanalyzeClick)
|
||||
}
|
||||
|
||||
is AnalysisStatus.Reanalyzing -> {
|
||||
ReanalysisInProgress(analysisStatus)
|
||||
}
|
||||
|
||||
is AnalysisStatus.UpToDate -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
if (productAnalysis.reviewGrade != null) {
|
||||
ReviewGradeCard(
|
||||
reviewGrade = productAnalysis.reviewGrade,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
if (productAnalysis.adjustedRating != null) {
|
||||
AdjustedProductRatingCard(
|
||||
rating = productAnalysis.adjustedRating,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
if (productAnalysis.highlightsInfo != null) {
|
||||
HighlightsCard(
|
||||
highlightsInfo = productAnalysis.highlightsInfo,
|
||||
onHighlightsExpandToggleClick = onHighlightsExpandToggleClick,
|
||||
isExpanded = isHighlightsExpanded,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
ReviewQualityInfoCard(
|
||||
productVendor = productVendor,
|
||||
isExpanded = isInfoExpanded,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onExpandToggleClick = onInfoExpandToggleClick,
|
||||
onLearnMoreClick = onReviewGradeLearnMoreClick,
|
||||
)
|
||||
|
||||
if (productAnalysis.recommendedProductState is RecommendedProductState.Product) {
|
||||
ProductRecommendation(
|
||||
product = productAnalysis.recommendedProductState,
|
||||
onClick = onRecommendedProductClick,
|
||||
onImpression = onRecommendedProductImpression,
|
||||
)
|
||||
}
|
||||
|
||||
ReviewQualityCheckSettingsCard(
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
isExpanded = isSettingsExpanded,
|
||||
onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange,
|
||||
onTurnOffReviewQualityCheckClick = onOptOutClick,
|
||||
onExpandToggleClick = onSettingsExpandToggleClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
ReviewQualityCheckFooter(
|
||||
onLinkClick = onFooterLinkClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReanalyzeCard(
|
||||
onReanalyzeClick: () -> Unit,
|
||||
) {
|
||||
InfoCard(
|
||||
title = stringResource(R.string.review_quality_check_outdated_analysis_warning_title),
|
||||
type = InfoType.InfoPlain,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonText = InfoCardButtonText(
|
||||
text = stringResource(R.string.review_quality_check_outdated_analysis_warning_action),
|
||||
onClick = onReanalyzeClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReanalysisInProgress(reanalyzing: AnalysisStatus.Reanalyzing) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
) {
|
||||
DeterminateProgressIndicator(
|
||||
progress = reanalyzing.progress.normalizedProgress,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.review_quality_check_analysis_in_progress_warning_title_2,
|
||||
reanalyzing.progress.formattedProgress,
|
||||
),
|
||||
style = FirefoxTheme.typography.subtitle1,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewGradeCard(
|
||||
reviewGrade: ReviewQualityCheckState.Grade,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
InfoCardContainer(modifier = modifier.semantics(mergeDescendants = true) { heading() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.review_quality_check_grade_title),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
ReviewGradeExpanded(grade = reviewGrade)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun AdjustedProductRatingCard(
|
||||
rating: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
InfoCardContainer(modifier = modifier.semantics(mergeDescendants = true) { heading() }) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.review_quality_check_adjusted_rating_title),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
modifier = Modifier.padding(
|
||||
end = 16.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
)
|
||||
|
||||
StarRating(
|
||||
value = rating,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.review_quality_check_adjusted_rating_description_2),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun HighlightsCard(
|
||||
highlightsInfo: HighlightsInfo,
|
||||
isExpanded: Boolean,
|
||||
onHighlightsExpandToggleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
InfoCardContainer(modifier = modifier) {
|
||||
val highlightsToDisplay = remember(isExpanded, highlightsInfo.highlights) {
|
||||
if (isExpanded) {
|
||||
highlightsInfo.highlights
|
||||
} else {
|
||||
highlightsInfo.highlightsForCompactMode
|
||||
}
|
||||
}
|
||||
val titleContentDescription =
|
||||
headingResource(id = R.string.review_quality_check_highlights_title)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.review_quality_check_highlights_title),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
modifier = Modifier.semantics {
|
||||
heading()
|
||||
contentDescription = titleContentDescription
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(animationSpec = spring()),
|
||||
) {
|
||||
highlightsToDisplay.onEachIndexed { indexHighlightTitle, highlight ->
|
||||
Box(
|
||||
modifier = Modifier.semantics {
|
||||
collectionInfo = CollectionInfo(rowCount = highlightsToDisplay.size, columnCount = 1)
|
||||
},
|
||||
) {
|
||||
HighlightTitle(
|
||||
highlightType = highlight.key,
|
||||
modifier = Modifier.semantics {
|
||||
collectionItemInfo = CollectionItemInfo(
|
||||
rowIndex = indexHighlightTitle,
|
||||
rowSpan = 1,
|
||||
columnIndex = 1,
|
||||
columnSpan = 1,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.semantics {
|
||||
collectionInfo =
|
||||
CollectionInfo(rowCount = highlight.value.size, columnCount = 1)
|
||||
},
|
||||
) {
|
||||
highlight.value.onEachIndexed { index, text ->
|
||||
HighlightText(
|
||||
text = text,
|
||||
modifier = Modifier.semantics {
|
||||
collectionItemInfo = CollectionItemInfo(
|
||||
rowIndex = index,
|
||||
rowSpan = 1,
|
||||
columnIndex = 1,
|
||||
columnSpan = 1,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
if (highlightsToDisplay.entries.last().key != highlight.key) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(
|
||||
targetState = isExpanded,
|
||||
label = "HighlightsCard-Crossfade",
|
||||
) { expanded ->
|
||||
if (expanded.not() && highlightsInfo.highlightsFadeVisible) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(32.dp)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
FirefoxTheme.colors.layer2.copy(alpha = 0f),
|
||||
FirefoxTheme.colors.layer2,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightsInfo.showMoreButtonVisible) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Divider(modifier = Modifier.extendWidthToParentBorder())
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
SecondaryButton(
|
||||
text = if (isExpanded) {
|
||||
stringResource(R.string.review_quality_check_highlights_show_less)
|
||||
} else {
|
||||
stringResource(R.string.review_quality_check_highlights_show_more)
|
||||
},
|
||||
onClick = onHighlightsExpandToggleClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HighlightText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.surrounded_with_quotes, text),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HighlightTitle(
|
||||
highlightType: HighlightType,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
val highlight = remember(highlightType) { highlightType.toHighlight() }
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = highlight.iconResourceId),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = highlight.titleResourceId),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.extendWidthToParentBorder(): Modifier =
|
||||
this.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(
|
||||
constraints.copy(
|
||||
maxWidth = constraints.maxWidth + combinedParentHorizontalPadding.roundToPx(),
|
||||
),
|
||||
)
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.place(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun HighlightType.toHighlight() =
|
||||
when (this) {
|
||||
HighlightType.QUALITY -> Highlight.QUALITY
|
||||
HighlightType.PRICE -> Highlight.PRICE
|
||||
HighlightType.SHIPPING -> Highlight.SHIPPING
|
||||
HighlightType.PACKAGING_AND_APPEARANCE -> Highlight.PACKAGING_AND_APPEARANCE
|
||||
HighlightType.COMPETITIVENESS -> Highlight.COMPETITIVENESS
|
||||
}
|
||||
|
||||
private enum class Highlight(
|
||||
val titleResourceId: Int,
|
||||
val iconResourceId: Int,
|
||||
) {
|
||||
QUALITY(
|
||||
titleResourceId = R.string.review_quality_check_highlights_type_quality,
|
||||
iconResourceId = R.drawable.mozac_ic_quality_24,
|
||||
),
|
||||
PRICE(
|
||||
titleResourceId = R.string.review_quality_check_highlights_type_price,
|
||||
iconResourceId = R.drawable.mozac_ic_price_24,
|
||||
),
|
||||
SHIPPING(
|
||||
titleResourceId = R.string.review_quality_check_highlights_type_shipping,
|
||||
iconResourceId = R.drawable.mozac_ic_shipping_24,
|
||||
),
|
||||
PACKAGING_AND_APPEARANCE(
|
||||
titleResourceId = R.string.review_quality_check_highlights_type_packaging_appearance,
|
||||
iconResourceId = R.drawable.mozac_ic_packaging_24,
|
||||
),
|
||||
COMPETITIVENESS(
|
||||
titleResourceId = R.string.review_quality_check_highlights_type_competitiveness,
|
||||
iconResourceId = R.drawable.mozac_ic_competitiveness_24,
|
||||
),
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ProductRecommendation(
|
||||
product: RecommendedProductState.Product,
|
||||
onClick: (String, String) -> Unit,
|
||||
onImpression: (String) -> Unit,
|
||||
) {
|
||||
val titleContentDescription = headingResource(id = R.string.review_quality_check_ad_title)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
InfoCardContainer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onShown(
|
||||
threshold = PRODUCT_RECOMMENDATION_IMPRESSION_THRESHOLD,
|
||||
settleTime = PRODUCT_RECOMMENDATION_SETTLE_TIME_MS,
|
||||
onVisible = { onImpression(product.aid) },
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
.clearAndSetSemantics {
|
||||
heading()
|
||||
contentDescription = titleContentDescription
|
||||
}
|
||||
.clickable(interactionSource = interactionSource, indication = null) {
|
||||
onClick(product.aid, product.productUrl)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.review_quality_check_ad_title),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.clickable { onClick(product.aid, product.productUrl) },
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Image(
|
||||
url = product.imageUrl,
|
||||
modifier = Modifier.size(productRecommendationImageSize),
|
||||
targetSize = productRecommendationImageSize,
|
||||
placeholder = { ImagePlaceholder() },
|
||||
fallback = { ImagePlaceholder() },
|
||||
)
|
||||
|
||||
Text(
|
||||
text = product.name,
|
||||
modifier = Modifier.weight(1.0f),
|
||||
color = FirefoxTheme.colors.textAccent,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
|
||||
ReviewGradeCompact(grade = product.reviewGrade)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = product.formattedPrice,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
)
|
||||
|
||||
StarRating(value = product.adjustedRating)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.review_quality_check_ad_caption,
|
||||
stringResource(id = R.string.shopping_product_name),
|
||||
),
|
||||
color = FirefoxTheme.colors.textSecondary,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImagePlaceholder() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(productRecommendationImageSize)
|
||||
.background(
|
||||
color = FirefoxTheme.colors.layer3,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_file_type_image),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ProductAnalysisPreviewModel(
|
||||
val productRecommendationsEnabled: Boolean?,
|
||||
val productAnalysis: AnalysisPresent,
|
||||
val productVendor: ReviewQualityCheckState.ProductVendor,
|
||||
) {
|
||||
constructor(
|
||||
productRecommendationsEnabled: Boolean? = false,
|
||||
productId: String = "123",
|
||||
reviewGrade: ReviewQualityCheckState.Grade? = ReviewQualityCheckState.Grade.B,
|
||||
analysisStatus: AnalysisStatus = AnalysisStatus.UpToDate,
|
||||
adjustedRating: Float? = 3.6f,
|
||||
productUrl: String = "",
|
||||
highlightsInfo: HighlightsInfo = HighlightsInfo(
|
||||
mapOf(
|
||||
HighlightType.QUALITY to listOf(
|
||||
"High quality",
|
||||
"Excellent craftsmanship",
|
||||
"Superior materials",
|
||||
),
|
||||
HighlightType.PRICE to listOf(
|
||||
"Affordable prices",
|
||||
"Great value for money",
|
||||
"Discounted offers",
|
||||
),
|
||||
HighlightType.SHIPPING to listOf(
|
||||
"Fast and reliable shipping",
|
||||
"Free shipping options",
|
||||
"Express delivery",
|
||||
),
|
||||
HighlightType.PACKAGING_AND_APPEARANCE to listOf(
|
||||
"Elegant packaging",
|
||||
"Attractive appearance",
|
||||
"Beautiful design",
|
||||
),
|
||||
HighlightType.COMPETITIVENESS to listOf(
|
||||
"Competitive pricing",
|
||||
"Strong market presence",
|
||||
"Unbeatable deals",
|
||||
),
|
||||
),
|
||||
),
|
||||
recommendedProductState: RecommendedProductState = RecommendedProductState.Initial,
|
||||
productVendor: ReviewQualityCheckState.ProductVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
) : this(
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
productAnalysis = AnalysisPresent(
|
||||
productId = productId,
|
||||
reviewGrade = reviewGrade,
|
||||
analysisStatus = analysisStatus,
|
||||
adjustedRating = adjustedRating,
|
||||
productUrl = productUrl,
|
||||
highlightsInfo = highlightsInfo,
|
||||
recommendedProductState = recommendedProductState,
|
||||
),
|
||||
productVendor = productVendor,
|
||||
)
|
||||
}
|
||||
|
||||
private class ProductAnalysisPreviewModelParameterProvider :
|
||||
PreviewParameterProvider<ProductAnalysisPreviewModel> {
|
||||
override val values: Sequence<ProductAnalysisPreviewModel>
|
||||
get() = sequenceOf(
|
||||
ProductAnalysisPreviewModel(),
|
||||
ProductAnalysisPreviewModel(
|
||||
analysisStatus = AnalysisStatus.NeedsAnalysis,
|
||||
),
|
||||
ProductAnalysisPreviewModel(
|
||||
analysisStatus = AnalysisStatus.Reanalyzing(
|
||||
ReviewQualityCheckState.OptedIn.ProductReviewState.Progress(40f),
|
||||
),
|
||||
),
|
||||
ProductAnalysisPreviewModel(
|
||||
analysisStatus = AnalysisStatus.Reanalyzing(
|
||||
ReviewQualityCheckState.OptedIn.ProductReviewState.Progress(95f),
|
||||
),
|
||||
),
|
||||
ProductAnalysisPreviewModel(
|
||||
reviewGrade = null,
|
||||
),
|
||||
ProductAnalysisPreviewModel(
|
||||
highlightsInfo = HighlightsInfo(
|
||||
mapOf(
|
||||
HighlightType.QUALITY to listOf(
|
||||
"High quality",
|
||||
"Excellent craftsmanship",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ProductAnalysisPreviewModel(
|
||||
productRecommendationsEnabled = true,
|
||||
recommendedProductState = RecommendedProductState.Product(
|
||||
aid = "aid",
|
||||
name = "The best desk ever with a really really really long product name that " +
|
||||
"forces the preview to wrap its text to at least 4 lines.",
|
||||
productUrl = "www.mozilla.com",
|
||||
imageUrl = "https://i.fakespot.io/b6vx27xf3rgwr1a597q6qd3rutp6",
|
||||
formattedPrice = "$123.45",
|
||||
reviewGrade = ReviewQualityCheckState.Grade.B,
|
||||
adjustedRating = 4.23f,
|
||||
isSponsored = true,
|
||||
analysisUrl = "",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun ProductAnalysisPreview(
|
||||
@PreviewParameter(ProductAnalysisPreviewModelParameterProvider::class) model: ProductAnalysisPreviewModel,
|
||||
) {
|
||||
FirefoxTheme {
|
||||
ReviewQualityCheckScaffold(
|
||||
onRequestDismiss = {},
|
||||
) {
|
||||
var productRecommendationsEnabled by remember { mutableStateOf(model.productRecommendationsEnabled) }
|
||||
var isSettingsExpanded by remember { mutableStateOf(false) }
|
||||
var isInfoExpanded by remember { mutableStateOf(false) }
|
||||
var isHighlightsExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
ProductAnalysis(
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
productAnalysis = model.productAnalysis,
|
||||
productVendor = model.productVendor,
|
||||
isSettingsExpanded = isSettingsExpanded,
|
||||
isInfoExpanded = isInfoExpanded,
|
||||
isHighlightsExpanded = isHighlightsExpanded,
|
||||
onOptOutClick = {},
|
||||
onReanalyzeClick = {},
|
||||
onProductRecommendationsEnabledStateChange = {
|
||||
productRecommendationsEnabled = it
|
||||
},
|
||||
onReviewGradeLearnMoreClick = {},
|
||||
onFooterLinkClick = {},
|
||||
onHighlightsExpandToggleClick = { isHighlightsExpanded = !isHighlightsExpanded },
|
||||
onSettingsExpandToggleClick = { isSettingsExpanded = !isSettingsExpanded },
|
||||
onInfoExpandToggleClick = { isInfoExpanded = !isInfoExpanded },
|
||||
onRecommendedProductClick = { _, _ -> },
|
||||
onRecommendedProductImpression = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.InfoCard
|
||||
import org.mozilla.fenix.compose.InfoCardButtonText
|
||||
import org.mozilla.fenix.compose.InfoType
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Product analysis error UI
|
||||
*
|
||||
* @param error The error state to display.
|
||||
* @param onReportBackInStockClick Invoked when the user clicks on the report back in stock button.
|
||||
* @param productRecommendationsEnabled The current state of the product recommendations toggle.
|
||||
* @param productVendor The vendor of the product.
|
||||
* @param isSettingsExpanded Whether or not the settings card is expanded.
|
||||
* @param isInfoExpanded Whether or not the info card is expanded.
|
||||
* @param onReviewGradeLearnMoreClick Invoked when the user clicks to learn more about review grades.
|
||||
* @param onOptOutClick Invoked when the user opts out of the review quality check feature.
|
||||
* @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product
|
||||
* recommendations toggle state.
|
||||
* @param onFooterLinkClick Invoked when the user clicks on the footer link.
|
||||
* @param onSettingsExpandToggleClick Invoked when the user expands or collapses the settings card.
|
||||
* @param onInfoExpandToggleClick Invoked when the user expands or collapses the info card.
|
||||
* @param modifier Modifier to apply to the layout.
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
fun ProductAnalysisError(
|
||||
error: ProductReviewState.Error,
|
||||
onReportBackInStockClick: () -> Unit,
|
||||
productRecommendationsEnabled: Boolean?,
|
||||
productVendor: ReviewQualityCheckState.ProductVendor,
|
||||
isSettingsExpanded: Boolean,
|
||||
isInfoExpanded: Boolean,
|
||||
onReviewGradeLearnMoreClick: () -> Unit,
|
||||
onOptOutClick: () -> Unit,
|
||||
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
|
||||
onFooterLinkClick: () -> Unit,
|
||||
onSettingsExpandToggleClick: () -> Unit,
|
||||
onInfoExpandToggleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
val (
|
||||
@StringRes titleResourceId: Int,
|
||||
@StringRes descriptionResourceId: Int,
|
||||
type: InfoType,
|
||||
) = when (error) {
|
||||
ProductReviewState.Error.GenericError -> {
|
||||
Triple(
|
||||
R.string.review_quality_check_generic_error_title,
|
||||
R.string.review_quality_check_generic_error_body,
|
||||
InfoType.Info,
|
||||
)
|
||||
}
|
||||
|
||||
ProductReviewState.Error.NetworkError -> {
|
||||
Triple(
|
||||
R.string.review_quality_check_no_connection_title,
|
||||
R.string.review_quality_check_no_connection_body,
|
||||
InfoType.Warning,
|
||||
)
|
||||
}
|
||||
|
||||
ProductReviewState.Error.UnsupportedProductTypeError -> {
|
||||
Triple(
|
||||
R.string.review_quality_check_not_analyzable_info_title,
|
||||
R.string.review_quality_check_not_analyzable_info_body,
|
||||
InfoType.Info,
|
||||
)
|
||||
}
|
||||
|
||||
ProductReviewState.Error.NotEnoughReviews -> {
|
||||
Triple(
|
||||
R.string.review_quality_check_no_reviews_warning_title,
|
||||
R.string.review_quality_check_no_reviews_warning_body,
|
||||
InfoType.Info,
|
||||
)
|
||||
}
|
||||
|
||||
ProductReviewState.Error.ProductNotAvailable -> {
|
||||
Triple(
|
||||
R.string.review_quality_check_product_availability_warning_title,
|
||||
R.string.review_quality_check_product_availability_warning_body,
|
||||
InfoType.Info,
|
||||
)
|
||||
}
|
||||
|
||||
ProductReviewState.Error.ProductAlreadyReported -> {
|
||||
Triple(
|
||||
R.string.review_quality_check_analysis_requested_other_user_info_title,
|
||||
R.string.review_quality_check_analysis_requested_other_user_info_body,
|
||||
InfoType.Info,
|
||||
)
|
||||
}
|
||||
|
||||
ProductReviewState.Error.ThanksForReporting -> {
|
||||
Triple(
|
||||
R.string.review_quality_check_analysis_requested_info_title,
|
||||
R.string.review_quality_check_analysis_requested_info_body,
|
||||
InfoType.Info,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (error == ProductReviewState.Error.ProductNotAvailable) {
|
||||
InfoCard(
|
||||
title = stringResource(id = titleResourceId),
|
||||
description = stringResource(id = descriptionResourceId),
|
||||
type = type,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonText = InfoCardButtonText(
|
||||
text = stringResource(R.string.review_quality_check_product_availability_warning_action_2),
|
||||
onClick = onReportBackInStockClick,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
InfoCard(
|
||||
title = stringResource(id = titleResourceId),
|
||||
description = stringResource(id = descriptionResourceId),
|
||||
type = type,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
ReviewQualityInfoCard(
|
||||
productVendor = productVendor,
|
||||
isExpanded = isInfoExpanded,
|
||||
onLearnMoreClick = onReviewGradeLearnMoreClick,
|
||||
onExpandToggleClick = onInfoExpandToggleClick,
|
||||
)
|
||||
|
||||
ReviewQualityCheckSettingsCard(
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
isExpanded = isSettingsExpanded,
|
||||
onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange,
|
||||
onTurnOffReviewQualityCheckClick = onOptOutClick,
|
||||
onExpandToggleClick = onSettingsExpandToggleClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
ReviewQualityCheckFooter(
|
||||
onLinkClick = onFooterLinkClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun ProductAnalysisErrorPreview() {
|
||||
FirefoxTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = FirefoxTheme.colors.layer1)
|
||||
.padding(all = 16.dp),
|
||||
) {
|
||||
var productRecommendationsEnabled by remember { mutableStateOf(false) }
|
||||
var isSettingsExpanded by remember { mutableStateOf(false) }
|
||||
var isInfoExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
ProductAnalysisError(
|
||||
error = ProductReviewState.Error.NetworkError,
|
||||
onReportBackInStockClick = { },
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
isSettingsExpanded = isSettingsExpanded,
|
||||
isInfoExpanded = isInfoExpanded,
|
||||
onReviewGradeLearnMoreClick = {},
|
||||
onOptOutClick = {},
|
||||
onProductRecommendationsEnabledStateChange = { productRecommendationsEnabled = it },
|
||||
onFooterLinkClick = {},
|
||||
onSettingsExpandToggleClick = { isSettingsExpanded = !isSettingsExpanded },
|
||||
onInfoExpandToggleClick = { isInfoExpanded = !isInfoExpanded },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.StartOffset
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
private const val ANIMATION_DURATION_MS = 1000
|
||||
private const val INITIAL_ALPHA = 0.25f
|
||||
private const val TARGET_ALPHA = 1f
|
||||
private val boxes = listOf(
|
||||
BoxInfo(height = 80.dp, offsetMillis = 500),
|
||||
BoxInfo(height = 80.dp, offsetMillis = 1000),
|
||||
BoxInfo(height = 192.dp, offsetMillis = 0),
|
||||
BoxInfo(height = 40.dp, offsetMillis = 500),
|
||||
BoxInfo(height = 192.dp, offsetMillis = 1000),
|
||||
)
|
||||
|
||||
/**
|
||||
* Loading UI for review quality check content.
|
||||
*/
|
||||
@Composable
|
||||
fun ProductReviewLoading(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition("ProductReviewLoading")
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
boxes.forEach { boxInfo ->
|
||||
val alpha by infiniteTransition.animateFloat(
|
||||
initialValue = INITIAL_ALPHA,
|
||||
targetValue = TARGET_ALPHA,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(ANIMATION_DURATION_MS, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(offsetMillis = boxInfo.offsetMillis),
|
||||
),
|
||||
label = "ProductReviewLoading-Alpha",
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(boxInfo.height)
|
||||
.fillMaxWidth()
|
||||
.alpha(alpha)
|
||||
.background(
|
||||
color = FirefoxTheme.colors.layer3,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class BoxInfo(
|
||||
val height: Dp,
|
||||
val offsetMillis: Int,
|
||||
)
|
||||
|
||||
@LightDarkPreview
|
||||
@Composable
|
||||
private fun ProductReviewLoadingPreview() {
|
||||
FirefoxTheme {
|
||||
ReviewQualityCheckScaffold(
|
||||
onRequestDismiss = {},
|
||||
) {
|
||||
ProductReviewLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
private val ProgressIndicatorWidth = 5.dp
|
||||
|
||||
/**
|
||||
* UI displaying a circular progress indicator with a determinate value.
|
||||
*
|
||||
* @param progress The progress value to display.
|
||||
* @param modifier [Modifier] to be applied to the indicator.
|
||||
*/
|
||||
@Composable
|
||||
fun DeterminateProgressIndicator(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val floatState = animateFloatAsState(
|
||||
progress,
|
||||
label = "DeterminateProgressIndicator",
|
||||
)
|
||||
|
||||
CircularProgressIndicator(
|
||||
modifier = modifier,
|
||||
progress = floatState.value,
|
||||
color = FirefoxTheme.colors.layerAccent,
|
||||
backgroundColor = FirefoxTheme.colors.actionTertiary,
|
||||
strokeWidth = ProgressIndicatorWidth,
|
||||
strokeCap = StrokeCap.Butt,
|
||||
)
|
||||
}
|
||||
|
||||
@LightDarkPreview
|
||||
@Composable
|
||||
private fun DeterminateProgressIndicatorPreviewDark() {
|
||||
FirefoxTheme {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(FirefoxTheme.colors.layer1)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
var progress: Float by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
DeterminateProgressIndicator(
|
||||
progress = progress,
|
||||
modifier = Modifier.size(48.dp),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Button(
|
||||
onClick = { progress += 0.1f },
|
||||
modifier = Modifier.height(56.dp),
|
||||
) {
|
||||
Text(text = "Increase progress")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { progress -= 0.1f },
|
||||
modifier = Modifier.height(56.dp),
|
||||
) {
|
||||
Text(text = "Decrease progress")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import mozilla.components.ui.colors.PhotonColors
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.Grade
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
private val height = 24.dp
|
||||
private val borderColor = Color(0x26000000)
|
||||
private val reviewGradeAColor = PhotonColors.Green20
|
||||
private val reviewGradeBColor = PhotonColors.Blue10
|
||||
private val reviewGradeCColor = PhotonColors.Yellow20
|
||||
private val reviewGradeDColor = PhotonColors.Orange20
|
||||
private val reviewGradeFColor = PhotonColors.Red30
|
||||
private val reviewGradeAColorExpanded = Color(0xFFEEFFF9)
|
||||
private val reviewGradeBColorExpanded = Color(0xFFDEFAFF)
|
||||
private val reviewGradeCColorExpanded = Color(0xFFFFF9DA)
|
||||
private val reviewGradeDColorExpanded = Color(0xFFFDEEE2)
|
||||
private val reviewGradeFColorExpanded = Color(0xFFFFEFF0)
|
||||
|
||||
/**
|
||||
* Review Grade of the product - A being the best and F being the worst.
|
||||
*/
|
||||
private enum class ReviewGrade(
|
||||
val stringResourceId: Int,
|
||||
val backgroundColor: Color,
|
||||
val expandedTextBackgroundColor: Color,
|
||||
) {
|
||||
A(
|
||||
stringResourceId = R.string.review_quality_check_grade_a_b_description,
|
||||
backgroundColor = reviewGradeAColor,
|
||||
expandedTextBackgroundColor = reviewGradeAColorExpanded,
|
||||
),
|
||||
B(
|
||||
stringResourceId = R.string.review_quality_check_grade_a_b_description,
|
||||
backgroundColor = reviewGradeBColor,
|
||||
expandedTextBackgroundColor = reviewGradeBColorExpanded,
|
||||
),
|
||||
C(
|
||||
stringResourceId = R.string.review_quality_check_grade_c_description,
|
||||
backgroundColor = reviewGradeCColor,
|
||||
expandedTextBackgroundColor = reviewGradeCColorExpanded,
|
||||
),
|
||||
D(
|
||||
stringResourceId = R.string.review_quality_check_grade_d_f_description,
|
||||
backgroundColor = reviewGradeDColor,
|
||||
expandedTextBackgroundColor = reviewGradeDColorExpanded,
|
||||
),
|
||||
F(
|
||||
stringResourceId = R.string.review_quality_check_grade_d_f_description,
|
||||
backgroundColor = reviewGradeFColor,
|
||||
expandedTextBackgroundColor = reviewGradeFColorExpanded,
|
||||
),
|
||||
}
|
||||
|
||||
/**
|
||||
* UI for displaying the review grade.
|
||||
*
|
||||
* @param modifier The modifier to be applied to the Composable.
|
||||
* @param grade The grade of the product.
|
||||
*/
|
||||
@Composable
|
||||
fun ReviewGradeCompact(
|
||||
modifier: Modifier = Modifier,
|
||||
grade: Grade,
|
||||
) {
|
||||
ReviewGradeLetter(
|
||||
reviewGrade = grade.toReviewGrade(),
|
||||
modifier = modifier.border(
|
||||
border = BorderStroke(
|
||||
width = 1.dp,
|
||||
color = borderColor,
|
||||
),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* UI for displaying the review grade with descriptive text.
|
||||
*
|
||||
* @param modifier The modifier to be applied to the Composable.
|
||||
* @param grade The grade of the product.
|
||||
*/
|
||||
@Composable
|
||||
fun ReviewGradeExpanded(
|
||||
modifier: Modifier = Modifier,
|
||||
grade: Grade,
|
||||
) {
|
||||
val reviewGrade = grade.toReviewGrade()
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = reviewGrade.expandedTextBackgroundColor,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
)
|
||||
.border(
|
||||
border = BorderStroke(
|
||||
width = 1.dp,
|
||||
color = borderColor,
|
||||
),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val cornerSize = CornerSize(0.dp)
|
||||
val shape =
|
||||
MaterialTheme.shapes.small.copy(topEnd = cornerSize, bottomEnd = cornerSize)
|
||||
|
||||
ReviewGradeLetter(
|
||||
reviewGrade = reviewGrade,
|
||||
shape = shape,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = reviewGrade.stringResourceId),
|
||||
color = PhotonColors.DarkGrey90,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common UI building block for the review grade.
|
||||
*/
|
||||
@Composable
|
||||
private fun ReviewGradeLetter(
|
||||
modifier: Modifier = Modifier,
|
||||
reviewGrade: ReviewGrade,
|
||||
shape: Shape = MaterialTheme.shapes.small,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(height)
|
||||
.background(
|
||||
color = reviewGrade.backgroundColor,
|
||||
shape = shape,
|
||||
)
|
||||
.wrapContentSize(Alignment.Center),
|
||||
) {
|
||||
Text(
|
||||
text = reviewGrade.name,
|
||||
color = PhotonColors.Black,
|
||||
style = FirefoxTheme.typography.subtitle2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps [Grade] to [ReviewGrade].
|
||||
*/
|
||||
private fun Grade.toReviewGrade(): ReviewGrade =
|
||||
when (this) {
|
||||
Grade.A -> ReviewGrade.A
|
||||
Grade.B -> ReviewGrade.B
|
||||
Grade.C -> ReviewGrade.C
|
||||
Grade.D -> ReviewGrade.D
|
||||
Grade.F -> ReviewGrade.F
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun ReviewGradePreview() {
|
||||
FirefoxTheme {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(FirefoxTheme.colors.layer1)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Grade.entries.forEach {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
ReviewGradeCompact(grade = it)
|
||||
|
||||
ReviewGradeExpanded(grade = it)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import mozilla.components.lib.state.ext.observeAsState
|
||||
import org.mozilla.fenix.shopping.store.BottomSheetDismissSource
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
|
||||
|
||||
/**
|
||||
* Top-level UI for the Review Quality Check feature.
|
||||
*
|
||||
* @param store [ReviewQualityCheckStore] that holds the state.
|
||||
* @param onRequestDismiss Invoked when a user action requests dismissal of the bottom sheet.
|
||||
* @param modifier The modifier to be applied to the Composable.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun ReviewQualityCheckBottomSheet(
|
||||
store: ReviewQualityCheckStore,
|
||||
onRequestDismiss: (source: BottomSheetDismissSource) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val reviewQualityCheckState by store.observeAsState(ReviewQualityCheckState.Initial) { it }
|
||||
val isOptedIn =
|
||||
remember(reviewQualityCheckState) { reviewQualityCheckState is ReviewQualityCheckState.OptedIn }
|
||||
|
||||
ReviewQualityCheckScaffold(
|
||||
onRequestDismiss = {
|
||||
onRequestDismiss(BottomSheetDismissSource.HANDLE_CLICKED)
|
||||
},
|
||||
modifier = modifier.animateContentSize(),
|
||||
) {
|
||||
when (val state = reviewQualityCheckState) {
|
||||
is ReviewQualityCheckState.NotOptedIn -> {
|
||||
ReviewQualityCheckContextualOnboarding(
|
||||
productVendors = state.productVendors,
|
||||
onPrimaryButtonClick = {
|
||||
store.dispatch(ReviewQualityCheckAction.OptIn)
|
||||
},
|
||||
onLearnMoreClick = {
|
||||
onRequestDismiss(BottomSheetDismissSource.LINK_OPENED)
|
||||
store.dispatch(ReviewQualityCheckAction.OpenOnboardingLearnMoreLink)
|
||||
},
|
||||
onPrivacyPolicyClick = {
|
||||
onRequestDismiss(BottomSheetDismissSource.LINK_OPENED)
|
||||
store.dispatch(ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink)
|
||||
},
|
||||
onTermsOfUseClick = {
|
||||
onRequestDismiss(BottomSheetDismissSource.LINK_OPENED)
|
||||
store.dispatch(ReviewQualityCheckAction.OpenOnboardingTermsLink)
|
||||
},
|
||||
onSecondaryButtonClick = {
|
||||
onRequestDismiss(BottomSheetDismissSource.NOT_NOW)
|
||||
store.dispatch(ReviewQualityCheckAction.NotNowClicked)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is ReviewQualityCheckState.OptedIn -> {
|
||||
ProductReview(
|
||||
state = state,
|
||||
onOptOutClick = {
|
||||
onRequestDismiss(BottomSheetDismissSource.OPT_OUT)
|
||||
store.dispatch(ReviewQualityCheckAction.OptOut)
|
||||
},
|
||||
onAnalyzeClick = {
|
||||
store.dispatch(ReviewQualityCheckAction.AnalyzeProduct)
|
||||
},
|
||||
onReanalyzeClick = {
|
||||
store.dispatch(ReviewQualityCheckAction.ReanalyzeProduct)
|
||||
},
|
||||
onReportBackInStockClick = {
|
||||
store.dispatch(ReviewQualityCheckAction.ReportProductBackInStock)
|
||||
},
|
||||
onProductRecommendationsEnabledStateChange = {
|
||||
store.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation)
|
||||
},
|
||||
onReviewGradeLearnMoreClick = {
|
||||
onRequestDismiss(BottomSheetDismissSource.LINK_OPENED)
|
||||
store.dispatch(ReviewQualityCheckAction.OpenExplainerLearnMoreLink)
|
||||
},
|
||||
onFooterLinkClick = {
|
||||
onRequestDismiss(BottomSheetDismissSource.LINK_OPENED)
|
||||
store.dispatch(ReviewQualityCheckAction.OpenPoweredByLink)
|
||||
},
|
||||
onSettingsExpandToggleClick = {
|
||||
store.dispatch(ReviewQualityCheckAction.ExpandCollapseSettings)
|
||||
},
|
||||
onInfoExpandToggleClick = {
|
||||
store.dispatch(ReviewQualityCheckAction.ExpandCollapseInfo)
|
||||
},
|
||||
onNoAnalysisPresent = {
|
||||
store.dispatch(ReviewQualityCheckAction.NoAnalysisDisplayed)
|
||||
},
|
||||
onHighlightsExpandToggleClick = {
|
||||
store.dispatch(ReviewQualityCheckAction.ExpandCollapseHighlights)
|
||||
},
|
||||
onRecommendedProductClick = { aid, url ->
|
||||
onRequestDismiss(BottomSheetDismissSource.LINK_OPENED)
|
||||
store.dispatch(ReviewQualityCheckAction.RecommendedProductClick(aid, url))
|
||||
},
|
||||
onProductRecommendationImpression = { aid ->
|
||||
store.dispatch(ReviewQualityCheckAction.RecommendedProductImpression(productAid = aid))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is ReviewQualityCheckState.Initial -> {}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isOptedIn) {
|
||||
if (isOptedIn) {
|
||||
store.dispatch(ReviewQualityCheckAction.FetchProductAnalysis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
private fun ProductReview(
|
||||
state: ReviewQualityCheckState.OptedIn,
|
||||
onOptOutClick: () -> Unit,
|
||||
onAnalyzeClick: () -> Unit,
|
||||
onReanalyzeClick: () -> Unit,
|
||||
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
|
||||
onHighlightsExpandToggleClick: () -> Unit,
|
||||
onNoAnalysisPresent: () -> Unit,
|
||||
onSettingsExpandToggleClick: () -> Unit,
|
||||
onInfoExpandToggleClick: () -> Unit,
|
||||
onReviewGradeLearnMoreClick: () -> Unit,
|
||||
onFooterLinkClick: () -> Unit,
|
||||
onRecommendedProductClick: (aid: String, url: String) -> Unit,
|
||||
onProductRecommendationImpression: (aid: String) -> Unit,
|
||||
onReportBackInStockClick: () -> Unit,
|
||||
) {
|
||||
when (val productReviewState = state.productReviewState) {
|
||||
is AnalysisPresent -> {
|
||||
ProductAnalysis(
|
||||
productRecommendationsEnabled = state.productRecommendationsPreference,
|
||||
productAnalysis = productReviewState,
|
||||
productVendor = state.productVendor,
|
||||
isSettingsExpanded = state.isSettingsExpanded,
|
||||
isInfoExpanded = state.isInfoExpanded,
|
||||
isHighlightsExpanded = state.isHighlightsExpanded,
|
||||
onOptOutClick = onOptOutClick,
|
||||
onReanalyzeClick = onReanalyzeClick,
|
||||
onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange,
|
||||
onHighlightsExpandToggleClick = onHighlightsExpandToggleClick,
|
||||
onSettingsExpandToggleClick = onSettingsExpandToggleClick,
|
||||
onInfoExpandToggleClick = onInfoExpandToggleClick,
|
||||
onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick,
|
||||
onFooterLinkClick = onFooterLinkClick,
|
||||
onRecommendedProductClick = onRecommendedProductClick,
|
||||
onRecommendedProductImpression = onProductRecommendationImpression,
|
||||
)
|
||||
}
|
||||
|
||||
is ReviewQualityCheckState.OptedIn.ProductReviewState.Error -> {
|
||||
ProductAnalysisError(
|
||||
error = productReviewState,
|
||||
onReportBackInStockClick = onReportBackInStockClick,
|
||||
productRecommendationsEnabled = state.productRecommendationsPreference,
|
||||
productVendor = state.productVendor,
|
||||
isSettingsExpanded = state.isSettingsExpanded,
|
||||
isInfoExpanded = state.isInfoExpanded,
|
||||
onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick,
|
||||
onOptOutClick = onOptOutClick,
|
||||
onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange,
|
||||
onFooterLinkClick = onFooterLinkClick,
|
||||
onSettingsExpandToggleClick = onSettingsExpandToggleClick,
|
||||
onInfoExpandToggleClick = onInfoExpandToggleClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
is ReviewQualityCheckState.OptedIn.ProductReviewState.Loading -> {
|
||||
ProductReviewLoading()
|
||||
}
|
||||
|
||||
is ReviewQualityCheckState.OptedIn.ProductReviewState.NoAnalysisPresent -> {
|
||||
LaunchedEffect(Unit) {
|
||||
onNoAnalysisPresent()
|
||||
}
|
||||
|
||||
NoAnalysis(
|
||||
noAnalysisPresent = productReviewState,
|
||||
onAnalyzeClick = onAnalyzeClick,
|
||||
productRecommendationsEnabled = state.productRecommendationsPreference,
|
||||
productVendor = state.productVendor,
|
||||
isSettingsExpanded = state.isSettingsExpanded,
|
||||
isInfoExpanded = state.isInfoExpanded,
|
||||
onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick,
|
||||
onOptOutClick = onOptOutClick,
|
||||
onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange,
|
||||
onSettingsExpandToggleClick = onSettingsExpandToggleClick,
|
||||
onInfoExpandToggleClick = onInfoExpandToggleClick,
|
||||
onFooterLinkClick = onFooterLinkClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.InfoCardContainer
|
||||
import org.mozilla.fenix.compose.LinkText
|
||||
import org.mozilla.fenix.compose.LinkTextState
|
||||
import org.mozilla.fenix.compose.button.PrimaryButton
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor
|
||||
import org.mozilla.fenix.shopping.ui.ext.displayName
|
||||
import org.mozilla.fenix.shopping.ui.ext.headingResource
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
private const val MAX_SUPPORTED_VENDORS_PER_TLD = 3
|
||||
|
||||
/**
|
||||
* A placeholder UI for review quality check contextual onboarding. The actual UI will be
|
||||
* implemented as part of Bug 1840103 with the illustration.
|
||||
*
|
||||
* @param productVendors List of retailers to be displayed in order.
|
||||
* @param onLearnMoreClick Invoked when a user clicks on the learn more link.
|
||||
* @param onPrivacyPolicyClick Invoked when a user clicks on the privacy policy link.
|
||||
* @param onTermsOfUseClick Invoked when a user clicks on the terms of use link.
|
||||
* @param onPrimaryButtonClick Invoked when a user clicks on the primary button.
|
||||
* @param onSecondaryButtonClick Invoked when a user clicks on the secondary button.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun ReviewQualityCheckContextualOnboarding(
|
||||
productVendors: List<ProductVendor>,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onPrivacyPolicyClick: () -> Unit,
|
||||
onTermsOfUseClick: () -> Unit,
|
||||
onPrimaryButtonClick: () -> Unit,
|
||||
onSecondaryButtonClick: () -> Unit,
|
||||
) {
|
||||
val learnMoreText =
|
||||
stringResource(id = R.string.review_quality_check_contextual_onboarding_learn_more_link)
|
||||
val privacyPolicyText =
|
||||
stringResource(id = R.string.review_quality_check_contextual_onboarding_privacy_policy_3)
|
||||
val termsOfUseText =
|
||||
stringResource(id = R.string.review_quality_check_contextual_onboarding_terms_use)
|
||||
val titleContentDescription =
|
||||
headingResource(R.string.review_quality_check_contextual_onboarding_title)
|
||||
|
||||
InfoCardContainer(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = stringResource(R.string.review_quality_check_contextual_onboarding_title),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.headline5,
|
||||
modifier = Modifier.semantics {
|
||||
heading()
|
||||
contentDescription = titleContentDescription
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = createDescriptionString(productVendors),
|
||||
color = FirefoxTheme.colors.textSecondary,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LinkText(
|
||||
text = stringResource(
|
||||
id = R.string.review_quality_check_contextual_onboarding_learn_more,
|
||||
stringResource(id = R.string.shopping_product_name),
|
||||
learnMoreText,
|
||||
),
|
||||
linkTextStates = listOf(
|
||||
LinkTextState(
|
||||
text = learnMoreText,
|
||||
url = "",
|
||||
onClick = {
|
||||
onLearnMoreClick()
|
||||
},
|
||||
),
|
||||
),
|
||||
style = FirefoxTheme.typography.body2.copy(
|
||||
color = FirefoxTheme.colors.textSecondary,
|
||||
),
|
||||
linkTextDecoration = TextDecoration.Underline,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LinkText(
|
||||
text = stringResource(
|
||||
id = R.string.review_quality_check_contextual_onboarding_caption_4,
|
||||
stringResource(id = R.string.firefox),
|
||||
privacyPolicyText,
|
||||
stringResource(id = R.string.shopping_product_name),
|
||||
termsOfUseText,
|
||||
),
|
||||
linkTextStates = listOf(
|
||||
LinkTextState(
|
||||
text = privacyPolicyText,
|
||||
url = "",
|
||||
onClick = {
|
||||
onPrivacyPolicyClick()
|
||||
},
|
||||
),
|
||||
LinkTextState(
|
||||
text = termsOfUseText,
|
||||
url = "",
|
||||
onClick = {
|
||||
onTermsOfUseClick()
|
||||
},
|
||||
),
|
||||
),
|
||||
style = FirefoxTheme.typography.caption
|
||||
.copy(
|
||||
color = FirefoxTheme.colors.textSecondary,
|
||||
),
|
||||
linkTextDecoration = TextDecoration.Underline,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.shopping_onboarding),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(all = 16.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
PrimaryButton(
|
||||
text = stringResource(R.string.review_quality_check_contextual_onboarding_primary_button_text),
|
||||
onClick = onPrimaryButtonClick,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
TextButton(
|
||||
onClick = onSecondaryButtonClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.review_quality_check_contextual_onboarding_secondary_button_text),
|
||||
color = FirefoxTheme.colors.textAccent,
|
||||
style = FirefoxTheme.typography.button,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun createDescriptionString(
|
||||
retailers: List<ProductVendor>,
|
||||
) = buildAnnotatedString {
|
||||
val retailerNames = retailers.map { it.displayName() }
|
||||
|
||||
val description = if (retailers.size == MAX_SUPPORTED_VENDORS_PER_TLD) {
|
||||
stringResource(
|
||||
id = R.string.review_quality_check_contextual_onboarding_description,
|
||||
retailerNames[0],
|
||||
stringResource(R.string.app_name),
|
||||
retailerNames[1],
|
||||
retailerNames[2],
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id = R.string.review_quality_check_contextual_onboarding_description_one_vendor,
|
||||
retailerNames.first(),
|
||||
stringResource(R.string.app_name),
|
||||
)
|
||||
}
|
||||
|
||||
append(description)
|
||||
|
||||
retailerNames.forEach { retailer ->
|
||||
val start = description.indexOf(retailer)
|
||||
|
||||
addStyle(
|
||||
style = SpanStyle(fontWeight = FontWeight.Bold),
|
||||
start = start,
|
||||
end = start + retailer.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun ProductAnalysisPreview() {
|
||||
FirefoxTheme {
|
||||
ReviewQualityCheckScaffold(
|
||||
onRequestDismiss = {},
|
||||
) {
|
||||
ReviewQualityCheckContextualOnboarding(
|
||||
productVendors = ReviewQualityCheckState.NotOptedIn().productVendors,
|
||||
onPrimaryButtonClick = {},
|
||||
onLearnMoreClick = {},
|
||||
onPrivacyPolicyClick = {},
|
||||
onTermsOfUseClick = {},
|
||||
onSecondaryButtonClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.LinkText
|
||||
import org.mozilla.fenix.compose.LinkTextState
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Review Quality Check footer with an embedded link to navigate to Fakespot.com.
|
||||
*
|
||||
* @param onLinkClick Invoked when the user clicks on the embedded link.
|
||||
*/
|
||||
@Composable
|
||||
fun ReviewQualityCheckFooter(
|
||||
onLinkClick: () -> Unit,
|
||||
) {
|
||||
val poweredByLinkText = stringResource(
|
||||
id = R.string.review_quality_check_powered_by_link,
|
||||
stringResource(id = R.string.shopping_product_name),
|
||||
)
|
||||
|
||||
LinkText(
|
||||
text = stringResource(
|
||||
id = R.string.review_quality_check_powered_by_2,
|
||||
poweredByLinkText,
|
||||
),
|
||||
linkTextStates = listOf(
|
||||
LinkTextState(
|
||||
text = poweredByLinkText,
|
||||
url = "",
|
||||
onClick = {
|
||||
onLinkClick()
|
||||
},
|
||||
),
|
||||
),
|
||||
style = FirefoxTheme.typography.body2.copy(
|
||||
color = FirefoxTheme.colors.textSecondary,
|
||||
),
|
||||
linkTextColor = FirefoxTheme.colors.textAccent,
|
||||
)
|
||||
}
|
||||
|
||||
@LightDarkPreview
|
||||
@Composable
|
||||
private fun ReviewQualityCheckFooterPreview() {
|
||||
FirefoxTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = FirefoxTheme.colors.layer1)
|
||||
.padding(all = 16.dp),
|
||||
) {
|
||||
ReviewQualityCheckFooter(
|
||||
onLinkClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.traversalIndex
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.BetaLabel
|
||||
import org.mozilla.fenix.compose.BottomSheetHandle
|
||||
import org.mozilla.fenix.shopping.ui.ext.headingResource
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
private val bottomSheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||
private const val BOTTOM_SHEET_HANDLE_WIDTH_PERCENT = 0.1f
|
||||
|
||||
/**
|
||||
* A scaffold for review quality check UI that implements the basic layout structure with
|
||||
* [BottomSheetHandle], [Header] and [content].
|
||||
*
|
||||
* @param onRequestDismiss Invoked when a user action requests dismissal of the bottom sheet.
|
||||
* @param modifier The modifier to be applied to the Composable.
|
||||
* @param content The content of the bottom sheet.
|
||||
*/
|
||||
@Composable
|
||||
fun ReviewQualityCheckScaffold(
|
||||
onRequestDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
color = FirefoxTheme.colors.layer1,
|
||||
shape = bottomSheetShape,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = FirefoxTheme.colors.layer1,
|
||||
shape = bottomSheetShape,
|
||||
)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(
|
||||
vertical = 8.dp,
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
) {
|
||||
BottomSheetHandle(
|
||||
onRequestDismiss = onRequestDismiss,
|
||||
contentDescription = stringResource(R.string.review_quality_check_close_handle_content_description),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(BOTTOM_SHEET_HANDLE_WIDTH_PERCENT)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.semantics { traversalIndex = -1f },
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Header()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
content()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header() {
|
||||
val reviewCheckerFeatureName = stringResource(R.string.review_quality_check_feature_name_2)
|
||||
val betaText = stringResource(R.string.beta_feature)
|
||||
val titleContentDescription = headingResource("$reviewCheckerFeatureName, $betaText")
|
||||
|
||||
Row(
|
||||
modifier = Modifier.semantics(mergeDescendants = true) {
|
||||
heading()
|
||||
contentDescription = titleContentDescription
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = reviewCheckerFeatureName,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.headline6,
|
||||
modifier = Modifier.clearAndSetSemantics {},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
BetaLabel(modifier = Modifier.clearAndSetSemantics {})
|
||||
}
|
||||
}
|
||||
|
||||
@LightDarkPreview
|
||||
@Composable
|
||||
private fun HeaderPreview() {
|
||||
FirefoxTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(color = FirefoxTheme.colors.layer1)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Header()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.ExpandableInfoCardContainer
|
||||
import org.mozilla.fenix.compose.SwitchWithLabel
|
||||
import org.mozilla.fenix.compose.button.SecondaryButton
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Review quality check settings card UI. Contains toggles to disable product recommendations and
|
||||
* the entire review quality check feature.
|
||||
*
|
||||
* @param productRecommendationsEnabled The current state of the product recommendations toggle.
|
||||
* @param isExpanded Whether or not the settings card is expanded.
|
||||
* @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product
|
||||
* recommendations toggle state.
|
||||
* @param onTurnOffReviewQualityCheckClick Invoked when the user opts out of the review quality check feature.
|
||||
* @param onExpandToggleClick Invoked when the user expands or collapses the settings card.
|
||||
* @param modifier Modifier to apply to the layout.
|
||||
*/
|
||||
@Composable
|
||||
fun ReviewQualityCheckSettingsCard(
|
||||
productRecommendationsEnabled: Boolean?,
|
||||
isExpanded: Boolean,
|
||||
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
|
||||
onTurnOffReviewQualityCheckClick: () -> Unit,
|
||||
onExpandToggleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExpandableInfoCardContainer(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.review_quality_check_settings_title),
|
||||
isExpanded = isExpanded,
|
||||
onExpandToggleClick = onExpandToggleClick,
|
||||
) {
|
||||
SettingsContent(
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange,
|
||||
onTurnOffReviewQualityCheckClick = onTurnOffReviewQualityCheckClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsContent(
|
||||
productRecommendationsEnabled: Boolean?,
|
||||
onProductRecommendationsEnabledStateChange: (Boolean) -> Unit,
|
||||
onTurnOffReviewQualityCheckClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (productRecommendationsEnabled != null) {
|
||||
SwitchWithLabel(
|
||||
label = stringResource(R.string.review_quality_check_settings_recommended_products),
|
||||
checked = productRecommendationsEnabled,
|
||||
onCheckedChange = onProductRecommendationsEnabledStateChange,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.review_quality_check_settings_turn_off),
|
||||
onClick = onTurnOffReviewQualityCheckClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@LightDarkPreview
|
||||
@Composable
|
||||
private fun ReviewQualityCheckSettingsCardPreview() {
|
||||
FirefoxTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = FirefoxTheme.colors.layer1)
|
||||
.padding(all = 16.dp),
|
||||
) {
|
||||
var isSettingsExpanded by remember { mutableStateOf(true) }
|
||||
var productRecommendationsEnabled by remember { mutableStateOf(true) }
|
||||
|
||||
ReviewQualityCheckSettingsCard(
|
||||
productRecommendationsEnabled = productRecommendationsEnabled,
|
||||
onProductRecommendationsEnabledStateChange = { productRecommendationsEnabled = it },
|
||||
onTurnOffReviewQualityCheckClick = {},
|
||||
isExpanded = isSettingsExpanded,
|
||||
onExpandToggleClick = { isSettingsExpanded = !isSettingsExpanded },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.ExpandableInfoCardContainer
|
||||
import org.mozilla.fenix.compose.LinkText
|
||||
import org.mozilla.fenix.compose.LinkTextState
|
||||
import org.mozilla.fenix.compose.parseHtml
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.ui.ext.displayName
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Info card UI containing an explanation of the review quality.
|
||||
*
|
||||
* @param productVendor The vendor of the product.
|
||||
* @param isExpanded Whether or not the card is expanded.
|
||||
* @param modifier Modifier to apply to the layout.
|
||||
* @param onExpandToggleClick Invoked when the user expands or collapses the card.
|
||||
* @param onLearnMoreClick Invoked when the user clicks to learn more about review grades.
|
||||
*/
|
||||
@Composable
|
||||
fun ReviewQualityInfoCard(
|
||||
productVendor: ReviewQualityCheckState.ProductVendor,
|
||||
isExpanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onExpandToggleClick: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
) {
|
||||
ExpandableInfoCardContainer(
|
||||
title = stringResource(id = R.string.review_quality_check_explanation_title),
|
||||
modifier = modifier,
|
||||
isExpanded = isExpanded,
|
||||
onExpandToggleClick = onExpandToggleClick,
|
||||
) {
|
||||
ReviewQualityInfo(
|
||||
productVendor = productVendor,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ReviewQualityInfo(
|
||||
productVendor: ReviewQualityCheckState.ProductVendor,
|
||||
modifier: Modifier = Modifier,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
val letterGradeText =
|
||||
stringResource(id = R.string.review_quality_check_info_review_grade_header)
|
||||
val adjustedGradingText =
|
||||
stringResource(id = R.string.review_quality_check_explanation_body_adjusted_grading)
|
||||
val highlightsText = stringResource(
|
||||
id = R.string.review_quality_check_explanation_body_highlights,
|
||||
productVendor.displayName(),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.review_quality_check_explanation_body_reliability,
|
||||
stringResource(R.string.shopping_product_name),
|
||||
),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = remember(letterGradeText) { parseHtml(letterGradeText) },
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
|
||||
ReviewGradingScaleInfo(
|
||||
reviewGrades = listOf(
|
||||
ReviewQualityCheckState.Grade.A,
|
||||
ReviewQualityCheckState.Grade.B,
|
||||
),
|
||||
info = stringResource(id = R.string.review_quality_check_info_grade_info_AB),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
ReviewGradingScaleInfo(
|
||||
reviewGrades = listOf(ReviewQualityCheckState.Grade.C),
|
||||
info = stringResource(id = R.string.review_quality_check_info_grade_info_C),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
ReviewGradingScaleInfo(
|
||||
reviewGrades = listOf(
|
||||
ReviewQualityCheckState.Grade.D,
|
||||
ReviewQualityCheckState.Grade.F,
|
||||
),
|
||||
info = stringResource(id = R.string.review_quality_check_info_grade_info_DF),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = remember(adjustedGradingText) { parseHtml(adjustedGradingText) },
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = remember(highlightsText) { parseHtml(highlightsText) },
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
|
||||
val link = stringResource(
|
||||
id = R.string.review_quality_check_info_learn_more_link_2,
|
||||
stringResource(R.string.shopping_product_name),
|
||||
)
|
||||
val text = stringResource(R.string.review_quality_check_info_learn_more, link)
|
||||
LinkText(
|
||||
text = text,
|
||||
linkTextStates = listOf(
|
||||
LinkTextState(
|
||||
text = link,
|
||||
url = "",
|
||||
onClick = {
|
||||
onLearnMoreClick()
|
||||
},
|
||||
),
|
||||
),
|
||||
style = FirefoxTheme.typography.body2.copy(
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
),
|
||||
linkTextColor = FirefoxTheme.colors.textAccent,
|
||||
linkTextDecoration = TextDecoration.Underline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewGradingScaleInfo(
|
||||
reviewGrades: List<ReviewQualityCheckState.Grade>,
|
||||
info: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.semantics(mergeDescendants = true) {},
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
reviewGrades.forEach { grade ->
|
||||
ReviewGradeCompact(grade = grade)
|
||||
}
|
||||
|
||||
if (reviewGrades.size == 1) {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = info,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun ReviewQualityInfoCardPreview() {
|
||||
FirefoxTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = FirefoxTheme.colors.layer1)
|
||||
.padding(all = 16.dp),
|
||||
) {
|
||||
var isInfoExpanded by remember { mutableStateOf(true) }
|
||||
|
||||
ReviewQualityInfoCard(
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
isExpanded = isInfoExpanded,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onLearnMoreClick = {},
|
||||
onExpandToggleClick = { isInfoExpanded = !isInfoExpanded },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.round
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val NUM_STARS = 5
|
||||
private const val MAX_RATING = 5f
|
||||
private const val MIN_RATING = 0f
|
||||
|
||||
/**
|
||||
* UI for displaying star rating bar with maximum 5 stars.
|
||||
*
|
||||
* @param value The rating to be displayed as filled stars.
|
||||
* @param modifier The modifier to be applied to the composable.
|
||||
*/
|
||||
@Composable
|
||||
fun StarRating(
|
||||
value: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val rating: Float = remember(value) {
|
||||
max(min(MAX_RATING, value), MIN_RATING).roundToNearestHalf()
|
||||
}
|
||||
val contentDescription = contentDescription(rating = value)
|
||||
|
||||
Row(
|
||||
modifier = modifier.semantics {
|
||||
this.contentDescription = contentDescription
|
||||
},
|
||||
) {
|
||||
repeat(NUM_STARS) {
|
||||
val starId: Int
|
||||
val colorFilter: ColorFilter?
|
||||
if (it < rating && it + 1 > rating) {
|
||||
starId = R.drawable.mozac_ic_star_one_half_fill_20
|
||||
colorFilter = null // use the colors values in the vector
|
||||
} else if (it < rating) {
|
||||
starId = R.drawable.mozac_ic_star_fill_20
|
||||
colorFilter = ColorFilter.tint(colorResource(id = R.color.mozac_ic_star_filled))
|
||||
} else {
|
||||
starId = R.drawable.mozac_ic_star_fill_20
|
||||
colorFilter = ColorFilter.tint(colorResource(id = R.color.mozac_ic_star_unfilled))
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = starId),
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun contentDescription(rating: Float): String {
|
||||
val formattedRating: Number = remember(rating) { rating.removeDecimalZero() }
|
||||
|
||||
return stringResource(
|
||||
R.string.review_quality_check_star_rating_content_description,
|
||||
formattedRating,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes decimal zero if present and returns an Int, otherwise returns the same float as Number.
|
||||
* e.g.4.0 becomes 4, 3.4 stays 3.4.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun Float.removeDecimalZero(): Number =
|
||||
if (this % 1 == 0f) {
|
||||
roundToInt()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the float to the nearest half instead of the whole number.
|
||||
* e.g 4.6 becomes 4.5 instead of 5.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun Float.roundToNearestHalf(): Float =
|
||||
round(this * 2) / 2f
|
||||
|
||||
@LightDarkPreview
|
||||
@Composable
|
||||
@Suppress("MagicNumber")
|
||||
private fun StarRatingPreview() {
|
||||
FirefoxTheme {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(FirefoxTheme.colors.layer1)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
listOf(0.4f, 0.9f, 1.6f, 2.2f, 3f, 3.6f, 4.1f, 4.65f, 4.9f).forEach {
|
||||
StarRating(value = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui.ext
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor
|
||||
|
||||
/**
|
||||
* Returns the display string corresponding to the particular [ProductVendor].
|
||||
*/
|
||||
@Composable
|
||||
fun ProductVendor.displayName(): String = when (this) {
|
||||
ProductVendor.AMAZON -> stringResource(id = R.string.review_quality_check_retailer_name_amazon)
|
||||
ProductVendor.BEST_BUY -> stringResource(id = R.string.review_quality_check_retailer_name_bestbuy)
|
||||
ProductVendor.WALMART -> stringResource(id = R.string.review_quality_check_retailer_name_walmart)
|
||||
}
|
||||
@@ -1750,22 +1750,6 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Indicates if the review quality check feature is enabled by the user.
|
||||
*/
|
||||
var isReviewQualityCheckEnabled by booleanPreference(
|
||||
key = appContext.getPreferenceKey(R.string.pref_key_is_review_quality_check_enabled),
|
||||
default = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Indicates if the review quality check product recommendations option is enabled by the user.
|
||||
*/
|
||||
var isReviewQualityCheckProductRecommendationsEnabled by booleanPreference(
|
||||
key = appContext.getPreferenceKey(R.string.pref_key_is_review_quality_check_product_recommendations_enabled),
|
||||
default = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Indicates if the navigation bar CFR should be displayed to the user.
|
||||
*/
|
||||
@@ -1798,14 +1782,6 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
||||
default = true,
|
||||
)
|
||||
|
||||
/**
|
||||
* Time in milliseconds since the user first opted in the review quality check feature.
|
||||
*/
|
||||
var reviewQualityCheckOptInTimeInMillis by longPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_should_show_review_quality_opt_in_time),
|
||||
default = 0L,
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the current mode for how https-only is enabled.
|
||||
*/
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 90 KiB |
@@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13.452,2H6v1.75h7.088l6.629,6.656 1.24,-1.234 -6.885,-6.914a0.875,0.875 0,0 0,-0.62 -0.258zM8.933,12.379l1.414,-1.415L8.933,9.55l-1.415,1.414 1.415,1.415z" >
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:startX="24"
|
||||
android:startY="0"
|
||||
android:endX="0"
|
||||
android:endY="24"
|
||||
android:startColor="@color/fx_mobile_icon_color_gradient_start"
|
||||
android:endColor="@color/fx_mobile_icon_color_gradient_end"/>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M3.5,6.375c0,-0.483 0.392,-0.875 0.875,-0.875h7.265c0.232,0 0.455,0.092 0.619,0.256l6.735,6.735a2.5,2.5 0,0 1,0 3.536l-5.215,5.215a2.5,2.5 0,0 1,-3.536 0l-6.487,-6.487a0.875,0.875 0,0 1,-0.256 -0.619L3.5,6.375zM5.25,7.25v6.524l6.23,6.23a0.75,0.75 0,0 0,1.061 0l5.215,-5.215a0.75,0.75 0,0 0,0 -1.06L11.278,7.25L5.25,7.25z" >
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:startX="24"
|
||||
android:startY="0"
|
||||
android:endX="0"
|
||||
android:endY="24"
|
||||
android:startColor="@color/fx_mobile_icon_color_gradient_start"
|
||||
android:endColor="@color/fx_mobile_icon_color_gradient_end"/>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
@@ -178,10 +178,6 @@
|
||||
<color name="mozac_ui_tabcounter_default_tint" tools:ignore="UnusedResources">@color/fx_mobile_icon_color_primary</color>
|
||||
<color name="mozac_ui_tabcounter_default_text" tools:ignore="UnusedResources">@color/fx_mobile_text_color_primary</color>
|
||||
|
||||
<!-- Star icon fill colors -->
|
||||
<color name="mozac_ic_star_filled">@color/photonLightGrey40</color>
|
||||
<color name="mozac_ic_star_unfilled">@color/photonDarkGrey05</color>
|
||||
|
||||
<!-- Private Mode mask icon circle fill colors -->
|
||||
<color name="mozac_ui_private_mode_circle_fill" tools:ignore="UnusedResources">@color/photonPurple60</color>
|
||||
</resources>
|
||||
|
||||
@@ -372,10 +372,6 @@
|
||||
<color name="mozac_feature_addons_messagebar_error_background_color" tools:ignore="UnusedResources">@color/fx_mobile_layer_color_critical</color>
|
||||
<color name="mozac_feature_addons_messagebar_warning_background_color" tools:ignore="UnusedResources">@color/fx_mobile_layer_color_warning</color>
|
||||
|
||||
<!-- Star icon fill colors -->
|
||||
<color name="mozac_ic_star_filled">@color/photonBlack</color>
|
||||
<color name="mozac_ic_star_unfilled">#D9D9D9</color>
|
||||
|
||||
<!-- Private Mode mask icon circle fill colors -->
|
||||
<color name="mozac_ui_private_mode_circle_fill" tools:ignore="UnusedResources">@color/photonPurple60</color>
|
||||
</resources>
|
||||
|
||||
@@ -397,11 +397,6 @@
|
||||
<string name="pref_key_growth_resume_last_sent" translatable="false">pref_key_growth_last_resumed</string>
|
||||
<string name="pref_key_growth_uri_load_last_sent" translatable="false">pref_key_growth_uri_load_last_sent</string>
|
||||
|
||||
<!--Shopping -->
|
||||
<string name="pref_key_is_review_quality_check_enabled">pref_key_is_review_quality_check_enabled</string>
|
||||
<string name="pref_key_is_review_quality_check_product_recommendations_enabled">pref_key_is_review_quality_check_product_recommendations_enabled</string>
|
||||
<string name="pref_key_should_show_review_quality_opt_in_time">pref_key_should_show_review_quality_opt_in_time</string>
|
||||
|
||||
<!--Translations -->
|
||||
<string name="pref_key_ignore_translations_data_saver_warning" translatable="false">pref_key_ignore_translations_data_saver_warning</string>
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
<string name="app_services_abbreviation" translatable="false">AS</string>
|
||||
<!-- Name for the Pocket product -->
|
||||
<string name="pocket_product_name" translatable="false">Pocket</string>
|
||||
<!-- Name for the Fakespot product -->
|
||||
<string name="shopping_product_name" translatable="false">Fakespot</string>
|
||||
|
||||
<!-- App preference label for keeping the debug menu (secret settings, secret debug info, nimbus experiments, etc.) permanently revealed -->
|
||||
<string name="preferences_persistent_debug_menu" translatable="false">Keep Debug Menu revealed</string>
|
||||
@@ -147,10 +145,6 @@
|
||||
|
||||
<string name="profiler_uploaded_url_to_clipboard">URL copied to clipboard successfully</string>
|
||||
|
||||
<string name="review_quality_check_retailer_name_amazon">Amazon</string>
|
||||
<string name="review_quality_check_retailer_name_bestbuy">Best Buy</string>
|
||||
<string name="review_quality_check_retailer_name_walmart">Walmart</string>
|
||||
|
||||
<!-- Debug drawer "contextual feature recommendation" (CFR) tools -->
|
||||
<!-- The description of the reset CFR section in CFR Tools -->
|
||||
<string name="debug_drawer_cfr_tools_reset_cfr_description">Toggle off a CFR to reset it. Toggle on a CFR to mark it as shown.</string>
|
||||
|
||||
@@ -2438,149 +2438,149 @@
|
||||
|
||||
<!-- Review quality check feature-->
|
||||
<!-- Name for the review quality check feature used as title for the panel. -->
|
||||
<string name="review_quality_check_feature_name_2">Review Checker</string>
|
||||
<string name="review_quality_check_feature_name_2" moz:removedIn="136" tools:ignore="UnusedResources">Review Checker</string>
|
||||
<!-- Summary for grades A and B for review quality check adjusted grading. -->
|
||||
<string name="review_quality_check_grade_a_b_description">Reliable reviews</string>
|
||||
<string name="review_quality_check_grade_a_b_description" moz:removedIn="136" tools:ignore="UnusedResources">Reliable reviews</string>
|
||||
<!-- Summary for grade C for review quality check adjusted grading. -->
|
||||
<string name="review_quality_check_grade_c_description">Mix of reliable and unreliable reviews</string>
|
||||
<string name="review_quality_check_grade_c_description" moz:removedIn="136" tools:ignore="UnusedResources">Mix of reliable and unreliable reviews</string>
|
||||
<!-- Summary for grades D and F for review quality check adjusted grading. -->
|
||||
<string name="review_quality_check_grade_d_f_description">Unreliable reviews</string>
|
||||
<string name="review_quality_check_grade_d_f_description" moz:removedIn="136" tools:ignore="UnusedResources">Unreliable reviews</string>
|
||||
<!-- Text for title presenting the reliability of a product's reviews. -->
|
||||
<string name="review_quality_check_grade_title">How reliable are these reviews?</string>
|
||||
<string name="review_quality_check_grade_title" moz:removedIn="136" tools:ignore="UnusedResources">How reliable are these reviews?</string>
|
||||
<!-- Title for when the rating has been updated by the review checker -->
|
||||
<string name="review_quality_check_adjusted_rating_title">Adjusted rating</string>
|
||||
<string name="review_quality_check_adjusted_rating_title" moz:removedIn="136" tools:ignore="UnusedResources">Adjusted rating</string>
|
||||
<!-- Description for a product's adjusted star rating. The text presents that the product's reviews which were evaluated as unreliable were removed from the adjusted rating. -->
|
||||
<string name="review_quality_check_adjusted_rating_description_2">Based on reliable reviews</string>
|
||||
<string name="review_quality_check_adjusted_rating_description_2" moz:removedIn="136" tools:ignore="UnusedResources">Based on reliable reviews</string>
|
||||
<!-- Title for list of highlights from a product's review emphasizing a product's important traits. -->
|
||||
<string name="review_quality_check_highlights_title">Highlights from recent reviews</string>
|
||||
<string name="review_quality_check_highlights_title" moz:removedIn="136" tools:ignore="UnusedResources">Highlights from recent reviews</string>
|
||||
<!-- Title for section explaining how we analyze the reliability of a product's reviews. -->
|
||||
<string name="review_quality_check_explanation_title">How we determine review quality</string>
|
||||
<string name="review_quality_check_explanation_title" moz:removedIn="136" tools:ignore="UnusedResources">How we determine review quality</string>
|
||||
<!-- Paragraph explaining how we analyze the reliability of a product's reviews. First parameter is the Fakespot product name. In the phrase "Fakespot by Mozilla", "by" can be localized. Does not need to stay by. -->
|
||||
<string name="review_quality_check_explanation_body_reliability">We use AI technology from %s by Mozilla to check the reliability of product reviews. This will only help you assess review quality, not product quality. </string>
|
||||
<string name="review_quality_check_explanation_body_reliability" moz:removedIn="136" tools:ignore="UnusedResources">We use AI technology from %s by Mozilla to check the reliability of product reviews. This will only help you assess review quality, not product quality. </string>
|
||||
<!-- Paragraph explaining the grading system we use to classify the reliability of a product's reviews. -->
|
||||
<string name="review_quality_check_info_review_grade_header"><![CDATA[We assign each product’s reviews a <b>letter grade</b> from A to F.]]></string>
|
||||
<string name="review_quality_check_info_review_grade_header" moz:removedIn="136" tools:ignore="UnusedResources"><![CDATA[We assign each product’s reviews a <b>letter grade</b> from A to F.]]></string>
|
||||
<!-- Description explaining grades A and B for review quality check adjusted grading. -->
|
||||
<string name="review_quality_check_info_grade_info_AB">Reliable reviews. We believe the reviews are likely from real customers who left honest, unbiased reviews.</string>
|
||||
<string name="review_quality_check_info_grade_info_AB" moz:removedIn="136" tools:ignore="UnusedResources">Reliable reviews. We believe the reviews are likely from real customers who left honest, unbiased reviews.</string>
|
||||
<!-- Description explaining grade C for review quality check adjusted grading. -->
|
||||
<string name="review_quality_check_info_grade_info_C">We believe there’s a mix of reliable and unreliable reviews.</string>
|
||||
<string name="review_quality_check_info_grade_info_C" moz:removedIn="136" tools:ignore="UnusedResources">We believe there’s a mix of reliable and unreliable reviews.</string>
|
||||
<!-- Description explaining grades D and F for review quality check adjusted grading. -->
|
||||
<string name="review_quality_check_info_grade_info_DF">Unreliable reviews. We believe the reviews are likely fake or from biased reviewers.</string>
|
||||
<string name="review_quality_check_info_grade_info_DF" moz:removedIn="136" tools:ignore="UnusedResources">Unreliable reviews. We believe the reviews are likely fake or from biased reviewers.</string>
|
||||
<!-- Paragraph explaining how a product's adjusted grading is calculated. -->
|
||||
<string name="review_quality_check_explanation_body_adjusted_grading"><![CDATA[The <b>adjusted rating</b> is based only on reviews we believe to be reliable.]]></string>
|
||||
<string name="review_quality_check_explanation_body_adjusted_grading" moz:removedIn="136" tools:ignore="UnusedResources"><![CDATA[The <b>adjusted rating</b> is based only on reviews we believe to be reliable.]]></string>
|
||||
<!-- Paragraph explaining product review highlights. First parameter is the name of the retailer (e.g. Amazon). -->
|
||||
<string name="review_quality_check_explanation_body_highlights"><![CDATA[<b>Highlights</b> are from %s reviews within the last 80 days that we believe to be reliable.]]></string>
|
||||
<string name="review_quality_check_explanation_body_highlights" moz:removedIn="136" tools:ignore="UnusedResources"><![CDATA[<b>Highlights</b> are from %s reviews within the last 80 days that we believe to be reliable.]]></string>
|
||||
<!-- Text for learn more caption presenting a link with information about review quality. First parameter is for clickable text defined in review_quality_check_info_learn_more_link. -->
|
||||
<string name="review_quality_check_info_learn_more">Learn more about %s.</string>
|
||||
<string name="review_quality_check_info_learn_more" moz:removedIn="136" tools:ignore="UnusedResources">Learn more about %s.</string>
|
||||
<!-- Clickable text that links to review quality check SuMo page. First parameter is the Fakespot product name. -->
|
||||
<string name="review_quality_check_info_learn_more_link_2">how %s determines review quality</string>
|
||||
<string name="review_quality_check_info_learn_more_link_2" moz:removedIn="136" tools:ignore="UnusedResources">how %s determines review quality</string>
|
||||
<!-- Text for title of settings section. -->
|
||||
<string name="review_quality_check_settings_title">Settings</string>
|
||||
<string name="review_quality_check_settings_title" moz:removedIn="136" tools:ignore="UnusedResources">Settings</string>
|
||||
<!-- Text for label for switch preference to show recommended products from review quality check settings section. -->
|
||||
<string name="review_quality_check_settings_recommended_products">Show ads in review checker</string>
|
||||
<string name="review_quality_check_settings_recommended_products" moz:removedIn="136" tools:ignore="UnusedResources">Show ads in review checker</string>
|
||||
<!-- Description for switch preference to show recommended products from review quality check settings section. First parameter is for clickable text defined in review_quality_check_settings_recommended_products_learn_more.-->
|
||||
<string name="review_quality_check_settings_recommended_products_description_2" tools:ignore="UnusedResources">You’ll see occasional ads for relevant products. We only advertise products with reliable reviews. %s</string>
|
||||
<string name="review_quality_check_settings_recommended_products_description_2" moz:removedIn="136" tools:ignore="UnusedResources">You’ll see occasional ads for relevant products. We only advertise products with reliable reviews. %s</string>
|
||||
<!-- Clickable text that links to review quality check recommended products support article. -->
|
||||
<string name="review_quality_check_settings_recommended_products_learn_more" tools:ignore="UnusedResources">Learn more</string>
|
||||
<string name="review_quality_check_settings_recommended_products_learn_more" moz:removedIn="136" tools:ignore="UnusedResources">Learn more</string>
|
||||
<!-- Text for turning sidebar off button from review quality check settings section. -->
|
||||
<string name="review_quality_check_settings_turn_off">Turn off review checker</string>
|
||||
<string name="review_quality_check_settings_turn_off" moz:removedIn="136" tools:ignore="UnusedResources">Turn off review checker</string>
|
||||
<!-- Text for title of recommended product section. This is displayed above a product image, suggested as an alternative to the product reviewed. -->
|
||||
<string name="review_quality_check_ad_title" tools:ignore="UnusedResources">More to consider</string>
|
||||
<string name="review_quality_check_ad_title" moz:removedIn="136" tools:ignore="UnusedResources">More to consider</string>
|
||||
<!-- Caption for recommended product section indicating this is an ad by Fakespot. First parameter is the Fakespot product name. -->
|
||||
<string name="review_quality_check_ad_caption" tools:ignore="UnusedResources">Ad by %s</string>
|
||||
<string name="review_quality_check_ad_caption" moz:removedIn="136" tools:ignore="UnusedResources">Ad by %s</string>
|
||||
<!-- Caption for review quality check panel. First parameter is for clickable text defined in review_quality_check_powered_by_link. -->
|
||||
<string name="review_quality_check_powered_by_2">Review checker is powered by %s</string>
|
||||
<string name="review_quality_check_powered_by_2" moz:removedIn="136" tools:ignore="UnusedResources">Review checker is powered by %s</string>
|
||||
<!-- Clickable text that links to Fakespot.com. First parameter is the Fakespot product name. In the phrase "Fakespot by Mozilla", "by" can be localized. Does not need to stay by. -->
|
||||
<string name="review_quality_check_powered_by_link" tools:ignore="UnusedResources">%s by Mozilla</string>
|
||||
<string name="review_quality_check_powered_by_link" moz:removedIn="136" tools:ignore="UnusedResources">%s by Mozilla</string>
|
||||
<!-- Text for title of warning card informing the user that the current analysis is outdated. -->
|
||||
<string name="review_quality_check_outdated_analysis_warning_title" tools:ignore="UnusedResources">New info to check</string>
|
||||
<string name="review_quality_check_outdated_analysis_warning_title" moz:removedIn="136" tools:ignore="UnusedResources">New info to check</string>
|
||||
<!-- Text for button from warning card informing the user that the current analysis is outdated. Clicking this should trigger the product's re-analysis. -->
|
||||
<string name="review_quality_check_outdated_analysis_warning_action" tools:ignore="UnusedResources">Check now</string>
|
||||
<string name="review_quality_check_outdated_analysis_warning_action" moz:removedIn="136" tools:ignore="UnusedResources">Check now</string>
|
||||
<!-- Title for warning card informing the user that the current product does not have enough reviews for a review analysis. -->
|
||||
<string name="review_quality_check_no_reviews_warning_title">Not enough reviews yet</string>
|
||||
<string name="review_quality_check_no_reviews_warning_title" moz:removedIn="136" tools:ignore="UnusedResources">Not enough reviews yet</string>
|
||||
<!-- Text for body of warning card informing the user that the current product does not have enough reviews for a review analysis. -->
|
||||
<string name="review_quality_check_no_reviews_warning_body">When this product has more reviews, we’ll be able to check their quality.</string>
|
||||
<string name="review_quality_check_no_reviews_warning_body" moz:removedIn="136" tools:ignore="UnusedResources">When this product has more reviews, we’ll be able to check their quality.</string>
|
||||
<!-- Title for warning card informing the user that the current product is currently not available. -->
|
||||
<string name="review_quality_check_product_availability_warning_title">Product is not available</string>
|
||||
<string name="review_quality_check_product_availability_warning_title" moz:removedIn="136" tools:ignore="UnusedResources">Product is not available</string>
|
||||
<!-- Text for the body of warning card informing the user that the current product is currently not available. -->
|
||||
<string name="review_quality_check_product_availability_warning_body">If you see this product is back in stock, report it and we’ll work on checking the reviews.</string>
|
||||
<string name="review_quality_check_product_availability_warning_body" moz:removedIn="136" tools:ignore="UnusedResources">If you see this product is back in stock, report it and we’ll work on checking the reviews.</string>
|
||||
<!-- Clickable text for warning card informing the user that the current product is currently not available. Clicking this should inform the server that the product is available. -->
|
||||
<string name="review_quality_check_product_availability_warning_action_2">Report product is in stock</string>
|
||||
<string name="review_quality_check_product_availability_warning_action_2" moz:removedIn="136" tools:ignore="UnusedResources">Report product is in stock</string>
|
||||
<!-- Title for warning card informing the user that the current product's analysis is still processing. The parameter is the percentage progress (0-100%) of the analysis process (e.g. 56%). -->
|
||||
<string name="review_quality_check_analysis_in_progress_warning_title_2">Checking review quality (%s)</string>
|
||||
<string name="review_quality_check_analysis_in_progress_warning_title_2" moz:removedIn="136" tools:ignore="UnusedResources">Checking review quality (%s)</string>
|
||||
<!-- Text for body of warning card informing the user that the current product's analysis is still processing. -->
|
||||
<string name="review_quality_check_analysis_in_progress_warning_body">This could take about 60 seconds.</string>
|
||||
<string name="review_quality_check_analysis_in_progress_warning_body" moz:removedIn="136" tools:ignore="UnusedResources">This could take about 60 seconds.</string>
|
||||
<!-- Title for info card displayed after the user reports a product is back in stock. -->
|
||||
<string name="review_quality_check_analysis_requested_info_title">Thanks for reporting!</string>
|
||||
<string name="review_quality_check_analysis_requested_info_title" moz:removedIn="136" tools:ignore="UnusedResources">Thanks for reporting!</string>
|
||||
<!-- Text for body of info card displayed after the user reports a product is back in stock. -->
|
||||
<string name="review_quality_check_analysis_requested_info_body">We should have info about this product’s reviews within 24 hours. Please check back.</string>
|
||||
<string name="review_quality_check_analysis_requested_info_body" moz:removedIn="136" tools:ignore="UnusedResources">We should have info about this product’s reviews within 24 hours. Please check back.</string>
|
||||
<!-- Title for info card displayed when the user review checker while on a product that Fakespot does not analyze (e.g. gift cards, music). -->
|
||||
<string name="review_quality_check_not_analyzable_info_title">We can’t check these reviews</string>
|
||||
<string name="review_quality_check_not_analyzable_info_title" moz:removedIn="136" tools:ignore="UnusedResources">We can’t check these reviews</string>
|
||||
<!-- Text for body of info card displayed when the user review checker while on a product that Fakespot does not analyze (e.g. gift cards, music). -->
|
||||
<string name="review_quality_check_not_analyzable_info_body">Unfortunately, we can’t check the review quality for certain types of products. For example, gift cards and streaming video, music, and games.</string>
|
||||
<string name="review_quality_check_not_analyzable_info_body" moz:removedIn="136" tools:ignore="UnusedResources">Unfortunately, we can’t check the review quality for certain types of products. For example, gift cards and streaming video, music, and games.</string>
|
||||
<!-- Title for info card displayed when another user reported the displayed product is back in stock. -->
|
||||
<string name="review_quality_check_analysis_requested_other_user_info_title" tools:ignore="UnusedResources">Info coming soon</string>
|
||||
<string name="review_quality_check_analysis_requested_other_user_info_title" moz:removedIn="136" tools:ignore="UnusedResources">Info coming soon</string>
|
||||
<!-- Text for body of info card displayed when another user reported the displayed product is back in stock. -->
|
||||
<string name="review_quality_check_analysis_requested_other_user_info_body" tools:ignore="UnusedResources">We should have info about this product’s reviews within 24 hours. Please check back.</string>
|
||||
<string name="review_quality_check_analysis_requested_other_user_info_body" moz:removedIn="136" tools:ignore="UnusedResources">We should have info about this product’s reviews within 24 hours. Please check back.</string>
|
||||
<!-- Title for info card displayed to the user when analysis finished updating. -->
|
||||
<string name="review_quality_check_analysis_updated_confirmation_title" tools:ignore="UnusedResources">Analysis is up to date</string>
|
||||
<string name="review_quality_check_analysis_updated_confirmation_title" moz:removedIn="136" tools:ignore="UnusedResources">Analysis is up to date</string>
|
||||
<!-- Text for the action button from info card displayed to the user when analysis finished updating. -->
|
||||
<string name="review_quality_check_analysis_updated_confirmation_action" tools:ignore="UnusedResources">Got it</string>
|
||||
<string name="review_quality_check_analysis_updated_confirmation_action" moz:removedIn="136" tools:ignore="UnusedResources">Got it</string>
|
||||
<!-- Title for error card displayed to the user when an error occurred. -->
|
||||
<string name="review_quality_check_generic_error_title">No info available right now</string>
|
||||
<string name="review_quality_check_generic_error_title" moz:removedIn="136" tools:ignore="UnusedResources">No info available right now</string>
|
||||
<!-- Text for body of error card displayed to the user when an error occurred. -->
|
||||
<string name="review_quality_check_generic_error_body">We’re working to resolve the issue. Please check back soon.</string>
|
||||
<string name="review_quality_check_generic_error_body" moz:removedIn="136" tools:ignore="UnusedResources">We’re working to resolve the issue. Please check back soon.</string>
|
||||
<!-- Title for error card displayed to the user when the device is disconnected from the network. -->
|
||||
<string name="review_quality_check_no_connection_title">No network connection</string>
|
||||
<string name="review_quality_check_no_connection_title" moz:removedIn="136" tools:ignore="UnusedResources">No network connection</string>
|
||||
<!-- Text for body of error card displayed to the user when the device is disconnected from the network. -->
|
||||
<string name="review_quality_check_no_connection_body">Check your network connection and then try reloading the page.</string>
|
||||
<string name="review_quality_check_no_connection_body" moz:removedIn="136" tools:ignore="UnusedResources">Check your network connection and then try reloading the page.</string>
|
||||
<!-- Title for card displayed to the user for products whose reviews were not analyzed yet. -->
|
||||
<string name="review_quality_check_no_analysis_title">No info about these reviews yet</string>
|
||||
<string name="review_quality_check_no_analysis_title" moz:removedIn="136" tools:ignore="UnusedResources">No info about these reviews yet</string>
|
||||
<!-- Text for the body of card displayed to the user for products whose reviews were not analyzed yet. -->
|
||||
<string name="review_quality_check_no_analysis_body">To know whether this product’s reviews are reliable, check the review quality. It only takes about 60 seconds.</string>
|
||||
<string name="review_quality_check_no_analysis_body" moz:removedIn="136" tools:ignore="UnusedResources">To know whether this product’s reviews are reliable, check the review quality. It only takes about 60 seconds.</string>
|
||||
<!-- Text for button from body of card displayed to the user for products whose reviews were not analyzed yet. Clicking this should trigger a product analysis. -->
|
||||
<string name="review_quality_check_no_analysis_link">Check review quality</string>
|
||||
<string name="review_quality_check_no_analysis_link" moz:removedIn="136" tools:ignore="UnusedResources">Check review quality</string>
|
||||
<!-- Headline for review quality check contextual onboarding card. -->
|
||||
<string name="review_quality_check_contextual_onboarding_title">Try our trusted guide to product reviews</string>
|
||||
<string name="review_quality_check_contextual_onboarding_title" moz:removedIn="136" tools:ignore="UnusedResources">Try our trusted guide to product reviews</string>
|
||||
<!-- Description for review quality check contextual onboarding card. The first and last two parameters are for retailer names (e.g. Amazon, Walmart). The second parameter is for the name of the application (e.g. Firefox). -->
|
||||
<string name="review_quality_check_contextual_onboarding_description">See how reliable product reviews are on %1$s before you buy. Review checker, an experimental feature from %2$s, is built right into the browser. It works on %3$s and %4$s, too.</string>
|
||||
<string name="review_quality_check_contextual_onboarding_description" moz:removedIn="136" tools:ignore="UnusedResources">See how reliable product reviews are on %1$s before you buy. Review checker, an experimental feature from %2$s, is built right into the browser. It works on %3$s and %4$s, too.</string>
|
||||
<!-- Description for review quality check contextual onboarding card. The first parameters is for retailer name (e.g. Amazon). The second parameter is for the name of the application (e.g. Firefox). -->
|
||||
<string name="review_quality_check_contextual_onboarding_description_one_vendor">See how reliable product reviews are on %1$s before you buy. Review Checker, an experimental feature from %2$s, is built right into the browser.</string>
|
||||
<string name="review_quality_check_contextual_onboarding_description_one_vendor" moz:removedIn="136" tools:ignore="UnusedResources">See how reliable product reviews are on %1$s before you buy. Review Checker, an experimental feature from %2$s, is built right into the browser.</string>
|
||||
<!-- Paragraph presenting review quality check feature. First parameter is the Fakespot product name. Second parameter is for clickable text defined in review_quality_check_contextual_onboarding_learn_more_link. In the phrase "Fakespot by Mozilla", "by" can be localized. Does not need to stay by. -->
|
||||
<string name="review_quality_check_contextual_onboarding_learn_more">Using the power of %1$s by Mozilla, we help you avoid biased and inauthentic reviews. Our AI model is always improving to protect you as you shop. %2$s</string>
|
||||
<string name="review_quality_check_contextual_onboarding_learn_more" moz:removedIn="136" tools:ignore="UnusedResources">Using the power of %1$s by Mozilla, we help you avoid biased and inauthentic reviews. Our AI model is always improving to protect you as you shop. %2$s</string>
|
||||
<!-- Clickable text from the contextual onboarding card that links to review quality check support article. -->
|
||||
<string name="review_quality_check_contextual_onboarding_learn_more_link">Learn more</string>
|
||||
<string name="review_quality_check_contextual_onboarding_learn_more_link" moz:removedIn="136" tools:ignore="UnusedResources">Learn more</string>
|
||||
<!-- Caption text to be displayed in review quality check contextual onboarding card above the opt-in button. First parameter is Firefox app name, third parameter is the Fakespot product name. Second & fourth are for clickable texts defined in review_quality_check_contextual_onboarding_privacy_policy_3 and review_quality_check_contextual_onboarding_terms_use. -->
|
||||
<string name="review_quality_check_contextual_onboarding_caption_4">By selecting “Yes, try it” you agree to %1$s’s %2$s and %3$s’s %4$s.</string>
|
||||
<string name="review_quality_check_contextual_onboarding_caption_4" moz:removedIn="136" tools:ignore="UnusedResources">By selecting “Yes, try it” you agree to %1$s’s %2$s and %3$s’s %4$s.</string>
|
||||
<!-- Clickable text from the review quality check contextual onboarding card that links to Fakespot privacy notice. -->
|
||||
<string name="review_quality_check_contextual_onboarding_privacy_policy_3">privacy notice</string>
|
||||
<string name="review_quality_check_contextual_onboarding_privacy_policy_3" moz:removedIn="136" tools:ignore="UnusedResources">privacy notice</string>
|
||||
<!-- Clickable text from the review quality check contextual onboarding card that links to Fakespot terms of use. -->
|
||||
<string name="review_quality_check_contextual_onboarding_terms_use">terms of use</string>
|
||||
<string name="review_quality_check_contextual_onboarding_terms_use" moz:removedIn="136" tools:ignore="UnusedResources">terms of use</string>
|
||||
<!-- Text for opt-in button from the review quality check contextual onboarding card. -->
|
||||
<string name="review_quality_check_contextual_onboarding_primary_button_text">Yes, try it</string>
|
||||
<string name="review_quality_check_contextual_onboarding_primary_button_text" moz:removedIn="136" tools:ignore="UnusedResources">Yes, try it</string>
|
||||
<!-- Text for opt-out button from the review quality check contextual onboarding card. -->
|
||||
<string name="review_quality_check_contextual_onboarding_secondary_button_text">Not now</string>
|
||||
<string name="review_quality_check_contextual_onboarding_secondary_button_text" moz:removedIn="136" tools:ignore="UnusedResources">Not now</string>
|
||||
<!-- Content description (not visible, for screen readers etc.) for opening browser menu button to open review quality check bottom sheet. -->
|
||||
<string name="review_quality_check_open_handle_content_description">Open review checker</string>
|
||||
<string name="review_quality_check_open_handle_content_description" moz:removedIn="136" tools:ignore="UnusedResources">Open review checker</string>
|
||||
<!-- Content description (not visible, for screen readers etc.) for closing browser menu button to open review quality check bottom sheet. -->
|
||||
<string name="review_quality_check_close_handle_content_description">Close review checker</string>
|
||||
<string name="review_quality_check_close_handle_content_description" moz:removedIn="136" tools:ignore="UnusedResources">Close review checker</string>
|
||||
<!-- Content description (not visible, for screen readers etc.) for review quality check star rating. First parameter is the number of stars (1-5) representing the rating. -->
|
||||
<string name="review_quality_check_star_rating_content_description">%1$s out of 5 stars</string>
|
||||
<string name="review_quality_check_star_rating_content_description" moz:removedIn="136" tools:ignore="UnusedResources">%1$s out of 5 stars</string>
|
||||
<!-- Text for minimize button from highlights card. When clicked the highlights card should reduce its size. -->
|
||||
<string name="review_quality_check_highlights_show_less">Show less</string>
|
||||
<string name="review_quality_check_highlights_show_less" moz:removedIn="136" tools:ignore="UnusedResources">Show less</string>
|
||||
<!-- Text for maximize button from highlights card. When clicked the highlights card should expand to its full size. -->
|
||||
<string name="review_quality_check_highlights_show_more">Show more</string>
|
||||
<string name="review_quality_check_highlights_show_more" moz:removedIn="136" tools:ignore="UnusedResources">Show more</string>
|
||||
<!-- Text for highlights card quality category header. Reviews shown under this header should refer the product's quality. -->
|
||||
<string name="review_quality_check_highlights_type_quality">Quality</string>
|
||||
<string name="review_quality_check_highlights_type_quality" moz:removedIn="136" tools:ignore="UnusedResources">Quality</string>
|
||||
<!-- Text for highlights card price category header. Reviews shown under this header should refer the product's price. -->
|
||||
<string name="review_quality_check_highlights_type_price">Price</string>
|
||||
<string name="review_quality_check_highlights_type_price" moz:removedIn="136" tools:ignore="UnusedResources">Price</string>
|
||||
<!-- Text for highlights card shipping category header. Reviews shown under this header should refer the product's shipping. -->
|
||||
<string name="review_quality_check_highlights_type_shipping">Shipping</string>
|
||||
<string name="review_quality_check_highlights_type_shipping" moz:removedIn="136" tools:ignore="UnusedResources">Shipping</string>
|
||||
<!-- Text for highlights card packaging and appearance category header. Reviews shown under this header should refer the product's packaging and appearance. -->
|
||||
<string name="review_quality_check_highlights_type_packaging_appearance">Packaging and appearance</string>
|
||||
<string name="review_quality_check_highlights_type_packaging_appearance" moz:removedIn="136" tools:ignore="UnusedResources">Packaging and appearance</string>
|
||||
<!-- Text for highlights card competitiveness category header. Reviews shown under this header should refer the product's competitiveness. -->
|
||||
<string name="review_quality_check_highlights_type_competitiveness">Competitiveness</string>
|
||||
<string name="review_quality_check_highlights_type_competitiveness" moz:removedIn="136" tools:ignore="UnusedResources">Competitiveness</string>
|
||||
<!-- Text that is surrounded by quotes. The parameter is the actual text that is in quotes. An example of that text could be: Excellent craftsmanship, and that is displayed as “Excellent craftsmanship”. The text comes from a buyer's review that the feature is highlighting" -->
|
||||
<string name="surrounded_with_quotes">“%s”</string>
|
||||
<string name="surrounded_with_quotes" moz:removedIn="136" tools:ignore="UnusedResources">“%s”</string>
|
||||
|
||||
<!-- Accessibility services actions labels. These will be appended to accessibility actions like "Double tap to.." but not by or applications but by services like Talkback. -->
|
||||
<!-- Action label for elements that can be collapsed if interacting with them. Talkback will append this to say "Double tap to collapse". -->
|
||||
|
||||
@@ -66,7 +66,6 @@ class BrowserToolbarCFRPresenterTest {
|
||||
val privateTab = createTab(url = "", private = true)
|
||||
val browserStore = createBrowserStore(tab = privateTab, selectedTabId = privateTab.id)
|
||||
val settings: Settings = mockk(relaxed = true) {
|
||||
every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis()
|
||||
every { shouldShowEraseActionCFR } returns false
|
||||
every { shouldShowCookieBannersCFR } returns true
|
||||
every { shouldUseCookieBannerPrivateMode } returns true
|
||||
@@ -143,7 +142,6 @@ class BrowserToolbarCFRPresenterTest {
|
||||
every { isTabStripEnabled() } returns false
|
||||
}
|
||||
val settings: Settings = mockk(relaxed = true) {
|
||||
every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis()
|
||||
every { shouldShowEraseActionCFR } returns false
|
||||
every { shouldShowCookieBannersCFR } returns false
|
||||
every { shouldUseCookieBannerPrivateMode } returns false
|
||||
@@ -174,7 +172,6 @@ class BrowserToolbarCFRPresenterTest {
|
||||
every { isTabStripEnabled() } returns true
|
||||
}
|
||||
val settings: Settings = mockk(relaxed = true) {
|
||||
every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis()
|
||||
every { shouldShowEraseActionCFR } returns false
|
||||
every { shouldShowCookieBannersCFR } returns false
|
||||
every { shouldUseCookieBannerPrivateMode } returns false
|
||||
@@ -205,7 +202,6 @@ class BrowserToolbarCFRPresenterTest {
|
||||
every { isTabStripEnabled() } returns false
|
||||
}
|
||||
val settings: Settings = mockk(relaxed = true) {
|
||||
every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis()
|
||||
every { shouldShowEraseActionCFR } returns false
|
||||
every { shouldShowCookieBannersCFR } returns false
|
||||
every { shouldUseCookieBannerPrivateMode } returns false
|
||||
|
||||
@@ -58,7 +58,6 @@ class RecordedNimbusContextTest {
|
||||
put("install_referrer_response_utm_campaign", "")
|
||||
put("install_referrer_response_utm_term", "")
|
||||
put("install_referrer_response_utm_content", "")
|
||||
put("is_review_checker_enabled", false)
|
||||
put("android_sdk_version", Build.VERSION.SDK_INT.toString())
|
||||
put("app_version", "")
|
||||
put("locale", "")
|
||||
@@ -103,7 +102,6 @@ class RecordedNimbusContextTest {
|
||||
put("installReferrerResponseUtmTerm", "")
|
||||
put("installReferrerResponseUtmContent", "")
|
||||
put("isFirstRun", false)
|
||||
put("isReviewCheckerEnabled", false)
|
||||
put("language", "en")
|
||||
put("locale", "")
|
||||
put("region", "US")
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping
|
||||
|
||||
import mozilla.components.concept.engine.shopping.Highlight
|
||||
import mozilla.components.concept.engine.shopping.ProductAnalysis
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.HighlightsInfo
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.RecommendedProductState
|
||||
|
||||
object ProductAnalysisTestData {
|
||||
|
||||
fun productAnalysis(
|
||||
productId: String? = "1",
|
||||
analysisURL: String = "https://test.com",
|
||||
grade: String? = "A",
|
||||
adjustedRating: Double? = 4.5,
|
||||
needsAnalysis: Boolean = false,
|
||||
pageNotSupported: Boolean = false,
|
||||
notEnoughReviews: Boolean = false,
|
||||
lastAnalysisTime: Long = 0L,
|
||||
deletedProductReported: Boolean = false,
|
||||
deletedProduct: Boolean = false,
|
||||
highlights: Highlight? = null,
|
||||
): ProductAnalysis = ProductAnalysis(
|
||||
productId = productId,
|
||||
analysisURL = analysisURL,
|
||||
grade = grade,
|
||||
adjustedRating = adjustedRating,
|
||||
needsAnalysis = needsAnalysis,
|
||||
pageNotSupported = pageNotSupported,
|
||||
notEnoughReviews = notEnoughReviews,
|
||||
lastAnalysisTime = lastAnalysisTime,
|
||||
deletedProductReported = deletedProductReported,
|
||||
deletedProduct = deletedProduct,
|
||||
highlights = highlights,
|
||||
)
|
||||
|
||||
fun analysisPresent(
|
||||
productId: String = "1",
|
||||
productUrl: String = "https://test.com",
|
||||
reviewGrade: ReviewQualityCheckState.Grade? = ReviewQualityCheckState.Grade.A,
|
||||
adjustedRating: Float? = 4.5f,
|
||||
analysisStatus: AnalysisStatus = AnalysisStatus.UpToDate,
|
||||
highlightsInfo: HighlightsInfo? = null,
|
||||
recommendedProductState: RecommendedProductState = RecommendedProductState.Initial,
|
||||
): ProductReviewState.AnalysisPresent =
|
||||
ProductReviewState.AnalysisPresent(
|
||||
productId = productId,
|
||||
productUrl = productUrl,
|
||||
reviewGrade = reviewGrade,
|
||||
adjustedRating = adjustedRating,
|
||||
analysisStatus = analysisStatus,
|
||||
highlightsInfo = highlightsInfo,
|
||||
recommendedProductState = recommendedProductState,
|
||||
)
|
||||
|
||||
fun noAnalysisPresent(
|
||||
progress: Float = -1f,
|
||||
): ProductReviewState.NoAnalysisPresent =
|
||||
ProductReviewState.NoAnalysisPresent(
|
||||
progress = ProductReviewState.Progress(progress),
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping
|
||||
|
||||
import mozilla.components.concept.engine.shopping.ProductRecommendation
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
|
||||
object ProductRecommendationTestData {
|
||||
|
||||
fun productRecommendation(
|
||||
aid: String = "aid",
|
||||
url: String = "https://test.com",
|
||||
grade: String = "A",
|
||||
adjustedRating: Double = 4.7,
|
||||
sponsored: Boolean = true,
|
||||
analysisUrl: String = "analysisUrl",
|
||||
imageUrl: String = "https://imageurl.com",
|
||||
name: String = "Test Product",
|
||||
price: String = "100",
|
||||
currency: String = "USD",
|
||||
): ProductRecommendation = ProductRecommendation(
|
||||
aid = aid,
|
||||
url = url,
|
||||
grade = grade,
|
||||
adjustedRating = adjustedRating,
|
||||
sponsored = sponsored,
|
||||
analysisUrl = analysisUrl,
|
||||
imageUrl = imageUrl,
|
||||
name = name,
|
||||
price = price,
|
||||
currency = currency,
|
||||
)
|
||||
|
||||
fun product(
|
||||
aid: String = "aid",
|
||||
url: String = "https://test.com",
|
||||
reviewGrade: ReviewQualityCheckState.Grade = ReviewQualityCheckState.Grade.A,
|
||||
adjustedRating: Double = 4.7,
|
||||
sponsored: Boolean = true,
|
||||
analysisUrl: String = "analysisUrl",
|
||||
imageUrl: String = "https://imageurl.com",
|
||||
name: String = "Test Product",
|
||||
formattedPrice: String = "$100",
|
||||
): ReviewQualityCheckState.RecommendedProductState.Product =
|
||||
ReviewQualityCheckState.RecommendedProductState.Product(
|
||||
aid = aid,
|
||||
productUrl = url,
|
||||
reviewGrade = reviewGrade,
|
||||
adjustedRating = adjustedRating.toFloat(),
|
||||
isSponsored = sponsored,
|
||||
analysisUrl = analysisUrl,
|
||||
imageUrl = imageUrl,
|
||||
name = name,
|
||||
formattedPrice = formattedPrice,
|
||||
)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping
|
||||
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.shopping.store.BottomSheetViewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
|
||||
|
||||
class ReviewQualityCheckBottomSheetStateFeatureTest {
|
||||
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule()
|
||||
|
||||
@Test
|
||||
fun `WHEN store state changes to not opted in from any other state THEN callback is invoked with half state`() {
|
||||
val store = ReviewQualityCheckStore(middleware = emptyList())
|
||||
var updatedState: BottomSheetViewState? = null
|
||||
val tested = ReviewQualityCheckBottomSheetStateFeature(
|
||||
store = store,
|
||||
onRequestStateUpdate = {
|
||||
updatedState = it
|
||||
},
|
||||
isScreenReaderEnabled = false,
|
||||
)
|
||||
|
||||
tested.start()
|
||||
store.dispatch(
|
||||
ReviewQualityCheckAction.OptInCompleted(
|
||||
isProductRecommendationsEnabled = true,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.WALMART,
|
||||
isHighlightsExpanded = false,
|
||||
isInfoExpanded = false,
|
||||
isSettingsExpanded = false,
|
||||
),
|
||||
).joinBlocking()
|
||||
store.dispatch(ReviewQualityCheckAction.OptOutCompleted(emptyList())).joinBlocking()
|
||||
|
||||
assertEquals(BottomSheetViewState.HALF_VIEW, updatedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN store state changes to not opted in from initial state THEN callback is invoked with full state`() {
|
||||
val store = ReviewQualityCheckStore(middleware = emptyList())
|
||||
var updatedState: BottomSheetViewState? = null
|
||||
val tested = ReviewQualityCheckBottomSheetStateFeature(
|
||||
store = store,
|
||||
onRequestStateUpdate = {
|
||||
updatedState = it
|
||||
},
|
||||
isScreenReaderEnabled = false,
|
||||
)
|
||||
assertEquals(ReviewQualityCheckState.Initial, store.state)
|
||||
|
||||
tested.start()
|
||||
store.dispatch(ReviewQualityCheckAction.OptOutCompleted(emptyList())).joinBlocking()
|
||||
|
||||
assertEquals(BottomSheetViewState.FULL_VIEW, updatedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN an accessibility screen reader is enabled WHEN user opens bottom sheet THEN it is opened fully`() {
|
||||
val store = ReviewQualityCheckStore(middleware = emptyList())
|
||||
var updatedState: BottomSheetViewState? = null
|
||||
val tested = ReviewQualityCheckBottomSheetStateFeature(
|
||||
store = store,
|
||||
onRequestStateUpdate = {
|
||||
updatedState = it
|
||||
},
|
||||
isScreenReaderEnabled = true,
|
||||
)
|
||||
|
||||
tested.start()
|
||||
store.dispatch(
|
||||
ReviewQualityCheckAction.OptInCompleted(
|
||||
isProductRecommendationsEnabled = true,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.WALMART,
|
||||
isHighlightsExpanded = false,
|
||||
isInfoExpanded = false,
|
||||
isSettingsExpanded = false,
|
||||
),
|
||||
).joinBlocking()
|
||||
|
||||
assertEquals(BottomSheetViewState.FULL_VIEW, updatedState)
|
||||
}
|
||||
}
|
||||
@@ -1,557 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.components.appstate.shopping.ShoppingState
|
||||
import org.mozilla.fenix.shopping.fake.FakeShoppingExperienceFeature
|
||||
|
||||
class ReviewQualityCheckFeatureTest {
|
||||
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule()
|
||||
|
||||
@Test
|
||||
fun `WHEN feature is not enabled THEN callback returns false`() = runTest {
|
||||
var availability: Boolean? = null
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "test-tab",
|
||||
isProductUrl = true,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = BrowserStore(
|
||||
initialState = browserState,
|
||||
),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(enabled = false),
|
||||
onIconVisibilityChange = {
|
||||
availability = it
|
||||
},
|
||||
onBottomSheetStateChange = {},
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
testScheduler.advanceTimeBy(250)
|
||||
|
||||
assertFalse(availability!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN feature is enabled and selected tab is not a product page THEN callback returns false`() =
|
||||
runTest {
|
||||
var availability: Boolean? = null
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "test-tab",
|
||||
isProductUrl = false,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = BrowserStore(
|
||||
initialState = browserState,
|
||||
),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {
|
||||
availability = it
|
||||
},
|
||||
onBottomSheetStateChange = {},
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
assertFalse(availability!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN feature is enabled and selected tab is not yet loaded THEN callback returns false`() =
|
||||
runTest {
|
||||
var availability: Boolean? = null
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "test-tab",
|
||||
isProductUrl = true,
|
||||
).let {
|
||||
it.copy(content = it.content.copy(loading = true))
|
||||
}
|
||||
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = BrowserStore(
|
||||
initialState = browserState,
|
||||
),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {
|
||||
availability = it
|
||||
},
|
||||
onBottomSheetStateChange = {},
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
assertFalse(availability!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN feature is enabled and selected tab is a product page THEN callback returns true`() =
|
||||
runTest {
|
||||
var availability: Boolean? = null
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "test-tab",
|
||||
isProductUrl = true,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = BrowserStore(
|
||||
initialState = browserState,
|
||||
),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {
|
||||
availability = it
|
||||
},
|
||||
onBottomSheetStateChange = {},
|
||||
debounceTimeoutMillis = { 0 },
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
assertTrue(availability!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN feature is enabled and selected tab is switched to a product page THEN callback returns true`() =
|
||||
runTest {
|
||||
var availability: Boolean? = null
|
||||
val tab1 = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "tab1",
|
||||
isProductUrl = false,
|
||||
)
|
||||
val tab2 = createTab(
|
||||
url = "https://www.shopping.org",
|
||||
id = "tab2",
|
||||
isProductUrl = true,
|
||||
)
|
||||
val browserStore = BrowserStore(
|
||||
initialState = BrowserState(
|
||||
tabs = listOf(tab1, tab2),
|
||||
selectedTabId = tab1.id,
|
||||
),
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = browserStore,
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {
|
||||
availability = it
|
||||
},
|
||||
onBottomSheetStateChange = {},
|
||||
debounceTimeoutMillis = { 0 },
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
assertFalse(availability!!)
|
||||
|
||||
browserStore.dispatch(TabListAction.SelectTabAction(tab2.id)).joinBlocking()
|
||||
|
||||
assertTrue(availability!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN feature is enabled and selected tab is switched to not a product page THEN callback returns false`() =
|
||||
runTest {
|
||||
var availability: Boolean? = null
|
||||
val tab1 = createTab(
|
||||
url = "https://www.shopping.org",
|
||||
id = "tab1",
|
||||
isProductUrl = true,
|
||||
)
|
||||
val tab2 = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "tab2",
|
||||
isProductUrl = false,
|
||||
)
|
||||
val browserStore = BrowserStore(
|
||||
initialState = BrowserState(
|
||||
tabs = listOf(tab1, tab2),
|
||||
selectedTabId = tab1.id,
|
||||
),
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = browserStore,
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {
|
||||
availability = it
|
||||
},
|
||||
onBottomSheetStateChange = {},
|
||||
debounceTimeoutMillis = { 0 },
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
assertTrue(availability!!)
|
||||
|
||||
browserStore.dispatch(TabListAction.SelectTabAction(tab2.id)).joinBlocking()
|
||||
|
||||
assertFalse(availability!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN feature is enabled and isProductUrl updates a lot THEN callback is only invoked when isProductUrl settles`() =
|
||||
runTest {
|
||||
val availability = mutableListOf<Boolean>()
|
||||
val tab1 = createTab(
|
||||
url = "https://www.shopping.org",
|
||||
id = "tab1",
|
||||
isProductUrl = false,
|
||||
)
|
||||
val browserStore = BrowserStore(
|
||||
initialState = BrowserState(
|
||||
tabs = listOf(tab1),
|
||||
selectedTabId = tab1.id,
|
||||
),
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = browserStore,
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {
|
||||
availability.add(it)
|
||||
},
|
||||
onBottomSheetStateChange = {},
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
assertEquals(listOf(false), availability)
|
||||
|
||||
browserStore.dispatch(
|
||||
ContentAction.UpdateProductUrlStateAction(
|
||||
tabId = tab1.id,
|
||||
isProductUrl = true,
|
||||
),
|
||||
).joinBlocking()
|
||||
|
||||
browserStore.dispatch(
|
||||
ContentAction.UpdateProductUrlStateAction(
|
||||
tabId = tab1.id,
|
||||
isProductUrl = false,
|
||||
),
|
||||
).joinBlocking()
|
||||
|
||||
browserStore.dispatch(
|
||||
ContentAction.UpdateProductUrlStateAction(
|
||||
tabId = tab1.id,
|
||||
isProductUrl = true,
|
||||
),
|
||||
).joinBlocking()
|
||||
|
||||
testScheduler.advanceTimeBy(250)
|
||||
|
||||
// The first true is never emitted because it is debounced
|
||||
assertNotEquals(listOf(false, true, false, true), availability)
|
||||
assertEquals(listOf(false, false, true), availability)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN feature is enabled and selected tab is switched to a product page after stop is called THEN callback is only called once with false`() =
|
||||
runTest {
|
||||
var availability: Boolean? = null
|
||||
var availabilityCount = 0
|
||||
val tab1 = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "tab1",
|
||||
isProductUrl = false,
|
||||
)
|
||||
val tab2 = createTab(
|
||||
url = "https://www.shopping.org",
|
||||
id = "tab2",
|
||||
isProductUrl = true,
|
||||
)
|
||||
val browserStore = BrowserStore(
|
||||
initialState = BrowserState(
|
||||
tabs = listOf(tab1, tab2),
|
||||
selectedTabId = tab1.id,
|
||||
),
|
||||
)
|
||||
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = browserStore,
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {
|
||||
availability = it
|
||||
availabilityCount++
|
||||
},
|
||||
onBottomSheetStateChange = {},
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
tested.stop()
|
||||
browserStore.dispatch(TabListAction.SelectTabAction(tab2.id)).joinBlocking()
|
||||
|
||||
assertEquals(1, availabilityCount)
|
||||
assertFalse(availability!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the shopping sheet is collapsed THEN the callback is called with false`() {
|
||||
val appStore = AppStore(
|
||||
initialState = AppState(
|
||||
shoppingState = ShoppingState(shoppingSheetExpanded = true),
|
||||
),
|
||||
)
|
||||
var isExpanded: Boolean? = null
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = appStore,
|
||||
browserStore = BrowserStore(),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {},
|
||||
onBottomSheetStateChange = {
|
||||
isExpanded = it
|
||||
},
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
appStore.dispatch(ShoppingAction.ShoppingSheetStateUpdated(expanded = false)).joinBlocking()
|
||||
|
||||
assertFalse(isExpanded!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the shopping sheet is expanded THEN the collapsed callback is called with true`() {
|
||||
val appStore = AppStore(
|
||||
initialState = AppState(
|
||||
shoppingState = ShoppingState(shoppingSheetExpanded = false),
|
||||
),
|
||||
)
|
||||
var isExpanded: Boolean? = null
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = appStore,
|
||||
browserStore = BrowserStore(),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {},
|
||||
onBottomSheetStateChange = {
|
||||
isExpanded = it
|
||||
},
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
appStore.dispatch(ShoppingAction.ShoppingSheetStateUpdated(expanded = true)).joinBlocking()
|
||||
|
||||
assertTrue(isExpanded!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the feature is restarted THEN first emission is collected to set the tint`() {
|
||||
val appStore = AppStore(
|
||||
initialState = AppState(
|
||||
shoppingState = ShoppingState(shoppingSheetExpanded = false),
|
||||
),
|
||||
)
|
||||
var isExpanded: Boolean? = null
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = appStore,
|
||||
browserStore = BrowserStore(),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {},
|
||||
onBottomSheetStateChange = {
|
||||
isExpanded = it
|
||||
},
|
||||
onProductPageDetected = {},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
tested.stop()
|
||||
|
||||
// emulate emission
|
||||
appStore.dispatch(ShoppingAction.ShoppingSheetStateUpdated(expanded = false)).joinBlocking()
|
||||
|
||||
tested.start()
|
||||
assertFalse(isExpanded!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN feature is enabled WHEN non product url accessed THEN callback not called`() {
|
||||
runTest {
|
||||
var invokedCounter = 0
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "test-tab",
|
||||
isProductUrl = false,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = BrowserStore(
|
||||
initialState = browserState,
|
||||
),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {},
|
||||
onBottomSheetStateChange = {},
|
||||
debounceTimeoutMillis = { 0 },
|
||||
onProductPageDetected = {
|
||||
invokedCounter++
|
||||
},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
assertEquals(invokedCounter, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN feature is enabled WHEN product url accessed THEN callback called`() {
|
||||
runTest {
|
||||
var invokedCounter = 0
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org",
|
||||
id = "test-tab",
|
||||
isProductUrl = true,
|
||||
).let {
|
||||
it.copy(content = it.content.copy(loading = false))
|
||||
}
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = BrowserStore(
|
||||
initialState = browserState,
|
||||
),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(),
|
||||
onIconVisibilityChange = {},
|
||||
onBottomSheetStateChange = {},
|
||||
debounceTimeoutMillis = { 0 },
|
||||
onProductPageDetected = {
|
||||
invokedCounter++
|
||||
},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
assertEquals(invokedCounter, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN feature is disabled WHEN non product url accessed THEN callback not called`() {
|
||||
runTest {
|
||||
var invokedCounter = 0
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "test-tab",
|
||||
isProductUrl = false,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = BrowserStore(
|
||||
initialState = browserState,
|
||||
),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(enabled = false),
|
||||
onIconVisibilityChange = {},
|
||||
onBottomSheetStateChange = {},
|
||||
debounceTimeoutMillis = { 0 },
|
||||
onProductPageDetected = {
|
||||
invokedCounter++
|
||||
},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
assertEquals(invokedCounter, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN feature is disabled WHEN product url accessed THEN callback called`() {
|
||||
runTest {
|
||||
var invokedCounter = 0
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "test-tab",
|
||||
isProductUrl = true,
|
||||
).let {
|
||||
it.copy(content = it.content.copy(loading = false))
|
||||
}
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
val tested = ReviewQualityCheckFeature(
|
||||
appStore = AppStore(),
|
||||
browserStore = BrowserStore(
|
||||
initialState = browserState,
|
||||
),
|
||||
shoppingExperienceFeature = FakeShoppingExperienceFeature(enabled = false),
|
||||
onIconVisibilityChange = {},
|
||||
onBottomSheetStateChange = {},
|
||||
debounceTimeoutMillis = { 0 },
|
||||
onProductPageDetected = {
|
||||
invokedCounter++
|
||||
},
|
||||
)
|
||||
|
||||
tested.start()
|
||||
|
||||
assertEquals(invokedCounter, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.fake
|
||||
|
||||
import org.mozilla.fenix.shopping.middleware.NetworkChecker
|
||||
|
||||
class FakeNetworkChecker(
|
||||
private val isConnected: Boolean = true,
|
||||
) : NetworkChecker {
|
||||
override fun isConnected(): Boolean = isConnected
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.fake
|
||||
|
||||
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckPreferences
|
||||
|
||||
class FakeReviewQualityCheckPreferences(
|
||||
private val isEnabled: Boolean = false,
|
||||
private val isProductRecommendationsEnabled: Boolean? = false,
|
||||
) : ReviewQualityCheckPreferences {
|
||||
override suspend fun enabled(): Boolean = isEnabled
|
||||
|
||||
override suspend fun productRecommendationsEnabled(): Boolean? = isProductRecommendationsEnabled
|
||||
|
||||
override suspend fun setEnabled(isEnabled: Boolean) {
|
||||
}
|
||||
|
||||
override suspend fun setProductRecommendationsEnabled(isEnabled: Boolean) {
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.fake
|
||||
|
||||
import mozilla.components.concept.engine.shopping.ProductAnalysis
|
||||
import mozilla.components.concept.engine.shopping.ProductRecommendation
|
||||
import org.mozilla.fenix.shopping.middleware.AnalysisStatusDto
|
||||
import org.mozilla.fenix.shopping.middleware.AnalysisStatusProgressDto
|
||||
import org.mozilla.fenix.shopping.middleware.ReportBackInStockStatusDto
|
||||
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckService
|
||||
|
||||
class FakeReviewQualityCheckService(
|
||||
private val productAnalysis: (Int) -> ProductAnalysis? = { null },
|
||||
private val reanalysis: AnalysisStatusDto? = null,
|
||||
private val statusProgress: () -> AnalysisStatusProgressDto? = { null },
|
||||
private val productRecommendation: () -> ProductRecommendation? = { null },
|
||||
private val report: ReportBackInStockStatusDto? = null,
|
||||
) : ReviewQualityCheckService {
|
||||
|
||||
private var analysisCount = 0
|
||||
|
||||
override suspend fun fetchProductReview(): ProductAnalysis? {
|
||||
return productAnalysis(analysisCount).also {
|
||||
analysisCount++
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reanalyzeProduct(): AnalysisStatusDto? = reanalysis
|
||||
|
||||
override suspend fun analysisStatus(): AnalysisStatusProgressDto? {
|
||||
return statusProgress.invoke()
|
||||
}
|
||||
|
||||
override suspend fun productRecommendation(shouldRecordAvailableTelemetry: Boolean): ProductRecommendation? {
|
||||
return productRecommendation.invoke()
|
||||
}
|
||||
|
||||
override suspend fun reportBackInStock(): ReportBackInStockStatusDto? = report
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.fake
|
||||
|
||||
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckTelemetryService
|
||||
|
||||
class FakeReviewQualityCheckTelemetryService(
|
||||
private val recordClick: (String) -> Unit = {},
|
||||
private val recordImpression: (String) -> Unit = {},
|
||||
) : ReviewQualityCheckTelemetryService {
|
||||
|
||||
override suspend fun recordRecommendedProductClick(productAid: String) {
|
||||
return recordClick.invoke(productAid)
|
||||
}
|
||||
|
||||
override suspend fun recordRecommendedProductImpression(productAid: String) {
|
||||
return recordImpression.invoke(productAid)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.fake
|
||||
|
||||
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckVendorsService
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor
|
||||
|
||||
class FakeReviewQualityCheckVendorsService(
|
||||
private val selectedTabUrl: String? = null,
|
||||
private val productVendors: List<ProductVendor> = listOf(
|
||||
ProductVendor.BEST_BUY,
|
||||
ProductVendor.WALMART,
|
||||
ProductVendor.AMAZON,
|
||||
),
|
||||
) : ReviewQualityCheckVendorsService {
|
||||
override fun selectedTabUrl(): String? = selectedTabUrl
|
||||
|
||||
override fun productVendors(): List<ProductVendor> = productVendors
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.fake
|
||||
|
||||
import org.mozilla.fenix.shopping.ShoppingExperienceFeature
|
||||
|
||||
class FakeShoppingExperienceFeature(
|
||||
private val enabled: Boolean = true,
|
||||
private val productRecommendationsExposureEnabled: Boolean = true,
|
||||
) : ShoppingExperienceFeature {
|
||||
|
||||
override val isEnabled: Boolean
|
||||
get() = enabled
|
||||
|
||||
override val isProductRecommendationsExposureEnabled: Boolean
|
||||
get() = productRecommendationsExposureEnabled
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.shopping.ProductAnalysis
|
||||
import mozilla.components.concept.engine.shopping.ProductRecommendation
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.GleanMetrics.Shopping
|
||||
import org.mozilla.fenix.helpers.FenixGleanTestRule
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.shopping.ProductAnalysisTestData
|
||||
import org.mozilla.fenix.shopping.ProductRecommendationTestData
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class DefaultReviewQualityCheckServiceTest {
|
||||
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule()
|
||||
|
||||
@get:Rule
|
||||
val gleanTestRule = FenixGleanTestRule(testContext)
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch is called WHEN onResult is invoked with the expected type THEN product analysis returns the same data`() =
|
||||
runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
val expected = ProductAnalysisTestData.productAnalysis()
|
||||
|
||||
every {
|
||||
engineSession.requestProductAnalysis(any(), any(), any())
|
||||
}.answers {
|
||||
secondArg<(ProductAnalysis) -> Unit>().invoke(expected)
|
||||
}
|
||||
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "test-tab",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.fetchProductReview()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch is called WHEN onException is invoked THEN product analysis returns null`() =
|
||||
runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
|
||||
every {
|
||||
engineSession.requestProductAnalysis(any(), any(), any())
|
||||
}.answers {
|
||||
thirdArg<(Throwable) -> Unit>().invoke(RuntimeException())
|
||||
}
|
||||
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "test-tab",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
assertNull(tested.fetchProductReview())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN fetch is called THEN fetch is called for the selected tab`() = runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
val expected = ProductAnalysisTestData.productAnalysis()
|
||||
|
||||
every {
|
||||
engineSession.requestProductAnalysis(any(), any(), any())
|
||||
}.answers {
|
||||
secondArg<(ProductAnalysis) -> Unit>().invoke(expected)
|
||||
}
|
||||
|
||||
val tab1 = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "1",
|
||||
)
|
||||
val tab2 = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "2",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab1, tab2),
|
||||
selectedTabId = tab2.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.fetchProductReview()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN product recommendations is called WHEN onResult is invoked with the result THEN recommendations returns the data and exposure is called`() =
|
||||
runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
val expected = ProductRecommendationTestData.productRecommendation()
|
||||
val productRecommendations = listOf(expected)
|
||||
|
||||
every {
|
||||
engineSession.requestProductRecommendations(any(), any(), any())
|
||||
}.answers {
|
||||
secondArg<(List<ProductRecommendation>) -> Unit>().invoke(productRecommendations)
|
||||
}
|
||||
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "test-tab",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productRecommendation(false)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
assertNotNull(Shopping.adsExposure.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN product recommendations is called WHEN onResult is invoked with a empty list and telemetry should be recorded THEN recommendations returns null and no ads available event is called`() =
|
||||
runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
|
||||
every {
|
||||
engineSession.requestProductRecommendations(any(), any(), any())
|
||||
}.answers {
|
||||
secondArg<(List<ProductRecommendation>) -> Unit>().invoke(emptyList())
|
||||
}
|
||||
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "test-tab",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productRecommendation(true)
|
||||
|
||||
assertNull(actual)
|
||||
assertNotNull(Shopping.surfaceNoAdsAvailable.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN product recommendations is called WHEN onResult is invoked with a empty list and telemetry should not be recorded THEN recommendations returns null and no ads available event is not called`() =
|
||||
runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
|
||||
every {
|
||||
engineSession.requestProductRecommendations(any(), any(), any())
|
||||
}.answers {
|
||||
secondArg<(List<ProductRecommendation>) -> Unit>().invoke(emptyList())
|
||||
}
|
||||
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "test-tab",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productRecommendation(false)
|
||||
|
||||
assertNull(actual)
|
||||
assertNull(Shopping.surfaceNoAdsAvailable.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN product recommendations is called WHEN onException is invoked THEN recommendations returns null`() =
|
||||
runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
|
||||
every {
|
||||
engineSession.requestProductRecommendations(any(), any(), any())
|
||||
}.answers {
|
||||
thirdArg<(Throwable) -> Unit>().invoke(RuntimeException())
|
||||
}
|
||||
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "test-tab",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productRecommendation(false)
|
||||
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN product recommendations is called WHEN onResult is invoked with the result THEN recommendations returns the same result without re-fetching again`() =
|
||||
runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
val expected = ProductRecommendationTestData.productRecommendation()
|
||||
val productRecommendations = listOf(expected)
|
||||
|
||||
every {
|
||||
engineSession.requestProductRecommendations(any(), any(), any())
|
||||
}.answers {
|
||||
secondArg<(List<ProductRecommendation>) -> Unit>().invoke(productRecommendations)
|
||||
}
|
||||
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "test-tab",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
tested.productRecommendation(false)
|
||||
tested.productRecommendation(false)
|
||||
val actual = tested.productRecommendation(false)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
|
||||
verify(exactly = 1) {
|
||||
engineSession.requestProductRecommendations(any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN product recommendations is called WHEN onResult is invoked with the empty result THEN recommendations fetches every time`() =
|
||||
runTest {
|
||||
val engineSession = mockk<EngineSession>()
|
||||
|
||||
every {
|
||||
engineSession.requestProductRecommendations(any(), any(), any())
|
||||
}.answers {
|
||||
secondArg<(List<ProductRecommendation>) -> Unit>().invoke(emptyList())
|
||||
}
|
||||
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.org/product",
|
||||
id = "test-tab",
|
||||
engineSession = engineSession,
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckService(BrowserStore(browserState))
|
||||
|
||||
tested.productRecommendation(false)
|
||||
tested.productRecommendation(false)
|
||||
val actual = tested.productRecommendation(false)
|
||||
|
||||
assertNull(actual)
|
||||
|
||||
verify(exactly = 3) {
|
||||
engineSession.requestProductRecommendations(any(), any(), any())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.ProductVendor
|
||||
|
||||
class DefaultReviewQualityCheckVendorsServiceTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN selected tab is an amazon_com page THEN amazon is first in product vendors list`() =
|
||||
runTest {
|
||||
val tab = createTab(
|
||||
url = "https://www.amazon.com/product",
|
||||
id = "test-tab",
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productVendors()
|
||||
val expected = listOf(
|
||||
ProductVendor.AMAZON,
|
||||
ProductVendor.BEST_BUY,
|
||||
ProductVendor.WALMART,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN selected tab is a walmart page THEN walmart is first in product vendors list`() =
|
||||
runTest {
|
||||
val tab = createTab(
|
||||
url = "https://www.walmart.com/product",
|
||||
id = "test-tab",
|
||||
)
|
||||
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productVendors()
|
||||
val expected = listOf(
|
||||
ProductVendor.WALMART,
|
||||
ProductVendor.AMAZON,
|
||||
ProductVendor.BEST_BUY,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN selected tab is a best buy page THEN best buy is first in product vendors list`() =
|
||||
runTest {
|
||||
val tab = createTab(
|
||||
url = "https://www.bestbuy.com/product",
|
||||
id = "test-tab",
|
||||
)
|
||||
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productVendors()
|
||||
val expected = listOf(
|
||||
ProductVendor.BEST_BUY,
|
||||
ProductVendor.AMAZON,
|
||||
ProductVendor.WALMART,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN selected tab is a not a vendor page THEN default product vendors list is returned`() =
|
||||
runTest {
|
||||
val tab = createTab(
|
||||
url = "https://www.shopping.xyz/product",
|
||||
id = "test-tab",
|
||||
)
|
||||
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productVendors()
|
||||
val expected = listOf(
|
||||
ProductVendor.AMAZON,
|
||||
ProductVendor.BEST_BUY,
|
||||
ProductVendor.WALMART,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN selected tab is a not a valid uri THEN default product vendors list is returned`() =
|
||||
runTest {
|
||||
val tab = createTab(
|
||||
url = "not a url",
|
||||
id = "test-tab",
|
||||
)
|
||||
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productVendors()
|
||||
val expected = listOf(
|
||||
ProductVendor.AMAZON,
|
||||
ProductVendor.BEST_BUY,
|
||||
ProductVendor.WALMART,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN selected tab is an amazon_de page THEN amazon is first in product vendors list`() =
|
||||
runTest {
|
||||
val tab = createTab(
|
||||
url = "https://www.amazon.de/product",
|
||||
id = "test-tab",
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productVendors()
|
||||
val expected = listOf(ProductVendor.AMAZON)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN selected tab is an amazon_fr page THEN amazon is first in product vendors list`() =
|
||||
runTest {
|
||||
val tab = createTab(
|
||||
url = "https://www.amazon.fr/product",
|
||||
id = "test-tab",
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productVendors()
|
||||
val expected = listOf(ProductVendor.AMAZON)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN selected tab is an amazon_in page THEN default product vendors list is returned`() =
|
||||
runTest {
|
||||
val tab = createTab(
|
||||
url = "https://www.amazon.in/product",
|
||||
id = "test-tab",
|
||||
)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab),
|
||||
selectedTabId = tab.id,
|
||||
)
|
||||
|
||||
val tested = DefaultReviewQualityCheckVendorsService(BrowserStore(browserState))
|
||||
|
||||
val actual = tested.productVendors()
|
||||
val expected = listOf(
|
||||
ProductVendor.AMAZON,
|
||||
ProductVendor.BEST_BUY,
|
||||
ProductVendor.WALMART,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class EnumMapperTest {
|
||||
|
||||
private enum class DeviceType {
|
||||
PHONE,
|
||||
TABLET,
|
||||
WEARABLE,
|
||||
OTHER_TYPE,
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN an enum WHEN a string is an enum object THEN it is mapped to the enum`() {
|
||||
val phone = "phone"
|
||||
val tablet = "TABLET"
|
||||
val wearable = "WEARABLE"
|
||||
val other = "other_type"
|
||||
val otherWithSpace = "other type"
|
||||
|
||||
assertEquals(DeviceType.PHONE, phone.asEnumOrDefault<DeviceType>())
|
||||
assertEquals(DeviceType.TABLET, tablet.asEnumOrDefault<DeviceType>())
|
||||
assertEquals(DeviceType.WEARABLE, wearable.asEnumOrDefault<DeviceType>())
|
||||
assertEquals(DeviceType.OTHER_TYPE, other.asEnumOrDefault<DeviceType>())
|
||||
assertEquals(DeviceType.OTHER_TYPE, otherWithSpace.asEnumOrDefault<DeviceType>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN an enum WHEN a string is not an enum object and not default is passed THEN null is returned`() {
|
||||
val input = "car"
|
||||
|
||||
assertNull(input.asEnumOrDefault<DeviceType>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN an enum WHEN a string is not an enum object and default is passed THEN default is returned`() {
|
||||
val input = "car"
|
||||
|
||||
assertEquals(DeviceType.OTHER_TYPE, input.asEnumOrDefault(DeviceType.OTHER_TYPE))
|
||||
}
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import mozilla.components.concept.engine.shopping.Highlight
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.shopping.ProductAnalysisTestData
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.HighlightType
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.HighlightsInfo
|
||||
|
||||
class ProductAnalysisMapperTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN ProductAnalysis has data THEN it is mapped to AnalysisPresent`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
productId = "id1",
|
||||
grade = "C",
|
||||
needsAnalysis = false,
|
||||
adjustedRating = 3.4,
|
||||
analysisURL = "https://example.com",
|
||||
highlights = Highlight(
|
||||
quality = listOf("quality"),
|
||||
price = listOf("price"),
|
||||
shipping = listOf("shipping"),
|
||||
appearance = listOf("appearance"),
|
||||
competitiveness = listOf("competitiveness"),
|
||||
),
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
productId = "id1",
|
||||
reviewGrade = ReviewQualityCheckState.Grade.C,
|
||||
analysisStatus = AnalysisStatus.UpToDate,
|
||||
adjustedRating = 3.4f,
|
||||
productUrl = "https://example.com",
|
||||
highlightsInfo = HighlightsInfo(
|
||||
mapOf(
|
||||
HighlightType.QUALITY to listOf("quality"),
|
||||
HighlightType.PRICE to listOf("price"),
|
||||
HighlightType.SHIPPING to listOf("shipping"),
|
||||
HighlightType.PACKAGING_AND_APPEARANCE to listOf("appearance"),
|
||||
HighlightType.COMPETITIVENESS to listOf("competitiveness"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN ProductAnalysis has data with some missing highlights THEN it is mapped to AnalysisPresent with the non null highlights`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
productId = "id1",
|
||||
grade = "C",
|
||||
needsAnalysis = true,
|
||||
adjustedRating = 3.4,
|
||||
analysisURL = "https://example.com",
|
||||
highlights = Highlight(
|
||||
quality = listOf("quality"),
|
||||
price = null,
|
||||
shipping = null,
|
||||
appearance = listOf("appearance"),
|
||||
competitiveness = listOf("competitiveness"),
|
||||
),
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
productId = "id1",
|
||||
reviewGrade = ReviewQualityCheckState.Grade.C,
|
||||
analysisStatus = AnalysisStatus.NeedsAnalysis,
|
||||
adjustedRating = 3.4f,
|
||||
productUrl = "https://example.com",
|
||||
highlightsInfo = HighlightsInfo(
|
||||
mapOf(
|
||||
HighlightType.QUALITY to listOf("quality"),
|
||||
HighlightType.PACKAGING_AND_APPEARANCE to listOf("appearance"),
|
||||
HighlightType.COMPETITIVENESS to listOf("competitiveness"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN ProductAnalysis has an invalid grade THEN it is mapped to AnalysisPresent with grade as null`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
productId = "id1",
|
||||
grade = "?",
|
||||
needsAnalysis = false,
|
||||
adjustedRating = 3.4,
|
||||
analysisURL = "https://example.com",
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
productId = "id1",
|
||||
reviewGrade = null,
|
||||
analysisStatus = AnalysisStatus.UpToDate,
|
||||
adjustedRating = 3.4f,
|
||||
productUrl = "https://example.com",
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN product analysis is null THEN it is mapped to Error`() {
|
||||
val actual = null.toProductReviewState()
|
||||
val expected = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.GenericError
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN product id is null and needs analysis is true THEN it is mapped to no analysis present`() {
|
||||
val actual =
|
||||
ProductAnalysisTestData.productAnalysis(
|
||||
productId = null,
|
||||
needsAnalysis = true,
|
||||
).toProductReviewState()
|
||||
val expected = ReviewQualityCheckState.OptedIn.ProductReviewState.NoAnalysisPresent()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN product id is null and needs analysis is false THEN it is mapped to no generic error`() {
|
||||
val actual =
|
||||
ProductAnalysisTestData.productAnalysis(
|
||||
productId = null,
|
||||
needsAnalysis = false,
|
||||
).toProductReviewState()
|
||||
val expected = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.GenericError
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN there are not enough reviews and no analysis needed THEN not enough reviews card is visible`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
notEnoughReviews = true,
|
||||
needsAnalysis = false,
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.NotEnoughReviews
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN there are enough reviews and no analysis needed THEN it is mapped to AnalysisPresent`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
notEnoughReviews = false,
|
||||
needsAnalysis = false,
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
productId = "1",
|
||||
reviewGrade = ReviewQualityCheckState.Grade.A,
|
||||
analysisStatus = AnalysisStatus.UpToDate,
|
||||
adjustedRating = 4.5f,
|
||||
productUrl = "https://test.com",
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN there are not enough reviews and analysis is needed THEN it is mapped to AnalysisPresent with NEEDS_ANALYSIS status`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
notEnoughReviews = true,
|
||||
needsAnalysis = true,
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
productId = "1",
|
||||
reviewGrade = ReviewQualityCheckState.Grade.A,
|
||||
analysisStatus = AnalysisStatus.NeedsAnalysis,
|
||||
adjustedRating = 4.5f,
|
||||
productUrl = "https://test.com",
|
||||
highlightsInfo = null,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN grade, rating and highlights are all null THEN it is mapped to no analysis present`() {
|
||||
val actual =
|
||||
ProductAnalysisTestData.productAnalysis(
|
||||
grade = null,
|
||||
adjustedRating = null,
|
||||
highlights = null,
|
||||
).toProductReviewState()
|
||||
val expected = ReviewQualityCheckState.OptedIn.ProductReviewState.NoAnalysisPresent()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN only rating is available THEN it is mapped to AnalysisPresent`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
grade = null,
|
||||
adjustedRating = 3.5,
|
||||
highlights = null,
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = null,
|
||||
adjustedRating = 3.5f,
|
||||
highlightsInfo = null,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN only grade is available THEN it is mapped to AnalysisPresent`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
grade = "B",
|
||||
adjustedRating = null,
|
||||
highlights = null,
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = ReviewQualityCheckState.Grade.B,
|
||||
adjustedRating = null,
|
||||
highlightsInfo = null,
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN only highlights are available THEN it is mapped to AnalysisPresent`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
grade = null,
|
||||
adjustedRating = null,
|
||||
highlights = Highlight(
|
||||
quality = listOf("quality"),
|
||||
price = null,
|
||||
shipping = null,
|
||||
appearance = listOf("appearance"),
|
||||
competitiveness = listOf("competitiveness"),
|
||||
),
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = null,
|
||||
adjustedRating = null,
|
||||
highlightsInfo = HighlightsInfo(
|
||||
mapOf(
|
||||
HighlightType.QUALITY to listOf("quality"),
|
||||
HighlightType.PACKAGING_AND_APPEARANCE to listOf("appearance"),
|
||||
HighlightType.COMPETITIVENESS to listOf("competitiveness"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN highlights and grade are available THEN it is mapped to AnalysisPresent`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
grade = "B",
|
||||
adjustedRating = null,
|
||||
highlights = Highlight(
|
||||
quality = listOf("quality"),
|
||||
price = null,
|
||||
shipping = null,
|
||||
appearance = listOf("appearance"),
|
||||
competitiveness = listOf("competitiveness"),
|
||||
),
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = ReviewQualityCheckState.Grade.B,
|
||||
adjustedRating = null,
|
||||
highlightsInfo = HighlightsInfo(
|
||||
mapOf(
|
||||
HighlightType.QUALITY to listOf("quality"),
|
||||
HighlightType.PACKAGING_AND_APPEARANCE to listOf("appearance"),
|
||||
HighlightType.COMPETITIVENESS to listOf("competitiveness"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN highlights and rating are available THEN it is mapped to AnalysisPresent`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
grade = null,
|
||||
adjustedRating = 3.4,
|
||||
highlights = Highlight(
|
||||
quality = listOf("quality"),
|
||||
price = null,
|
||||
shipping = null,
|
||||
appearance = listOf("appearance"),
|
||||
competitiveness = listOf("competitiveness"),
|
||||
),
|
||||
).toProductReviewState()
|
||||
|
||||
val expected = ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = null,
|
||||
adjustedRating = 3.4f,
|
||||
highlightsInfo = HighlightsInfo(
|
||||
mapOf(
|
||||
HighlightType.QUALITY to listOf("quality"),
|
||||
HighlightType.PACKAGING_AND_APPEARANCE to listOf("appearance"),
|
||||
HighlightType.COMPETITIVENESS to listOf("competitiveness"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN page not supported is true THEN it is mapped to unsupported product error `() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
pageNotSupported = true,
|
||||
).toProductReviewState()
|
||||
|
||||
val expected =
|
||||
ReviewQualityCheckState.OptedIn.ProductReviewState.Error.UnsupportedProductTypeError
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN product deleted is true and has not been reported THEN it is mapped to not available and not back in stock`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
deletedProduct = true,
|
||||
deletedProductReported = false,
|
||||
).toProductReviewState()
|
||||
|
||||
val expected =
|
||||
ReviewQualityCheckState.OptedIn.ProductReviewState.Error.ProductNotAvailable
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN product deleted is true and has been reported THEN it is mapped to not available and back in stock`() {
|
||||
val actual = ProductAnalysisTestData.productAnalysis(
|
||||
deletedProduct = true,
|
||||
deletedProductReported = true,
|
||||
).toProductReviewState()
|
||||
|
||||
val expected =
|
||||
ReviewQualityCheckState.OptedIn.ProductReviewState.Error.ProductAlreadyReported
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.helpers.LocaleTestRule
|
||||
import org.mozilla.fenix.shopping.ProductRecommendationTestData
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import java.util.Locale
|
||||
|
||||
class ProductRecommendationMapperTest {
|
||||
|
||||
@get:Rule
|
||||
val localeTestRule = LocaleTestRule(Locale.US)
|
||||
|
||||
@Test
|
||||
fun `WHEN ProductRecommendation is null THEN it is mapped to Initial`() {
|
||||
val actual = null.toRecommendedProductState()
|
||||
val expected = ReviewQualityCheckState.RecommendedProductState.Initial
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN ProductRecommendation has data THEN it is mapped to product`() {
|
||||
val productRecommendation = ProductRecommendationTestData.productRecommendation()
|
||||
val actual = productRecommendation.toRecommendedProductState()
|
||||
val expected = ProductRecommendationTestData.product()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN ProductRecommendation has data with invalid currency code THEN it is mapped to product`() {
|
||||
val productRecommendation = ProductRecommendationTestData.productRecommendation(
|
||||
price = "100",
|
||||
currency = "invalid",
|
||||
)
|
||||
val actual = productRecommendation.toRecommendedProductState()
|
||||
val expected = ProductRecommendationTestData.product(
|
||||
formattedPrice = "100",
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class RetryKtTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN predicate is false THEN return data on first attempt`() = runTest {
|
||||
var count = 0
|
||||
|
||||
val actual = retry(predicate = { false }) {
|
||||
count += 1
|
||||
count
|
||||
}
|
||||
val expected = 1
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN predicate is true THEN retry max times`() = runTest {
|
||||
var count = 0
|
||||
|
||||
val actual = retry(
|
||||
maxRetries = 10,
|
||||
predicate = { true },
|
||||
) {
|
||||
count += 1
|
||||
count
|
||||
}
|
||||
|
||||
val expected = 10
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN predicate changes to false from true THEN return data on that attempt`() = runTest {
|
||||
var count = 0
|
||||
|
||||
val actual = retry(
|
||||
maxRetries = 10,
|
||||
predicate = { it < 5 },
|
||||
) {
|
||||
count += 1
|
||||
count
|
||||
}
|
||||
|
||||
val expected = 5
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
|
||||
|
||||
class ReviewQualityCheckNavigationMiddlewareTest {
|
||||
|
||||
private val sumoUrl = "https://support.mozilla.org/en-US/products/mobile"
|
||||
private lateinit var store: ReviewQualityCheckStore
|
||||
private lateinit var browserStore: BrowserStore
|
||||
private lateinit var addTabUseCase: TabsUseCases.SelectOrAddUseCase
|
||||
private lateinit var middleware: ReviewQualityCheckNavigationMiddleware
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
browserStore = BrowserStore()
|
||||
addTabUseCase = mockk(relaxed = true)
|
||||
middleware = ReviewQualityCheckNavigationMiddleware(
|
||||
selectOrAddUseCase = addTabUseCase,
|
||||
getReviewQualityCheckSumoUrl = mockk {
|
||||
every { this@mockk.invoke() } returns sumoUrl
|
||||
},
|
||||
)
|
||||
store = ReviewQualityCheckStore(
|
||||
middleware = listOf(middleware),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN opening an external link THEN the link should be opened in a new tab`() {
|
||||
val action = ReviewQualityCheckAction.OpenExplainerLearnMoreLink
|
||||
store.waitUntilIdle()
|
||||
assertEquals(0, browserStore.state.tabs.size)
|
||||
|
||||
store.dispatch(action).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
verify {
|
||||
addTabUseCase.invoke(sumoUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,512 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.middleware
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.GleanMetrics.Shopping
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.components.appstate.shopping.ShoppingState
|
||||
import org.mozilla.fenix.helpers.FenixGleanTestRule
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.shopping.ProductAnalysisTestData
|
||||
import org.mozilla.fenix.shopping.fake.FakeReviewQualityCheckTelemetryService
|
||||
import org.mozilla.fenix.shopping.store.BottomSheetDismissSource
|
||||
import org.mozilla.fenix.shopping.store.BottomSheetViewState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class ReviewQualityCheckTelemetryMiddlewareTest {
|
||||
|
||||
@get:Rule
|
||||
val gleanTestRule = FenixGleanTestRule(testContext)
|
||||
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule()
|
||||
|
||||
private lateinit var store: ReviewQualityCheckStore
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
store = ReviewQualityCheckStore(
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
store.waitUntilIdle()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the user opts in the feature THEN the opt in event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.OptIn).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceOptInAccepted.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the bottom sheet is closed THEN the bottom sheet closed event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.BottomSheetClosed(BottomSheetDismissSource.CLICK_OUTSIDE))
|
||||
.joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceClosed.testGetValue())
|
||||
val event = Shopping.surfaceClosed.testGetValue()!!
|
||||
assertEquals(1, event.size)
|
||||
assertEquals(
|
||||
BottomSheetDismissSource.CLICK_OUTSIDE.sourceName,
|
||||
event.single().extra?.getValue("source"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the bottom sheet is displayed THEN the bottom sheet displayed event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.BottomSheetDisplayed(BottomSheetViewState.HALF_VIEW))
|
||||
.joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceDisplayed.testGetValue())
|
||||
val event = Shopping.surfaceDisplayed.testGetValue()!!
|
||||
assertEquals(1, event.size)
|
||||
assertEquals(BottomSheetViewState.HALF_VIEW.state, event.single().extra?.getValue("view"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the learn more link from the explainer card is clicked THEN the explainer learn more event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.OpenExplainerLearnMoreLink).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceReviewQualityExplainerUrlClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the terms and conditions link from the onboarding card is clicked THEN the terms and conditions event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.OpenOnboardingTermsLink).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceShowTermsClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the privacy policy link from the onboarding card is clicked THEN the privacy policy event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceShowPrivacyPolicyClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the learn more link from the onboarding card is clicked THEN the onboarding learn more event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.OpenOnboardingLearnMoreLink).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceLearnMoreClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the not now button from the onboarding card is clicked THEN the not now event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.NotNowClicked).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceNotNowClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the expand button from the highlights card is clicked THEN the show more recent reviews event is recorded`() {
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = true,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
isHighlightsExpanded = false,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.ExpandCollapseHighlights).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceShowMoreRecentReviewsClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the collapse button from the highlights card is clicked THEN the show more recent reviews event is not recorded`() {
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = true,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
isHighlightsExpanded = true,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.ExpandCollapseHighlights).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNull(Shopping.surfaceShowMoreRecentReviewsClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the expand button from the settings card is clicked THEN the settings expand event is recorded`() {
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = true,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
isSettingsExpanded = false,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.ExpandCollapseSettings).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceExpandSettings.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the collapse button from the settings card is clicked THEN the settings expand event is not recorded`() {
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = true,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
isSettingsExpanded = true,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.ExpandCollapseSettings).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNull(Shopping.surfaceExpandSettings.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN no analysis is present THEN the no analysis event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.NoAnalysisDisplayed).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceNoReviewReliabilityAvailable.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN analyze button is clicked THEN the analyze reviews event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.AnalyzeProduct).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceAnalyzeReviewsNoneAvailableClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN reanalyze button is clicked THEN the reanalyze event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.ReanalyzeProduct).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceReanalyzeClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN back in stock button is clicked THEN the reactivate event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.ReportProductBackInStock).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceReactivatedButtonClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the user is opted out after initializing the feature after THEN the onboarding displayed event is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.OptOutCompleted(emptyList())).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceOnboardingDisplayed.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the user is tapped the 'Powered by Fakespot by Mozilla' link THEN the link clicked telemetry is recorded`() {
|
||||
store.dispatch(ReviewQualityCheckAction.OpenPoweredByLink).joinBlocking()
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfacePoweredByFakespotLinkClicked.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a product review has been updated WHEN restore analysis is false THEN the stale analysis event is recorded`() {
|
||||
val productReviewState = ProductAnalysisTestData.analysisPresent(
|
||||
analysisStatus = AnalysisPresent.AnalysisStatus.NeedsAnalysis,
|
||||
)
|
||||
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = null,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.BEST_BUY,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
|
||||
tested.dispatch(
|
||||
ReviewQualityCheckAction.UpdateProductReview(
|
||||
productReviewState = productReviewState,
|
||||
restoreAnalysis = false,
|
||||
),
|
||||
).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceStaleAnalysisShown.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a product review has been updated WHEN restore analysis is true THEN the stale analysis event is not recorded`() {
|
||||
val productReviewState = ProductAnalysisTestData.analysisPresent(
|
||||
analysisStatus = AnalysisPresent.AnalysisStatus.NeedsAnalysis,
|
||||
)
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = null,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.BEST_BUY,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
|
||||
tested.dispatch(
|
||||
ReviewQualityCheckAction.UpdateProductReview(
|
||||
productReviewState = productReviewState,
|
||||
restoreAnalysis = true,
|
||||
),
|
||||
).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNull(Shopping.surfaceStaleAnalysisShown.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a product review has been updated WHEN it is not a stale analysis THEN the stale analysis event is not recorded`() {
|
||||
val productReviewState = ProductAnalysisTestData.analysisPresent()
|
||||
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = null,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.BEST_BUY,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
|
||||
tested.dispatch(
|
||||
ReviewQualityCheckAction.UpdateProductReview(
|
||||
productReviewState = productReviewState,
|
||||
restoreAnalysis = true,
|
||||
),
|
||||
).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNull(Shopping.surfaceStaleAnalysisShown.testGetValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a recommendation impression action is dispatched WHEN app state does not contain key with tab id, product url and aid THEN ad impression telemetry probe is sent`() =
|
||||
runTest {
|
||||
var productViewed: String? = null
|
||||
val tested = ReviewQualityCheckStore(
|
||||
middleware = provideTelemetryMiddleware(
|
||||
reviewQualityCheckTelemetryService = FakeReviewQualityCheckTelemetryService(
|
||||
recordImpression = {
|
||||
productViewed = it
|
||||
},
|
||||
),
|
||||
browserState = BrowserState(
|
||||
selectedTabId = "tabId",
|
||||
tabs = listOf(
|
||||
createTab(
|
||||
id = "tabId",
|
||||
url = "pdp",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.RecommendedProductImpression("productId"))
|
||||
.joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceAdsImpression.testGetValue())
|
||||
assertEquals("productId", productViewed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN recommendation impression action is dispatched many times and app state does not initially contain key with tab id, product url and aid THEN ad impression telemetry probe is sent only once`() =
|
||||
runTest {
|
||||
var productViewed: String? = null
|
||||
var impressionCount = 0
|
||||
val appStore = AppStore()
|
||||
val tested = ReviewQualityCheckStore(
|
||||
middleware = provideTelemetryMiddleware(
|
||||
reviewQualityCheckTelemetryService = FakeReviewQualityCheckTelemetryService(
|
||||
recordImpression = {
|
||||
productViewed = it
|
||||
impressionCount++
|
||||
},
|
||||
),
|
||||
browserState = BrowserState(
|
||||
selectedTabId = "tabId",
|
||||
tabs = listOf(
|
||||
createTab(
|
||||
id = "tabId",
|
||||
url = "pdp",
|
||||
),
|
||||
),
|
||||
),
|
||||
appStore = appStore,
|
||||
),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
for (i in 1..100) {
|
||||
tested.dispatch(ReviewQualityCheckAction.RecommendedProductImpression("productId"))
|
||||
.joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
appStore.waitUntilIdle()
|
||||
}
|
||||
|
||||
assertNotNull(Shopping.surfaceAdsImpression.testGetValue())
|
||||
assertEquals("productId", productViewed)
|
||||
assertEquals(1, impressionCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a recommendation impression action is dispatched WHEN app state contains key with tab id, product url and aid THEN ad impression telemetry probe is NOT sent`() =
|
||||
runTest {
|
||||
var productViewed: String? = null
|
||||
val tested = ReviewQualityCheckStore(
|
||||
middleware = provideTelemetryMiddleware(
|
||||
reviewQualityCheckTelemetryService = FakeReviewQualityCheckTelemetryService(
|
||||
recordImpression = { productViewed = it },
|
||||
),
|
||||
browserState = BrowserState(
|
||||
selectedTabId = "tabId",
|
||||
tabs = listOf(
|
||||
createTab(
|
||||
id = "tabId",
|
||||
url = "pdp",
|
||||
),
|
||||
),
|
||||
),
|
||||
appStore = AppStore(
|
||||
AppState(
|
||||
shoppingState = ShoppingState(
|
||||
recordedProductRecommendationImpressions = setOf(
|
||||
ShoppingState.ProductRecommendationImpressionKey(
|
||||
tabId = "tabId",
|
||||
productUrl = "pdp",
|
||||
aid = "productId",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.RecommendedProductImpression("productId"))
|
||||
.joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNull(Shopping.surfaceAdsImpression.testGetValue())
|
||||
assertNull(productViewed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN a product recommendation is clicked THEN the ad clicked telemetry probe is sent`() =
|
||||
runTest {
|
||||
var productClicked: String? = null
|
||||
val tested = ReviewQualityCheckStore(
|
||||
middleware = provideTelemetryMiddleware(
|
||||
reviewQualityCheckTelemetryService = FakeReviewQualityCheckTelemetryService(
|
||||
recordClick = { productClicked = it },
|
||||
),
|
||||
),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.RecommendedProductClick("productId", ""))
|
||||
.joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertNotNull(Shopping.surfaceAdsClicked.testGetValue())
|
||||
assertEquals("productId", productClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the user has opted in WHEN the user switches product recommendations on THEN send enabled product recommendations toggled telemetry probe`() {
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = false,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
isHighlightsExpanded = false,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
"enabled",
|
||||
Shopping.surfaceAdsSettingToggled.testGetValue()!!.first().extra!!["action"],
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the user has opted in WHEN the user switches product recommendations off THEN send disabled product recommendations toggled telemetry probe`() {
|
||||
val tested = ReviewQualityCheckStore(
|
||||
initialState = ReviewQualityCheckState.OptedIn(
|
||||
productRecommendationsPreference = true,
|
||||
productRecommendationsExposure = true,
|
||||
productVendor = ReviewQualityCheckState.ProductVendor.AMAZON,
|
||||
isHighlightsExpanded = false,
|
||||
),
|
||||
middleware = provideTelemetryMiddleware(),
|
||||
)
|
||||
tested.waitUntilIdle()
|
||||
tested.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation).joinBlocking()
|
||||
tested.waitUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
"disabled",
|
||||
Shopping.surfaceAdsSettingToggled.testGetValue()!!.first().extra!!["action"],
|
||||
)
|
||||
}
|
||||
|
||||
private fun provideTelemetryMiddleware(
|
||||
reviewQualityCheckTelemetryService: FakeReviewQualityCheckTelemetryService = FakeReviewQualityCheckTelemetryService(),
|
||||
browserState: BrowserState = BrowserState(),
|
||||
appStore: AppStore = AppStore(),
|
||||
) = listOf(
|
||||
ReviewQualityCheckTelemetryMiddleware(
|
||||
reviewQualityCheckTelemetryService,
|
||||
BrowserStore(browserState),
|
||||
appStore,
|
||||
coroutinesTestRule.scope,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.store
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.shopping.ProductAnalysisTestData
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.HighlightsInfo
|
||||
import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.Progress
|
||||
|
||||
class ReviewQualityCheckStateTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN highlights are present THEN highlights to display in compact mode should contain first 2 highlights of the first highlight type`() {
|
||||
val highlights = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.QUALITY to listOf(
|
||||
"High quality",
|
||||
"Excellent craftsmanship",
|
||||
"Superior materials",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.PRICE to listOf(
|
||||
"Affordable prices",
|
||||
"Great value for money",
|
||||
"Discounted offers",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.SHIPPING to listOf(
|
||||
"Fast and reliable shipping",
|
||||
"Free shipping options",
|
||||
"Express delivery",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.PACKAGING_AND_APPEARANCE to listOf(
|
||||
"Elegant packaging",
|
||||
"Attractive appearance",
|
||||
"Beautiful design",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.COMPETITIVENESS to listOf(
|
||||
"Competitive pricing",
|
||||
"Strong market presence",
|
||||
"Unbeatable deals",
|
||||
),
|
||||
)
|
||||
val highlightsInfo = HighlightsInfo(highlights)
|
||||
|
||||
val expected = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.QUALITY to listOf(
|
||||
"High quality",
|
||||
"Excellent craftsmanship",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expected, highlightsInfo.highlightsForCompactMode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN only 1 highlight is present THEN highlights to display in compact mode should contain that one`() {
|
||||
val highlights = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.PRICE to listOf(
|
||||
"Affordable prices",
|
||||
),
|
||||
)
|
||||
val highlightsInfo = HighlightsInfo(highlights)
|
||||
|
||||
val expected = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.PRICE to listOf(
|
||||
"Affordable prices",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(expected, highlightsInfo.highlightsForCompactMode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN AnalysisPresent is created with grade, rating and highlights as null THEN exception is thrown`() {
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = null,
|
||||
adjustedRating = null,
|
||||
highlightsInfo = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN AnalysisPresent is created with at least one of grade, rating and highlights as not null THEN no exception is thrown`() {
|
||||
val ratingPresent = kotlin.runCatching {
|
||||
ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = null,
|
||||
adjustedRating = 1.2f,
|
||||
highlightsInfo = null,
|
||||
)
|
||||
}
|
||||
|
||||
val gradePresent = kotlin.runCatching {
|
||||
ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = ReviewQualityCheckState.Grade.A,
|
||||
adjustedRating = null,
|
||||
highlightsInfo = null,
|
||||
)
|
||||
}
|
||||
|
||||
val highlightsPresent = kotlin.runCatching {
|
||||
ProductAnalysisTestData.analysisPresent(
|
||||
reviewGrade = null,
|
||||
adjustedRating = null,
|
||||
highlightsInfo = HighlightsInfo(
|
||||
mapOf(ReviewQualityCheckState.HighlightType.QUALITY to listOf("")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
assert(ratingPresent.isSuccess)
|
||||
assert(gradePresent.isSuccess)
|
||||
assert(highlightsPresent.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN AnalysisPresent has more than 2 highlights snippets THEN show more button and highlights fade are visible`() {
|
||||
val highlights = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.QUALITY to listOf(
|
||||
"High quality",
|
||||
"Excellent craftsmanship",
|
||||
"Superior materials",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.PRICE to listOf(
|
||||
"Affordable prices",
|
||||
"Great value for money",
|
||||
"Discounted offers",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.SHIPPING to listOf(
|
||||
"Fast and reliable shipping",
|
||||
"Free shipping options",
|
||||
"Express delivery",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.PACKAGING_AND_APPEARANCE to listOf(
|
||||
"Elegant packaging",
|
||||
"Attractive appearance",
|
||||
"Beautiful design",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.COMPETITIVENESS to listOf(
|
||||
"Competitive pricing",
|
||||
"Strong market presence",
|
||||
"Unbeatable deals",
|
||||
),
|
||||
)
|
||||
val analysis = ProductAnalysisTestData.analysisPresent(
|
||||
highlightsInfo = HighlightsInfo(highlights),
|
||||
)
|
||||
|
||||
assertTrue(analysis.highlightsInfo!!.highlightsFadeVisible)
|
||||
assertTrue(analysis.highlightsInfo!!.showMoreButtonVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN AnalysisPresent has exactly 1 highlights snippet THEN show more button and highlights fade are not visible`() {
|
||||
val highlights = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.PRICE to listOf("Affordable prices"),
|
||||
)
|
||||
val analysis = ProductAnalysisTestData.analysisPresent(
|
||||
highlightsInfo = HighlightsInfo(highlights),
|
||||
)
|
||||
|
||||
assertFalse(analysis.highlightsInfo!!.highlightsFadeVisible)
|
||||
assertFalse(analysis.highlightsInfo!!.showMoreButtonVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN AnalysisPresent has exactly 2 highlights snippets THEN show more button and highlights fade are not visible`() {
|
||||
val highlights = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.SHIPPING to listOf(
|
||||
"Fast and reliable shipping",
|
||||
"Free shipping options",
|
||||
),
|
||||
)
|
||||
val analysis = ProductAnalysisTestData.analysisPresent(
|
||||
highlightsInfo = HighlightsInfo(highlights),
|
||||
)
|
||||
|
||||
assertFalse(analysis.highlightsInfo!!.highlightsFadeVisible)
|
||||
assertFalse(analysis.highlightsInfo!!.showMoreButtonVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN AnalysisPresent has a single highlights section and the section has more than 2 snippets THEN show more button and highlights fade are visible`() {
|
||||
val highlights = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.SHIPPING to listOf(
|
||||
"Fast and reliable shipping",
|
||||
"Free shipping options",
|
||||
"Express delivery",
|
||||
),
|
||||
)
|
||||
val analysis = ProductAnalysisTestData.analysisPresent(
|
||||
highlightsInfo = HighlightsInfo(highlights),
|
||||
)
|
||||
|
||||
assertTrue(analysis.highlightsInfo!!.highlightsFadeVisible)
|
||||
assertTrue(analysis.highlightsInfo!!.showMoreButtonVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN AnalysisPresent has only 1 highlight snippet for the first category and more for others THEN show more button is visible and highlights fade is not visible`() {
|
||||
val highlights = mapOf(
|
||||
ReviewQualityCheckState.HighlightType.QUALITY to listOf(
|
||||
"High quality",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.PACKAGING_AND_APPEARANCE to listOf(
|
||||
"Elegant packaging",
|
||||
"Attractive appearance",
|
||||
"Beautiful design",
|
||||
),
|
||||
ReviewQualityCheckState.HighlightType.COMPETITIVENESS to listOf(
|
||||
"Competitive pricing",
|
||||
"Strong market presence",
|
||||
"Unbeatable deals",
|
||||
),
|
||||
)
|
||||
val analysis = ProductAnalysisTestData.analysisPresent(
|
||||
highlightsInfo = HighlightsInfo(highlights),
|
||||
)
|
||||
|
||||
assertTrue(analysis.highlightsInfo!!.showMoreButtonVisible)
|
||||
assertFalse(analysis.highlightsInfo!!.highlightsFadeVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN progress has a positive value THEN normalized progress should match`() {
|
||||
val progress = Progress(61.6f)
|
||||
assertEquals(0.616f, progress.normalizedProgress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN no analysis is present with progress THEN normalized progress should match and progress bar is visible`() {
|
||||
val analysis = ProductAnalysisTestData.noAnalysisPresent(progress = 61.6f)
|
||||
assertTrue(analysis.isProgressBarVisible)
|
||||
assertEquals(0.616f, analysis.progress.normalizedProgress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN no analysis is present with negative progress THEN progress bar is not visible`() {
|
||||
val analysis = ProductAnalysisTestData.noAnalysisPresent(progress = -1f)
|
||||
assertFalse(analysis.isProgressBarVisible)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.shopping.ui
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class StarRatingKtTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN a float with a decimal THEN remove the zero if present`() {
|
||||
assertEquals(3, 3.0f.removeDecimalZero())
|
||||
assertEquals(4.5f, 4.5f.removeDecimalZero())
|
||||
assertEquals(5, 5.0f.removeDecimalZero())
|
||||
assertEquals(0.2f, 0.2f.removeDecimalZero())
|
||||
assertEquals(1.00001f, 1.00001f.removeDecimalZero())
|
||||
assertEquals(4.999f, 4.999f.removeDecimalZero())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a float with a decimal THEN round its value to the nearest half`() {
|
||||
assertEquals(3.0f, 3.1f.roundToNearestHalf())
|
||||
assertEquals(4.5f, 4.6f.roundToNearestHalf())
|
||||
assertEquals(5.0f, 4.9f.roundToNearestHalf())
|
||||
assertEquals(0.5f, 0.3f.roundToNearestHalf())
|
||||
assertEquals(2.5f, 2.26f.roundToNearestHalf())
|
||||
assertEquals(2.0f, 2.25f.roundToNearestHalf())
|
||||
}
|
||||
}
|
||||